@jacobbd/relay-ai 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import pc16 from "picocolors";
4
+ import pc17 from "picocolors";
5
5
 
6
6
  // src/ui.ts
7
7
  import pc from "picocolors";
@@ -35,7 +35,7 @@ import { join } from "path";
35
35
  // package.json
36
36
  var package_default = {
37
37
  name: "@jacobbd/relay-ai",
38
- version: "0.2.8",
38
+ version: "0.3.1",
39
39
  publishConfig: {
40
40
  access: "public"
41
41
  },
@@ -455,8 +455,12 @@ function isDeepSeekReasoningModel(modelId) {
455
455
  const lower = modelId.toLowerCase();
456
456
  return lower === "deepseek-v4-flash" || lower === "deepseek-v4-pro" || lower.startsWith("deepseek-v4-flash-") || lower.startsWith("deepseek-v4-pro-") || lower === "deepseek-reasoner" || lower === "deepseek-chat";
457
457
  }
458
+ function isKimiReasoningModel(modelId) {
459
+ const lower = modelId.toLowerCase();
460
+ return lower.startsWith("kimi-");
461
+ }
458
462
  function hasSupportedParameter(metadata, param) {
459
- return (metadata?.supportedParameters ?? []).some((p19) => p19 === param);
463
+ return (metadata?.supportedParameters ?? []).some((p21) => p21 === param);
460
464
  }
461
465
  function isOpenRouterRoute(npm, metadata) {
462
466
  return npm === "@openrouter/ai-sdk-provider" || metadata?.providerId === "openrouter" || metadata?.apiBaseUrl?.includes("openrouter.ai") === true;
@@ -678,6 +682,17 @@ function getReasoningCapabilities(npm, modelId, metadata) {
678
682
  wireFormat: { kind: "deepseek-thinking" }
679
683
  };
680
684
  }
685
+ if (isKimiReasoningModel(modelId)) {
686
+ return {
687
+ levels: [...OPENAI_EFFORT_LEVELS],
688
+ defaultLevel: "high",
689
+ supportsSummaries: false,
690
+ mode: "controllable",
691
+ source: "provider-rule",
692
+ confidence: "documented",
693
+ wireFormat: { kind: "openai-reasoning-effort" }
694
+ };
695
+ }
681
696
  if (hasSupportedParameter(metadata, "reasoning_effort")) {
682
697
  return {
683
698
  levels: ["low", "medium", "high", "xhigh"],
@@ -762,6 +777,10 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
762
777
  if (isDeepSeekReasoningModel(modelId)) {
763
778
  return deepSeekEffortProviderOptions(effort);
764
779
  }
780
+ if (isKimiReasoningModel(modelId)) {
781
+ const reasoningEffort = mapCodexEffortToOpenAI(effort);
782
+ return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
783
+ }
765
784
  if (hasSupportedParameter(metadata, "reasoning_effort")) {
766
785
  const reasoningEffort = mapCodexEffortToOpenAI(effort);
767
786
  return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort }, "openai-compatible": { reasoningEffort } } : void 0;
@@ -1104,7 +1123,7 @@ function printFavoritesOnlyPanel() {
1104
1123
  }
1105
1124
 
1106
1125
  // src/cli.ts
1107
- import * as p18 from "@clack/prompts";
1126
+ import * as p20 from "@clack/prompts";
1108
1127
  import { realpathSync } from "fs";
1109
1128
  import { fileURLToPath } from "url";
1110
1129
 
@@ -2562,9 +2581,9 @@ function migrateLegacyCloudProviders(registry) {
2562
2581
  return changed;
2563
2582
  }
2564
2583
  function migrateOAuthOpenAiProvider(registry) {
2565
- if (registry.providers.some((p19) => p19.id === "openai-oauth")) return false;
2584
+ if (registry.providers.some((p21) => p21.id === "openai-oauth")) return false;
2566
2585
  const idx = registry.providers.findIndex(
2567
- (p19) => p19.id === "openai" && p19.authType === "oauth"
2586
+ (p21) => p21.id === "openai" && p21.authType === "oauth"
2568
2587
  );
2569
2588
  if (idx < 0) return false;
2570
2589
  const existing = registry.providers[idx];
@@ -2577,9 +2596,9 @@ function migrateOAuthOpenAiProvider(registry) {
2577
2596
  return true;
2578
2597
  }
2579
2598
  function migrateOAuthXaiProvider(registry) {
2580
- if (registry.providers.some((p19) => p19.id === "xai-oauth")) return false;
2599
+ if (registry.providers.some((p21) => p21.id === "xai-oauth")) return false;
2581
2600
  const idx = registry.providers.findIndex(
2582
- (p19) => p19.id === "xai" && p19.authType === "oauth"
2601
+ (p21) => p21.id === "xai" && p21.authType === "oauth"
2583
2602
  );
2584
2603
  if (idx < 0) return false;
2585
2604
  const existing = registry.providers[idx];
@@ -2636,33 +2655,33 @@ function writeSecureFile(path, content) {
2636
2655
  }
2637
2656
  function parseProvider(raw) {
2638
2657
  if (!raw || typeof raw !== "object") return null;
2639
- const p19 = raw;
2640
- if (typeof p19.id !== "string" || !isValidProviderId(p19.id)) return null;
2641
- if (typeof p19.templateId !== "string" || !p19.templateId) return null;
2642
- if (typeof p19.name !== "string" || !p19.name) return null;
2643
- if (typeof p19.enabled !== "boolean") return null;
2644
- if (typeof p19.authRef !== "string" || !p19.authRef) return null;
2645
- if (typeof p19.addedAt !== "string" || !p19.addedAt) return null;
2646
- const api = p19.api;
2658
+ const p21 = raw;
2659
+ if (typeof p21.id !== "string" || !isValidProviderId(p21.id)) return null;
2660
+ if (typeof p21.templateId !== "string" || !p21.templateId) return null;
2661
+ if (typeof p21.name !== "string" || !p21.name) return null;
2662
+ if (typeof p21.enabled !== "boolean") return null;
2663
+ if (typeof p21.authRef !== "string" || !p21.authRef) return null;
2664
+ if (typeof p21.addedAt !== "string" || !p21.addedAt) return null;
2665
+ const api = p21.api;
2647
2666
  if (!api || typeof api !== "object") return null;
2648
2667
  const provider = {
2649
- id: p19.id,
2650
- templateId: p19.templateId,
2651
- name: p19.name,
2652
- enabled: p19.enabled,
2653
- authRef: p19.authRef,
2668
+ id: p21.id,
2669
+ templateId: p21.templateId,
2670
+ name: p21.name,
2671
+ enabled: p21.enabled,
2672
+ authRef: p21.authRef,
2654
2673
  api,
2655
- addedAt: p19.addedAt
2674
+ addedAt: p21.addedAt
2656
2675
  };
2657
- if (p19.subscriptionFilter === "free" || p19.subscriptionFilter === "zen" || p19.subscriptionFilter === "go") {
2658
- provider.subscriptionFilter = p19.subscriptionFilter;
2676
+ if (p21.subscriptionFilter === "free" || p21.subscriptionFilter === "zen" || p21.subscriptionFilter === "go") {
2677
+ provider.subscriptionFilter = p21.subscriptionFilter;
2659
2678
  }
2660
- if (p19.authType === "api" || p19.authType === "oauth" || p19.authType === "none") {
2661
- provider.authType = p19.authType;
2679
+ if (p21.authType === "api" || p21.authType === "oauth" || p21.authType === "none") {
2680
+ provider.authType = p21.authType;
2662
2681
  }
2663
- if (typeof p19.refreshedAt === "string") provider.refreshedAt = p19.refreshedAt;
2664
- if (p19.modelsCache && typeof p19.modelsCache === "object") {
2665
- const cache = p19.modelsCache;
2682
+ if (typeof p21.refreshedAt === "string") provider.refreshedAt = p21.refreshedAt;
2683
+ if (p21.modelsCache && typeof p21.modelsCache === "object") {
2684
+ const cache = p21.modelsCache;
2666
2685
  if (typeof cache.fetchedAt === "string" && Array.isArray(cache.models)) {
2667
2686
  provider.modelsCache = {
2668
2687
  fetchedAt: cache.fetchedAt,
@@ -3525,6 +3544,113 @@ function listCredentialSkippedProviders(raw, authEntries, importedIds, alreadyRe
3525
3544
  return skipped;
3526
3545
  }
3527
3546
 
3547
+ // src/trace-log.ts
3548
+ import {
3549
+ chmodSync as chmodSync4,
3550
+ existsSync as existsSync7,
3551
+ mkdirSync as mkdirSync4,
3552
+ readFileSync as readFileSync6,
3553
+ unlinkSync,
3554
+ writeFileSync as writeFileSync3
3555
+ } from "fs";
3556
+ import { join as join8 } from "path";
3557
+ import pc2 from "picocolors";
3558
+ var DIR_MODE2 = 448;
3559
+ var FILE_MODE4 = 384;
3560
+ var CLAUDE_DEBUG_LOG = "claude-debug.log";
3561
+ var PROXY_DEBUG_LOG = "proxy-debug.log";
3562
+ var CODEX_PROXY_DEBUG_LOG = "codex-proxy-debug.log";
3563
+ var GEMINI_PROXY_DEBUG_LOG = "gemini-proxy-debug.log";
3564
+ var PROVIDER_DEBUG_LOG = "provider-debug.log";
3565
+ function ensureLogsDir() {
3566
+ const dir = getLogsPath();
3567
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE2 });
3568
+ try {
3569
+ chmodSync4(dir, DIR_MODE2);
3570
+ } catch {
3571
+ }
3572
+ return dir;
3573
+ }
3574
+ function getClaudeDebugLogPath() {
3575
+ return join8(ensureLogsDir(), CLAUDE_DEBUG_LOG);
3576
+ }
3577
+ function prepareClaudeTraceLog() {
3578
+ const path = getClaudeDebugLogPath();
3579
+ resetTraceLog(path);
3580
+ return path;
3581
+ }
3582
+ function getProxyDebugLogPath() {
3583
+ return join8(ensureLogsDir(), PROXY_DEBUG_LOG);
3584
+ }
3585
+ function getCodexProxyDebugLogPath() {
3586
+ return join8(ensureLogsDir(), CODEX_PROXY_DEBUG_LOG);
3587
+ }
3588
+ function getGeminiProxyDebugLogPath() {
3589
+ return join8(ensureLogsDir(), GEMINI_PROXY_DEBUG_LOG);
3590
+ }
3591
+ function getProviderDebugLogPath() {
3592
+ return join8(ensureLogsDir(), PROVIDER_DEBUG_LOG);
3593
+ }
3594
+ function makeTraceLogger(logPath) {
3595
+ resetTraceLog(logPath);
3596
+ return (message) => writeSecureLogLine(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
3597
+ }
3598
+ function resetTraceLog(path) {
3599
+ ensureLogsDir();
3600
+ if (existsSync7(path)) {
3601
+ try {
3602
+ unlinkSync(path);
3603
+ } catch {
3604
+ }
3605
+ }
3606
+ }
3607
+ var REDACTION_PATTERNS = [
3608
+ // Bearer / Authorization headers
3609
+ (line) => line.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]"),
3610
+ (line) => line.replace(/("authorization"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
3611
+ (line) => line.replace(/(x-api-key"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
3612
+ // Common API key prefixes
3613
+ (line) => line.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"),
3614
+ (line) => line.replace(/\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, "sk-ant-[REDACTED]"),
3615
+ (line) => line.replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, "AIza[REDACTED]"),
3616
+ (line) => line.replace(/\bgsk_[A-Za-z0-9]{20,}\b/g, "gsk_[REDACTED]")
3617
+ ];
3618
+ function redactTraceLine(line) {
3619
+ let out = line;
3620
+ for (const apply of REDACTION_PATTERNS) {
3621
+ out = apply(out);
3622
+ }
3623
+ return out;
3624
+ }
3625
+ function redactTraceLog(content) {
3626
+ return content.split("\n").map(redactTraceLine).join("\n");
3627
+ }
3628
+ function writeSecureLogLine(path, line) {
3629
+ ensureLogsDir();
3630
+ const redacted = redactTraceLine(line);
3631
+ try {
3632
+ writeFileSync3(path, `${redacted}
3633
+ `, { flag: "a", mode: FILE_MODE4 });
3634
+ chmodSync4(path, FILE_MODE4);
3635
+ } catch {
3636
+ }
3637
+ }
3638
+ function printTraceLog(debugLogPath) {
3639
+ if (!existsSync7(debugLogPath)) return;
3640
+ const raw = readFileSync6(debugLogPath, "utf8");
3641
+ const log19 = redactTraceLog(raw);
3642
+ const errorLines = log19.split("\n").filter(
3643
+ (l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status") || l.includes("resolveModel failed") || l.includes("resolveModel fallback")
3644
+ );
3645
+ console.log("\n" + pc2.bold(pc2.cyan("\u2500\u2500 Debug trace \u2500\u2500")));
3646
+ if (errorLines.length > 0) {
3647
+ errorLines.slice(0, 30).forEach((l) => console.log(pc2.dim(l)));
3648
+ } else {
3649
+ console.log(pc2.dim("(no errors found in debug log)"));
3650
+ }
3651
+ console.log(pc2.dim(`Full log: ${debugLogPath}`));
3652
+ }
3653
+
3528
3654
  // src/registry/fetch-template-models.ts
3529
3655
  var TEST_TIMEOUT_MS = 1e4;
3530
3656
  function modelFormatForNpm(npm) {
@@ -3532,7 +3658,9 @@ function modelFormatForNpm(npm) {
3532
3658
  }
3533
3659
  function modelsUrl(baseUrl) {
3534
3660
  const trimmed = baseUrl.replace(/\/$/, "");
3535
- if (trimmed.endsWith("/v1")) return `${trimmed}/models`;
3661
+ if (/\/(v\d+[a-z]*|openai|beta)$/.test(trimmed)) {
3662
+ return `${trimmed}/models`;
3663
+ }
3536
3664
  return `${trimmed}/v1/models`;
3537
3665
  }
3538
3666
  function parseModelList(body, npm) {
@@ -3569,6 +3697,22 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
3569
3697
  hint: "Use relay-ai providers import from OpenCode for advanced setups."
3570
3698
  };
3571
3699
  }
3700
+ if (template.modelSource === "static-seed") {
3701
+ const models = (template.staticModels || []).map((sm) => {
3702
+ const family = sm.id.split(/[-/:]/)[0] ?? sm.id;
3703
+ return {
3704
+ id: sm.id,
3705
+ name: sm.name,
3706
+ upstreamModelId: sm.id,
3707
+ family,
3708
+ brand: deriveBrand(family),
3709
+ contextWindow: resolveContextWindow(sm.id),
3710
+ modelFormat: modelFormatForNpm(template.npm),
3711
+ npm: template.npm
3712
+ };
3713
+ });
3714
+ return { models, baseUrl };
3715
+ }
3572
3716
  const url = modelsUrl(baseUrl);
3573
3717
  const controller = new AbortController();
3574
3718
  const timer = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS);
@@ -3594,8 +3738,16 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
3594
3738
  hint: "Check the base URL \u2014 redirects are blocked for security."
3595
3739
  };
3596
3740
  }
3741
+ let logTrace;
3742
+ if (process.env.RELAY_AI_TRACE === "1") {
3743
+ logTrace = makeTraceLogger(getProviderDebugLogPath());
3744
+ }
3597
3745
  if (!response.ok) {
3598
3746
  const body = await response.text().catch(() => "");
3747
+ if (logTrace) {
3748
+ logTrace(`[fetchTemplateModels] HTTP ${response.status} from ${url}`);
3749
+ logTrace(`[fetchTemplateModels] Body: ${body}`);
3750
+ }
3599
3751
  const detail = body.slice(0, 200).trim();
3600
3752
  if (response.status === 401 || response.status === 403) {
3601
3753
  return {
@@ -3612,7 +3764,18 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride) {
3612
3764
  hint: detail || "Check your API key and try again."
3613
3765
  };
3614
3766
  }
3615
- const json = await response.json();
3767
+ const rawBodyText = await response.text().catch(() => "");
3768
+ if (logTrace) {
3769
+ logTrace(`[fetchTemplateModels] HTTP ${response.status} from ${url}`);
3770
+ logTrace(`[fetchTemplateModels] Body: ${rawBodyText}`);
3771
+ }
3772
+ let json = {};
3773
+ try {
3774
+ if (rawBodyText.trim()) {
3775
+ json = JSON.parse(rawBodyText);
3776
+ }
3777
+ } catch {
3778
+ }
3616
3779
  const models = parseModelList(json, template.npm);
3617
3780
  if (models.length === 0) {
3618
3781
  return {
@@ -3768,8 +3931,23 @@ async function fetchAnthropicModels(baseUrl, apiKey) {
3768
3931
  redirect: "manual",
3769
3932
  signal: controller.signal
3770
3933
  });
3934
+ let logTrace;
3935
+ if (process.env.RELAY_AI_TRACE === "1") {
3936
+ logTrace = makeTraceLogger(getProviderDebugLogPath());
3937
+ }
3938
+ const rawBodyText = await response.text().catch(() => "");
3939
+ if (logTrace) {
3940
+ logTrace(`[fetchAnthropicModels] HTTP ${response.status} from ${modelsUrl2}`);
3941
+ logTrace(`[fetchAnthropicModels] Body: ${rawBodyText}`);
3942
+ }
3771
3943
  if (response.ok) {
3772
- const json = await response.json();
3944
+ let json = {};
3945
+ try {
3946
+ if (rawBodyText.trim()) {
3947
+ json = JSON.parse(rawBodyText);
3948
+ }
3949
+ } catch {
3950
+ }
3773
3951
  const models = [];
3774
3952
  for (const row of json.data ?? []) {
3775
3953
  const id = row.id?.trim();
@@ -3812,10 +3990,10 @@ function uniqueProviderId(displayName, registry) {
3812
3990
  let base = customProviderId(displayName);
3813
3991
  if (!base.startsWith("custom-")) base = `custom-${slugifyProviderId(displayName)}`;
3814
3992
  if (!isValidProviderId(base)) base = "custom-provider";
3815
- if (!registry.providers.some((p19) => p19.id === base)) return base;
3993
+ if (!registry.providers.some((p21) => p21.id === base)) return base;
3816
3994
  for (let i = 2; i < 100; i++) {
3817
3995
  const candidate = `${base}-${i}`;
3818
- if (isValidProviderId(candidate) && !registry.providers.some((p19) => p19.id === candidate)) {
3996
+ if (isValidProviderId(candidate) && !registry.providers.some((p21) => p21.id === candidate)) {
3819
3997
  return candidate;
3820
3998
  }
3821
3999
  }
@@ -3939,6 +4117,58 @@ var PROVIDER_TEMPLATES = [
3939
4117
  modelSource: "api-list",
3940
4118
  supported: true
3941
4119
  },
4120
+ {
4121
+ id: "deepseek",
4122
+ name: "DeepSeek",
4123
+ authType: "api",
4124
+ npm: "@ai-sdk/openai-compatible",
4125
+ defaultBaseUrl: "https://api.deepseek.com/v1",
4126
+ signupUrl: "https://platform.deepseek.com",
4127
+ modelSource: "api-list",
4128
+ supported: true
4129
+ },
4130
+ {
4131
+ id: "zhipu",
4132
+ name: "Zhipu AI (GLM)",
4133
+ authType: "api",
4134
+ npm: "@ai-sdk/openai-compatible",
4135
+ defaultBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
4136
+ signupUrl: "https://open.bigmodel.cn",
4137
+ modelSource: "api-list",
4138
+ supported: true
4139
+ },
4140
+ {
4141
+ id: "moonshot",
4142
+ name: "Moonshot (Kimi)",
4143
+ authType: "api",
4144
+ npm: "@ai-sdk/openai-compatible",
4145
+ defaultBaseUrl: "https://api.moonshot.cn/v1",
4146
+ signupUrl: "https://platform.moonshot.cn",
4147
+ modelSource: "api-list",
4148
+ supported: true
4149
+ },
4150
+ {
4151
+ id: "moonshot-global",
4152
+ name: "Moonshot Global (kimi.ai)",
4153
+ authType: "api",
4154
+ npm: "@ai-sdk/openai-compatible",
4155
+ defaultBaseUrl: "https://api.moonshot.ai/v1",
4156
+ signupUrl: "https://platform.kimi.ai",
4157
+ modelSource: "api-list",
4158
+ supported: true
4159
+ },
4160
+ {
4161
+ id: "kimi-code",
4162
+ name: "Kimi Code (Subscription Required)",
4163
+ authType: "api",
4164
+ npm: "@ai-sdk/openai-compatible",
4165
+ defaultBaseUrl: "https://api.kimi.com/coding/v1",
4166
+ modelSource: "static-seed",
4167
+ staticModels: [
4168
+ { id: "kimi-for-coding", name: "Kimi Code K2.7 (Unified)" }
4169
+ ],
4170
+ supported: true
4171
+ },
3942
4172
  {
3943
4173
  id: "xai",
3944
4174
  name: "xAI",
@@ -4353,7 +4583,7 @@ async function importFromOpencode(options = {}) {
4353
4583
  }
4354
4584
  continue;
4355
4585
  }
4356
- const existingIdx = registry.providers.findIndex((p19) => p19.id === entry.id);
4586
+ const existingIdx = registry.providers.findIndex((p21) => p21.id === entry.id);
4357
4587
  const existing = existingIdx >= 0 ? registry.providers[existingIdx] : void 0;
4358
4588
  if (existing && options.resolveConflict) {
4359
4589
  const choice = await options.resolveConflict({
@@ -4391,7 +4621,7 @@ async function importFromOpencode(options = {}) {
4391
4621
  if (isOAuth) oauthImported += 1;
4392
4622
  }
4393
4623
  const alreadyReportedIds = new Set(skipped.map((s) => s.id));
4394
- const registryProviderIds = new Set(registry.providers.map((p19) => p19.id));
4624
+ const registryProviderIds = new Set(registry.providers.map((p21) => p21.id));
4395
4625
  for (const provider of listCredentialSkippedProviders(
4396
4626
  raw,
4397
4627
  authEntries,
@@ -4418,129 +4648,28 @@ import * as p2 from "@clack/prompts";
4418
4648
  import { appendFileSync as appendFileSync2, readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
4419
4649
  import { homedir as homedir6 } from "os";
4420
4650
  import { spawnSync } from "child_process";
4421
-
4422
- // src/trace-log.ts
4423
- import {
4424
- chmodSync as chmodSync4,
4425
- existsSync as existsSync7,
4426
- mkdirSync as mkdirSync4,
4427
- readFileSync as readFileSync6,
4428
- unlinkSync,
4429
- writeFileSync as writeFileSync3
4430
- } from "fs";
4431
- import { join as join8 } from "path";
4432
- import pc2 from "picocolors";
4433
- var DIR_MODE2 = 448;
4434
- var FILE_MODE4 = 384;
4435
- var CLAUDE_DEBUG_LOG = "claude-debug.log";
4436
- var PROXY_DEBUG_LOG = "proxy-debug.log";
4437
- var CODEX_PROXY_DEBUG_LOG = "codex-proxy-debug.log";
4438
- function ensureLogsDir() {
4439
- const dir = getLogsPath();
4440
- mkdirSync4(dir, { recursive: true, mode: DIR_MODE2 });
4441
- try {
4442
- chmodSync4(dir, DIR_MODE2);
4443
- } catch {
4651
+ function detectShellProfile() {
4652
+ const shell = process.env["SHELL"] ?? "";
4653
+ if (process.platform === "darwin") {
4654
+ if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
4655
+ if (shell.includes("bash")) return { display: "~/.bash_profile", path: `${homedir6()}/.bash_profile` };
4656
+ return { display: "~/.profile", path: `${homedir6()}/.profile` };
4444
4657
  }
4445
- return dir;
4446
- }
4447
- function getClaudeDebugLogPath() {
4448
- return join8(ensureLogsDir(), CLAUDE_DEBUG_LOG);
4449
- }
4450
- function prepareClaudeTraceLog() {
4451
- const path = getClaudeDebugLogPath();
4452
- resetTraceLog(path);
4453
- return path;
4454
- }
4455
- function getProxyDebugLogPath() {
4456
- return join8(ensureLogsDir(), PROXY_DEBUG_LOG);
4457
- }
4458
- function getCodexProxyDebugLogPath() {
4459
- return join8(ensureLogsDir(), CODEX_PROXY_DEBUG_LOG);
4460
- }
4461
- function makeTraceLogger(logPath) {
4462
- resetTraceLog(logPath);
4463
- return (message) => writeSecureLogLine(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${message}`);
4464
- }
4465
- function resetTraceLog(path) {
4466
- ensureLogsDir();
4467
- if (existsSync7(path)) {
4468
- try {
4469
- unlinkSync(path);
4470
- } catch {
4471
- }
4658
+ if (process.platform === "linux") {
4659
+ if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
4660
+ if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
4661
+ return { display: "~/.profile", path: `${homedir6()}/.profile` };
4472
4662
  }
4663
+ if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
4664
+ return { display: "~/.profile", path: `${homedir6()}/.profile` };
4473
4665
  }
4474
- var REDACTION_PATTERNS = [
4475
- // Bearer / Authorization headers
4476
- (line) => line.replace(/Bearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [REDACTED]"),
4477
- (line) => line.replace(/("authorization"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
4478
- (line) => line.replace(/(x-api-key"\s*:\s*")[^"]+/gi, "$1[REDACTED]"),
4479
- // Common API key prefixes
4480
- (line) => line.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"),
4481
- (line) => line.replace(/\bsk-ant-[A-Za-z0-9_-]{8,}\b/g, "sk-ant-[REDACTED]"),
4482
- (line) => line.replace(/\bAIza[A-Za-z0-9_-]{20,}\b/g, "AIza[REDACTED]"),
4483
- (line) => line.replace(/\bgsk_[A-Za-z0-9]{20,}\b/g, "gsk_[REDACTED]")
4484
- ];
4485
- function redactTraceLine(line) {
4486
- let out = line;
4487
- for (const apply of REDACTION_PATTERNS) {
4488
- out = apply(out);
4489
- }
4490
- return out;
4491
- }
4492
- function redactTraceLog(content) {
4493
- return content.split("\n").map(redactTraceLine).join("\n");
4494
- }
4495
- function writeSecureLogLine(path, line) {
4496
- ensureLogsDir();
4497
- const redacted = redactTraceLine(line);
4498
- try {
4499
- writeFileSync3(path, `${redacted}
4500
- `, { flag: "a", mode: FILE_MODE4 });
4501
- chmodSync4(path, FILE_MODE4);
4502
- } catch {
4503
- }
4504
- }
4505
- function printTraceLog(debugLogPath) {
4506
- if (!existsSync7(debugLogPath)) return;
4507
- const raw = readFileSync6(debugLogPath, "utf8");
4508
- const log17 = redactTraceLog(raw);
4509
- const errorLines = log17.split("\n").filter(
4510
- (l) => l.includes("error") || l.includes("Error") || l.includes('"type":"error"') || l.includes("status")
4511
- );
4512
- console.log("\n" + pc2.bold(pc2.cyan("\u2500\u2500 Debug trace \u2500\u2500")));
4513
- if (errorLines.length > 0) {
4514
- errorLines.slice(0, 30).forEach((l) => console.log(pc2.dim(l)));
4515
- } else {
4516
- console.log(pc2.dim("(no errors found in debug log)"));
4517
- }
4518
- console.log(pc2.dim(`Full log: ${debugLogPath}`));
4519
- }
4520
-
4521
- // src/key-setup.ts
4522
- function detectShellProfile() {
4523
- const shell = process.env["SHELL"] ?? "";
4524
- if (process.platform === "darwin") {
4525
- if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
4526
- if (shell.includes("bash")) return { display: "~/.bash_profile", path: `${homedir6()}/.bash_profile` };
4527
- return { display: "~/.profile", path: `${homedir6()}/.profile` };
4528
- }
4529
- if (process.platform === "linux") {
4530
- if (shell.includes("zsh")) return { display: "~/.zshrc", path: `${homedir6()}/.zshrc` };
4531
- if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
4532
- return { display: "~/.profile", path: `${homedir6()}/.profile` };
4533
- }
4534
- if (shell.includes("bash")) return { display: "~/.bashrc", path: `${homedir6()}/.bashrc` };
4535
- return { display: "~/.profile", path: `${homedir6()}/.profile` };
4536
- }
4537
- async function resolveOrCollectApiKey(simulate = false, trace = false) {
4538
- if (!simulate) {
4539
- const existing = resolveApiKey();
4540
- if (existing) return existing;
4666
+ async function resolveOrCollectApiKey(simulate = false, trace = false) {
4667
+ if (!simulate) {
4668
+ const existing = resolveApiKey();
4669
+ if (existing) return existing;
4541
4670
  }
4542
4671
  const isMac = process.platform === "darwin";
4543
- const isWindows4 = process.platform === "win32";
4672
+ const isWindows5 = process.platform === "win32";
4544
4673
  const isLinux = process.platform === "linux";
4545
4674
  if (simulate) {
4546
4675
  printDryRunPanel();
@@ -4554,7 +4683,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
4554
4683
  };
4555
4684
  const storedKey = await readFromCredentialStore(keyDiag);
4556
4685
  if (storedKey) {
4557
- const storeName = isMac ? "macOS Keychain" : isWindows4 ? "Windows Credential Manager" : "Secret Service";
4686
+ const storeName = isMac ? "macOS Keychain" : isWindows5 ? "Windows Credential Manager" : "Secret Service";
4558
4687
  p2.log.success(`Found key in ${storeName}`);
4559
4688
  process.env["OPENCODE_API_KEY"] = storedKey;
4560
4689
  return storedKey;
@@ -4584,7 +4713,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
4584
4713
  { value: "session", label: "This session only", hint: "Not saved anywhere \u2014 you'll be asked again next time" }
4585
4714
  ];
4586
4715
  }
4587
- if (isWindows4) {
4716
+ if (isWindows5) {
4588
4717
  return [
4589
4718
  { value: "credential-manager", label: "Windows Credential Manager", hint: "Key stored securely; relay-ai reads it automatically next time" },
4590
4719
  { value: "setx", label: "Persistent environment variable (plaintext)", hint: "Runs setx \u2014 key visible in System Properties \u2192 Environment Variables" },
@@ -4606,7 +4735,7 @@ async function resolveOrCollectApiKey(simulate = false, trace = false) {
4606
4735
  const saveChoice = await p2.select({
4607
4736
  message: "Where should we save the key?",
4608
4737
  options: saveOptions,
4609
- initialValue: isMac ? "keychain" : isWindows4 ? "credential-manager" : secretServiceAvailable ? "secret-service" : "profile"
4738
+ initialValue: isMac ? "keychain" : isWindows5 ? "credential-manager" : secretServiceAvailable ? "secret-service" : "profile"
4610
4739
  });
4611
4740
  if (p2.isCancel(saveChoice)) {
4612
4741
  p2.cancel("Cancelled.");
@@ -4753,10 +4882,10 @@ async function runFirstRunWizard(trace = false) {
4753
4882
  if (p3.isCancel(retry) || retry === "cancel") return "cancel";
4754
4883
  return runFirstRunWizard(trace);
4755
4884
  }
4756
- const spinner9 = p3.spinner();
4757
- spinner9.start("Importing from OpenCode...");
4885
+ const spinner10 = p3.spinner();
4886
+ spinner10.start("Importing from OpenCode...");
4758
4887
  const result = await importFromOpencode();
4759
- spinner9.stop("");
4888
+ spinner10.stop("");
4760
4889
  if (result.error) {
4761
4890
  p3.log.error(result.error);
4762
4891
  return runFirstRunWizard(trace);
@@ -5104,9 +5233,9 @@ function serializeToolResultContent(content) {
5104
5233
 
5105
5234
  // src/tool-search.ts
5106
5235
  var TOOL_SEARCH_TYPE_PREFIX = "tool_search_tool";
5107
- function isToolSearchTool(tool4) {
5108
- if (typeof tool4.type === "string" && tool4.type.startsWith(TOOL_SEARCH_TYPE_PREFIX)) return true;
5109
- const name = tool4.name ?? "";
5236
+ function isToolSearchTool(tool5) {
5237
+ if (typeof tool5.type === "string" && tool5.type.startsWith(TOOL_SEARCH_TYPE_PREFIX)) return true;
5238
+ const name = tool5.name ?? "";
5110
5239
  return name.includes("tool_search") || name === "ToolSearch";
5111
5240
  }
5112
5241
  function extractReferencedToolNames(messages) {
@@ -5145,16 +5274,16 @@ function resolveUpstreamTools(tools, messages) {
5145
5274
  if (!tools?.length) return [];
5146
5275
  const referenced = extractReferencedToolNames(messages);
5147
5276
  const upstream = [];
5148
- for (const tool4 of tools) {
5149
- if (isToolSearchTool(tool4)) {
5150
- upstream.push(tool4);
5277
+ for (const tool5 of tools) {
5278
+ if (isToolSearchTool(tool5)) {
5279
+ upstream.push(tool5);
5151
5280
  continue;
5152
5281
  }
5153
- if (tool4.defer_loading === true) {
5154
- if (referenced.has(tool4.name)) upstream.push(tool4);
5282
+ if (tool5.defer_loading === true) {
5283
+ if (referenced.has(tool5.name)) upstream.push(tool5);
5155
5284
  continue;
5156
5285
  }
5157
- upstream.push(tool4);
5286
+ upstream.push(tool5);
5158
5287
  }
5159
5288
  return upstream;
5160
5289
  }
@@ -5208,12 +5337,15 @@ function annotateToolNames(messages) {
5208
5337
  }
5209
5338
  }
5210
5339
  function thinkingToSdkPart(block, npm) {
5211
- if (npm !== "@ai-sdk/google" && npm !== "@ai-sdk/openai") return null;
5212
5340
  const text5 = block.thinking ?? "";
5213
5341
  if (npm === "@ai-sdk/openai" && !block.signature && !text5.trim()) return null;
5214
5342
  const part = { type: "reasoning", text: text5 };
5215
5343
  if (block.signature) {
5216
- part.providerOptions = npm === "@ai-sdk/google" ? { google: { thoughtSignature: block.signature } } : { openai: { reasoningEncryptedContent: block.signature } };
5344
+ if (npm === "@ai-sdk/google") {
5345
+ part.providerOptions = { google: { thoughtSignature: block.signature } };
5346
+ } else if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/openai-compatible") {
5347
+ part.providerOptions = { openai: { reasoningEncryptedContent: block.signature } };
5348
+ }
5217
5349
  }
5218
5350
  return part;
5219
5351
  }
@@ -5228,8 +5360,8 @@ function translateMessages(messages, npm) {
5228
5360
  for (const b of blocks) {
5229
5361
  if (b.type === "text") parts.push({ type: "text", text: b.text ?? "" });
5230
5362
  else if (b.type === "image") {
5231
- const p19 = imagePart(b);
5232
- if (p19) parts.push(p19);
5363
+ const p21 = imagePart(b);
5364
+ if (p21) parts.push(p21);
5233
5365
  }
5234
5366
  }
5235
5367
  if (toolResults.length) {
@@ -5315,7 +5447,7 @@ function translateRequest(body, npm, options) {
5315
5447
  providerOptions
5316
5448
  };
5317
5449
  }
5318
- async function writeAnthropicStream(fullStream, modelId, write, log17) {
5450
+ async function writeAnthropicStream(fullStream, modelId, write, log19) {
5319
5451
  const messageId = "msg_" + Date.now();
5320
5452
  let blockIndex = -1;
5321
5453
  let started = false;
@@ -5447,7 +5579,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log17) {
5447
5579
  case "error": {
5448
5580
  const e = part.error;
5449
5581
  const errMsg = e?.message || (typeof part.error === "string" ? part.error : JSON.stringify(e?.data ?? part.error));
5450
- log17?.(() => `sdk stream error: ${errMsg}`);
5582
+ log19?.(() => `sdk stream error: ${errMsg}`);
5451
5583
  closeOpen();
5452
5584
  emit("error", { type: "error", error: { type: "api_error", message: errMsg } });
5453
5585
  return;
@@ -5461,7 +5593,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log17) {
5461
5593
  emit("message_delta", { type: "message_delta", delta: { stop_reason: finishReason, stop_sequence: null }, usage });
5462
5594
  emit("message_stop", { type: "message_stop" });
5463
5595
  }
5464
- async function streamAnthropicResponse(model, params, modelId, write, log17) {
5596
+ async function streamAnthropicResponse(model, params, modelId, write, log19) {
5465
5597
  const result = streamText({ model, ...params });
5466
5598
  Promise.resolve(result.text).catch(() => {
5467
5599
  });
@@ -5473,7 +5605,7 @@ async function streamAnthropicResponse(model, params, modelId, write, log17) {
5473
5605
  });
5474
5606
  Promise.resolve(result.usage).catch(() => {
5475
5607
  });
5476
- await writeAnthropicStream(result.fullStream, modelId, write, log17);
5608
+ await writeAnthropicStream(result.fullStream, modelId, write, log19);
5477
5609
  }
5478
5610
  async function generateAnthropicResponse(model, params, modelId) {
5479
5611
  const r = await generateText({ model, ...params });
@@ -5862,6 +5994,8 @@ function loadPreferences() {
5862
5994
  lastProvider,
5863
5995
  lastCodexProvider: config.lastCodexProvider,
5864
5996
  lastCodexModel: config.lastCodexModel,
5997
+ lastGeminiProvider: config.lastGeminiProvider,
5998
+ lastGeminiModel: config.lastGeminiModel,
5865
5999
  recentModelsByProvider: config.recentModelsByProvider,
5866
6000
  favoriteModels: config.favoriteModels,
5867
6001
  server: config.server
@@ -5874,6 +6008,8 @@ function savePreferences(prefs) {
5874
6008
  if (prefs.lastProvider !== void 0) config.lastProvider = prefs.lastProvider;
5875
6009
  if (prefs.lastCodexProvider !== void 0) config.lastCodexProvider = prefs.lastCodexProvider;
5876
6010
  if (prefs.lastCodexModel !== void 0) config.lastCodexModel = prefs.lastCodexModel;
6011
+ if (prefs.lastGeminiProvider !== void 0) config.lastGeminiProvider = prefs.lastGeminiProvider;
6012
+ if (prefs.lastGeminiModel !== void 0) config.lastGeminiModel = prefs.lastGeminiModel;
5877
6013
  if (prefs.recentModelsByProvider !== void 0) config.recentModelsByProvider = prefs.recentModelsByProvider;
5878
6014
  if (prefs.favoriteModels !== void 0) config.favoriteModels = prefs.favoriteModels;
5879
6015
  writeConfig(config);
@@ -5883,7 +6019,7 @@ function recordLaunchSelection(agent, providerId, modelId, prefs) {
5883
6019
  const prevRecent = prefs.recentModelsByProvider?.[providerId] ?? [];
5884
6020
  const updatedRecent = [modelId, ...prevRecent.filter((id) => id !== modelId)].slice(0, MAX_RECENT_MODELS);
5885
6021
  savePreferences({
5886
- ...agent === "claude" ? { lastProvider: providerId, lastModel: modelId } : { lastCodexProvider: providerId, lastCodexModel: modelId },
6022
+ ...agent === "claude" ? { lastProvider: providerId, lastModel: modelId } : agent === "codex" ? { lastCodexProvider: providerId, lastCodexModel: modelId } : { lastGeminiProvider: providerId, lastGeminiModel: modelId },
5887
6023
  recentModelsByProvider: { ...prefs.recentModelsByProvider, [providerId]: updatedRecent }
5888
6024
  });
5889
6025
  }
@@ -6106,14 +6242,14 @@ function zenGoAsLocalProvider(backendId, models) {
6106
6242
  };
6107
6243
  }
6108
6244
  function providersForPicker(catalog) {
6109
- const registryIds = new Set(catalog.localProviders.map((p19) => p19.id));
6245
+ const registryIds = new Set(catalog.localProviders.map((p21) => p21.id));
6110
6246
  const providers = [
6111
6247
  ...catalog.zenModels.length > 0 && !registryIds.has("zen") ? [zenGoAsLocalProvider("zen", catalog.zenModels)] : [],
6112
6248
  ...catalog.goModels.length > 0 && !registryIds.has("go") ? [zenGoAsLocalProvider("go", catalog.goModels)] : [],
6113
6249
  ...catalog.localProviders
6114
6250
  ];
6115
- for (const p19 of providers) {
6116
- p19.models.sort((a, b) => {
6251
+ for (const p21 of providers) {
6252
+ p21.models.sort((a, b) => {
6117
6253
  const nameA = a.name || a.id;
6118
6254
  const nameB = b.name || b.id;
6119
6255
  return nameA.localeCompare(nameB, void 0, { sensitivity: "base", numeric: true });
@@ -6124,7 +6260,7 @@ function providersForPicker(catalog) {
6124
6260
  async function resolveLocalProviderApiKey(provider) {
6125
6261
  const direct = provider.apiKey?.trim();
6126
6262
  if (direct) return direct;
6127
- const reg = loadRegistry().providers.find((p19) => p19.id === provider.id);
6263
+ const reg = loadRegistry().providers.find((p21) => p21.id === provider.id);
6128
6264
  const authRef = reg?.authRef ?? (provider.id === "zen" || provider.id === "go" ? "keyring:global:opencode" : null);
6129
6265
  if (!authRef) return null;
6130
6266
  return resolveProviderCredential(provider.id, authRef);
@@ -6454,21 +6590,21 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
6454
6590
 
6455
6591
  `);
6456
6592
  for await (const part of fullStream) {
6457
- const p19 = part;
6458
- switch (p19.type) {
6593
+ const p21 = part;
6594
+ switch (p21.type) {
6459
6595
  case "text-delta":
6460
- send({ role: "assistant", content: p19.textDelta ?? p19.text ?? "" });
6596
+ send({ role: "assistant", content: p21.textDelta ?? p21.text ?? "" });
6461
6597
  break;
6462
6598
  case "tool-input-start":
6463
6599
  case "tool-call-streaming-start":
6464
- send({ role: "assistant", tool_calls: [{ index: 0, id: p19.id ?? p19.toolCallId, type: "function", function: { name: p19.toolName, arguments: "" } }] });
6600
+ send({ role: "assistant", tool_calls: [{ index: 0, id: p21.id ?? p21.toolCallId, type: "function", function: { name: p21.toolName, arguments: "" } }] });
6465
6601
  break;
6466
6602
  case "tool-input-delta":
6467
6603
  case "tool-call-delta":
6468
- send({ tool_calls: [{ index: 0, function: { arguments: p19.delta ?? p19.text ?? p19.argsTextDelta ?? "" } }] });
6604
+ send({ tool_calls: [{ index: 0, function: { arguments: p21.delta ?? p21.text ?? p21.argsTextDelta ?? "" } }] });
6469
6605
  break;
6470
6606
  case "finish":
6471
- send({}, p19.finishReason || "stop");
6607
+ send({}, p21.finishReason || "stop");
6472
6608
  break;
6473
6609
  }
6474
6610
  }
@@ -7121,10 +7257,10 @@ async function getServerPasswordForMode(mode) {
7121
7257
  }
7122
7258
  async function configureExposedProviders() {
7123
7259
  p6.log.info("Add providers to expose. Listed providers are removed when selected \u2014 like favorites.");
7124
- const spinner9 = p6.spinner();
7125
- spinner9.start("Loading providers...");
7260
+ const spinner10 = p6.spinner();
7261
+ spinner10.start("Loading providers...");
7126
7262
  const catalog = await fetchProviderCatalog({ agent: "server" });
7127
- spinner9.stop("");
7263
+ spinner10.stop("");
7128
7264
  const available = providerOptionsFromCatalog(catalog);
7129
7265
  const picked = await selectServerProviders(available, getServerExposedProviders() ?? void 0);
7130
7266
  if (!picked) return void 0;
@@ -7222,8 +7358,8 @@ async function resolveServerUpstreamApiKey() {
7222
7358
  }));
7223
7359
  if (apiKey) {
7224
7360
  const isMac = process.platform === "darwin";
7225
- const isWindows4 = process.platform === "win32";
7226
- const storeName = isMac ? "macOS Keychain" : isWindows4 ? "Windows Credential Manager" : "Secret Service";
7361
+ const isWindows5 = process.platform === "win32";
7362
+ const storeName = isMac ? "macOS Keychain" : isWindows5 ? "Windows Credential Manager" : "Secret Service";
7227
7363
  p6.log.success(`Found key in ${storeName}`);
7228
7364
  return apiKey;
7229
7365
  }
@@ -7250,8 +7386,8 @@ async function runServerCommand(options = {}) {
7250
7386
  if (pwResult === void 0) return 0;
7251
7387
  const { password: serverPassword, wasSaved: passwordWasSaved } = pwResult;
7252
7388
  const host = mode === "network" ? "0.0.0.0" : "127.0.0.1";
7253
- const spinner9 = p6.spinner();
7254
- spinner9.start("Fetching available models...");
7389
+ const spinner10 = p6.spinner();
7390
+ spinner10.start("Fetching available models...");
7255
7391
  let models;
7256
7392
  try {
7257
7393
  models = await loadServerModels();
@@ -7261,13 +7397,13 @@ async function runServerCommand(options = {}) {
7261
7397
  if (runConfig.favoritesOnly) {
7262
7398
  const favorites = loadPreferences().favoriteModels ?? [];
7263
7399
  if (favorites.length === 0) {
7264
- spinner9.stop(pc6.red("No favorite models configured"));
7400
+ spinner10.stop(pc6.red("No favorite models configured"));
7265
7401
  p6.log.error("Run `relay-ai models` to add favorites, or turn off favorites-only in the server wizard.");
7266
7402
  return 1;
7267
7403
  }
7268
7404
  models = filterServerModelsByFavorites(models, favorites).slice(0, MAX_MODEL_CATALOG);
7269
7405
  if (models.length === 0) {
7270
- spinner9.stop(pc6.red("No favorite models matched the current provider filter"));
7406
+ spinner10.stop(pc6.red("No favorite models matched the current provider filter"));
7271
7407
  p6.log.error("Adjust favorites with `relay-ai models` or change exposed providers in the server wizard.");
7272
7408
  return 1;
7273
7409
  }
@@ -7279,7 +7415,7 @@ async function runServerCommand(options = {}) {
7279
7415
  p6.log.info("Desktop/Cowork picker will only show these. Edit with `relay-ai models`.");
7280
7416
  }
7281
7417
  if (models.length === 0) {
7282
- spinner9.stop(pc6.red("No models to expose"));
7418
+ spinner10.stop(pc6.red("No models to expose"));
7283
7419
  p6.log.error("Add providers with `relay-ai providers add` or configure exposed providers in the server wizard.");
7284
7420
  return 1;
7285
7421
  }
@@ -7288,10 +7424,10 @@ async function runServerCommand(options = {}) {
7288
7424
  const filterNote = runConfig.exposedProviders ? ` \u2014 ${runConfig.exposedProviders.length} provider${runConfig.exposedProviders.length !== 1 ? "s" : ""}` : "";
7289
7425
  const favoritesNote = runConfig.favoritesOnly ? " \u2014 favorites only" : "";
7290
7426
  const maskNote = runConfig.maskGatewayIds ? " \u2014 discovery ids masked" : "";
7291
- spinner9.stop(`Loaded ${models.length} models (${localCount} from registry providers)${filterNote}${favoritesNote}${maskNote}`);
7427
+ spinner10.stop(`Loaded ${models.length} models (${localCount} from registry providers)${filterNote}${favoritesNote}${maskNote}`);
7292
7428
  if (summary) p6.log.info(summary);
7293
7429
  } catch (err) {
7294
- spinner9.stop(pc6.red("Failed to load models"));
7430
+ spinner10.stop(pc6.red("Failed to load models"));
7295
7431
  console.error(pc6.red(String(err instanceof Error ? err.message : err)));
7296
7432
  return 1;
7297
7433
  }
@@ -7800,7 +7936,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7800
7936
  return { added: false, error: "API key cannot be empty." };
7801
7937
  }
7802
7938
  const registry = loadRegistry();
7803
- const existing = registry.providers.find((p19) => p19.id === template.id);
7939
+ const existing = registry.providers.find((p21) => p21.id === template.id);
7804
7940
  if (existing && !opts?.replaceExisting) {
7805
7941
  return {
7806
7942
  added: false,
@@ -7850,7 +7986,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7850
7986
  }
7851
7987
  };
7852
7988
  if (existing) {
7853
- const idx = registry.providers.findIndex((p19) => p19.id === template.id);
7989
+ const idx = registry.providers.findIndex((p21) => p21.id === template.id);
7854
7990
  registry.providers[idx] = entry;
7855
7991
  } else {
7856
7992
  registry.providers.push(entry);
@@ -7862,11 +7998,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7862
7998
 
7863
7999
  // src/registry/crud.ts
7864
8000
  function credentialStillReferenced(authRef, remaining) {
7865
- return remaining.some((p19) => p19.authRef === authRef);
8001
+ return remaining.some((p21) => p21.authRef === authRef);
7866
8002
  }
7867
8003
  async function removeProviderFromRegistry(id, opts) {
7868
8004
  const registry = loadRegistry();
7869
- const index = registry.providers.findIndex((p19) => p19.id === id);
8005
+ const index = registry.providers.findIndex((p21) => p21.id === id);
7870
8006
  if (index < 0) {
7871
8007
  return { removed: false, id, credentialDeleted: false, error: `Provider not found: ${id}` };
7872
8008
  }
@@ -7890,7 +8026,7 @@ async function removeProviderFromRegistry(id, opts) {
7890
8026
  }
7891
8027
  function addZenRegistryStub(opts) {
7892
8028
  const registry = loadRegistry();
7893
- if (registry.providers.some((p19) => p19.id === "zen")) {
8029
+ if (registry.providers.some((p21) => p21.id === "zen")) {
7894
8030
  return { added: false, reason: "OpenCode Zen is already configured." };
7895
8031
  }
7896
8032
  registry.providers.push(zenRegistryStub(opts?.subscriptionFilter));
@@ -7899,7 +8035,7 @@ function addZenRegistryStub(opts) {
7899
8035
  }
7900
8036
  function addGoRegistryStub() {
7901
8037
  const registry = loadRegistry();
7902
- if (registry.providers.some((p19) => p19.id === "go")) {
8038
+ if (registry.providers.some((p21) => p21.id === "go")) {
7903
8039
  return { added: false, reason: "OpenCode Go is already configured." };
7904
8040
  }
7905
8041
  registry.providers.push(goRegistryStub());
@@ -7908,7 +8044,7 @@ function addGoRegistryStub() {
7908
8044
  }
7909
8045
  function toggleProviderEnabled(id) {
7910
8046
  const registry = loadRegistry();
7911
- const provider = registry.providers.find((p19) => p19.id === id);
8047
+ const provider = registry.providers.find((p21) => p21.id === id);
7912
8048
  if (!provider) return { toggled: false, error: `Provider not found: ${id}` };
7913
8049
  provider.enabled = !provider.enabled;
7914
8050
  saveRegistry(registry);
@@ -8142,7 +8278,7 @@ async function refreshApiListProvider(provider, apiKey) {
8142
8278
  };
8143
8279
  }
8144
8280
  function updateProviderCache(registry, providerId, models, baseUrl) {
8145
- const idx = registry.providers.findIndex((p19) => p19.id === providerId);
8281
+ const idx = registry.providers.findIndex((p21) => p21.id === providerId);
8146
8282
  if (idx < 0) return;
8147
8283
  const now = (/* @__PURE__ */ new Date()).toISOString();
8148
8284
  const existing = registry.providers[idx];
@@ -8157,7 +8293,7 @@ function updateProviderCache(registry, providerId, models, baseUrl) {
8157
8293
  };
8158
8294
  }
8159
8295
  async function refreshProviderModels(providerId, apiKey, registry = loadRegistry()) {
8160
- const provider = registry.providers.find((p19) => p19.id === providerId);
8296
+ const provider = registry.providers.find((p21) => p21.id === providerId);
8161
8297
  if (!provider) {
8162
8298
  return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
8163
8299
  }
@@ -8264,7 +8400,7 @@ async function refreshAllProviderModels(resolveKey) {
8264
8400
  const opencodeKey = await readGlobalOpencodeCredential();
8265
8401
  if (opencodeKey) {
8266
8402
  let changed = false;
8267
- if (!registry.providers.some((p19) => p19.id === "zen")) {
8403
+ if (!registry.providers.some((p21) => p21.id === "zen")) {
8268
8404
  registry.providers.push({
8269
8405
  id: "zen",
8270
8406
  templateId: "zen",
@@ -8278,7 +8414,7 @@ async function refreshAllProviderModels(resolveKey) {
8278
8414
  });
8279
8415
  changed = true;
8280
8416
  }
8281
- if (!registry.providers.some((p19) => p19.id === "go")) {
8417
+ if (!registry.providers.some((p21) => p21.id === "go")) {
8282
8418
  registry.providers.push({
8283
8419
  id: "go",
8284
8420
  templateId: "go",
@@ -8296,7 +8432,7 @@ async function refreshAllProviderModels(resolveKey) {
8296
8432
  saveRegistry(registry);
8297
8433
  }
8298
8434
  }
8299
- const enabledProviders = registry.providers.filter((p19) => p19.enabled);
8435
+ const enabledProviders = registry.providers.filter((p21) => p21.enabled);
8300
8436
  for (const provider of enabledProviders) {
8301
8437
  const key = await resolveRefreshCredential(provider, resolveKey);
8302
8438
  refreshed.push(await refreshProviderModels(provider.id, key, registry));
@@ -8350,42 +8486,42 @@ function openBrowser(url) {
8350
8486
  async function runNativeDeviceCode(providerId) {
8351
8487
  const label = PROVIDER_DISPLAY[providerId];
8352
8488
  printOAuthStepsPanel(`${label} \u2014 Sign in`, label);
8353
- const spinner9 = p9.spinner();
8354
- spinner9.start("Waiting for authorization...");
8489
+ const spinner10 = p9.spinner();
8490
+ spinner10.start("Waiting for authorization...");
8355
8491
  try {
8356
8492
  if (providerId === "xai" || providerId === "xai-oauth") {
8357
8493
  const tokens2 = await runXaiDeviceCodeFlow(({ url, userCode }) => {
8358
- spinner9.stop("");
8494
+ spinner10.stop("");
8359
8495
  p9.log.info(`Visit: ${pc9.cyan(url)}`);
8360
8496
  p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
8361
8497
  openBrowser(url);
8362
- spinner9.start("Waiting for authorization...");
8498
+ spinner10.start("Waiting for authorization...");
8363
8499
  });
8364
- spinner9.stop(pc9.green("Signed in to xAI"));
8500
+ spinner10.stop(pc9.green("Signed in to xAI"));
8365
8501
  return tokensToStoredCredential(tokens2);
8366
8502
  }
8367
8503
  if (providerId === "github-copilot") {
8368
8504
  const tokens2 = await runGithubDeviceCodeFlow(({ url, userCode }) => {
8369
- spinner9.stop("");
8505
+ spinner10.stop("");
8370
8506
  p9.log.info(`Visit: ${pc9.cyan(url)}`);
8371
8507
  p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
8372
8508
  openBrowser(url);
8373
- spinner9.start("Waiting for authorization...");
8509
+ spinner10.start("Waiting for authorization...");
8374
8510
  });
8375
- spinner9.stop(pc9.green("Signed in to GitHub Copilot"));
8511
+ spinner10.stop(pc9.green("Signed in to GitHub Copilot"));
8376
8512
  return tokensToStoredCredential(tokens2);
8377
8513
  }
8378
8514
  const { tokens, accountId } = await runOpenAiDeviceCodeFlow(({ url, userCode }) => {
8379
- spinner9.stop("");
8515
+ spinner10.stop("");
8380
8516
  p9.log.info(`Visit: ${pc9.cyan(url)}`);
8381
8517
  p9.log.info(`Enter code: ${pc9.bold(userCode)}`);
8382
8518
  openBrowser(url);
8383
- spinner9.start("Waiting for authorization...");
8519
+ spinner10.start("Waiting for authorization...");
8384
8520
  });
8385
- spinner9.stop(pc9.green("Signed in to OpenAI ChatGPT"));
8521
+ spinner10.stop(pc9.green("Signed in to OpenAI ChatGPT"));
8386
8522
  return tokensToStoredCredential(tokens, void 0, accountId);
8387
8523
  } catch (err) {
8388
- spinner9.stop("");
8524
+ spinner10.stop("");
8389
8525
  throw err;
8390
8526
  }
8391
8527
  }
@@ -8586,10 +8722,10 @@ async function runProvidersImport() {
8586
8722
  if (p10.isCancel(choice)) return "skip";
8587
8723
  return choice;
8588
8724
  } : void 0;
8589
- const spinner9 = p10.spinner();
8590
- spinner9.start("Importing from OpenCode...");
8725
+ const spinner10 = p10.spinner();
8726
+ spinner10.start("Importing from OpenCode...");
8591
8727
  const result = await importFromOpencode({ resolveConflict });
8592
- spinner9.stop("");
8728
+ spinner10.stop("");
8593
8729
  if (result.error) {
8594
8730
  p10.log.error(result.error);
8595
8731
  return 1;
@@ -8654,19 +8790,19 @@ async function runProvidersRefreshModels(providerId) {
8654
8790
  const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
8655
8791
  if (providerId) {
8656
8792
  const registry = loadRegistry();
8657
- const provider = registry.providers.find((p19) => p19.id === providerId);
8793
+ const provider = registry.providers.find((p21) => p21.id === providerId);
8658
8794
  if (!provider) {
8659
8795
  p10.log.error(`Provider not found: ${providerId}`);
8660
8796
  return 1;
8661
8797
  }
8662
- const spinner10 = p10.spinner();
8663
- spinner10.start(`Refreshing ${provider.name}...`);
8798
+ const spinner11 = p10.spinner();
8799
+ spinner11.start(`Refreshing ${provider.name}...`);
8664
8800
  const key = await resolveRefreshCredential(
8665
8801
  provider,
8666
- async (p19) => resolveProviderCredential(p19.id, p19.authRef)
8802
+ async (p21) => resolveProviderCredential(p21.id, p21.authRef)
8667
8803
  );
8668
8804
  const result = await refreshProviderModels(providerId, key);
8669
- spinner10.stop("");
8805
+ spinner11.stop("");
8670
8806
  if (result.skipped) {
8671
8807
  const countNote = result.modelCount ? ` (${result.modelCount} cached models kept)` : "";
8672
8808
  p10.log.warn(`${result.name}: ${result.reason}${countNote}`);
@@ -8681,10 +8817,10 @@ async function runProvidersRefreshModels(providerId) {
8681
8817
  p10.log.success(`${result.name}: ${result.modelCount} model${result.modelCount === 1 ? "" : "s"} updated${diffStr}.`);
8682
8818
  return 0;
8683
8819
  }
8684
- const spinner9 = p10.spinner();
8685
- spinner9.start("Refreshing model lists...");
8820
+ const spinner10 = p10.spinner();
8821
+ spinner10.start("Refreshing model lists...");
8686
8822
  const { refreshed } = await refreshAllProviderModels(resolveKey);
8687
- spinner9.stop("");
8823
+ spinner10.stop("");
8688
8824
  const ok = refreshed.filter((r) => r.ok && !r.skipped);
8689
8825
  const skipped = refreshed.filter((r) => r.skipped);
8690
8826
  const failed = refreshed.filter((r) => !r.ok);
@@ -8724,7 +8860,7 @@ async function runProvidersList() {
8724
8860
  async function pickTemplateFromCatalog() {
8725
8861
  while (true) {
8726
8862
  const registry = loadRegistry();
8727
- const configuredIds = new Set(registry.providers.map((p19) => p19.id));
8863
+ const configuredIds = new Set(registry.providers.map((p21) => p21.id));
8728
8864
  const templates = listAddableTemplates(configuredIds);
8729
8865
  if (templates.length === 0) return null;
8730
8866
  const method = await p10.select({
@@ -8779,7 +8915,7 @@ async function pickTemplateFromCatalog() {
8779
8915
  }
8780
8916
  }
8781
8917
  async function runTemplateAddFlow() {
8782
- if (listAddableTemplates(loadRegistry().providers.map((p19) => p19.id)).length === 0) {
8918
+ if (listAddableTemplates(loadRegistry().providers.map((p21) => p21.id)).length === 0) {
8783
8919
  p10.log.info("All catalog providers are already configured.");
8784
8920
  return 0;
8785
8921
  }
@@ -8801,12 +8937,12 @@ async function runTemplateAddFlow() {
8801
8937
  apiKey2 = collected;
8802
8938
  }
8803
8939
  await migrateGlobalOpencodeCredential();
8804
- const spinner10 = p10.spinner();
8805
- spinner10.start(`Adding ${template.name}...`);
8940
+ const spinner11 = p10.spinner();
8941
+ spinner11.start(`Adding ${template.name}...`);
8806
8942
  const zenStub = addZenRegistryStub();
8807
8943
  const goStub = addGoRegistryStub();
8808
8944
  if (!zenStub.added && !goStub.added) {
8809
- spinner10.stop("");
8945
+ spinner11.stop("");
8810
8946
  p10.log.warn("OpenCode Zen / Go is already configured.");
8811
8947
  return 0;
8812
8948
  }
@@ -8815,7 +8951,7 @@ async function runTemplateAddFlow() {
8815
8951
  await refreshProviderModels("zen", apiKey2, registry),
8816
8952
  await refreshProviderModels("go", apiKey2, registry)
8817
8953
  ];
8818
- spinner10.stop("");
8954
+ spinner11.stop("");
8819
8955
  const modelCount = refreshResults.reduce((total, result2) => total + (result2.modelCount ?? 0), 0);
8820
8956
  const failed = refreshResults.filter((result2) => !result2.ok);
8821
8957
  if (failed.length === 0) {
@@ -8858,10 +8994,10 @@ async function runTemplateAddFlow() {
8858
8994
  }
8859
8995
  const rawKey = String(apiKeyInput).trim();
8860
8996
  const apiKey = template.apiKeyOptional && !rawKey ? template.id : rawKey;
8861
- const spinner9 = p10.spinner();
8862
- spinner9.start(`Testing connection to ${template.name}...`);
8997
+ const spinner10 = p10.spinner();
8998
+ spinner10.start(`Testing connection to ${template.name}...`);
8863
8999
  const result = await addProviderFromTemplate(template, apiKey, { baseUrl: baseUrlOverride });
8864
- spinner9.stop("");
9000
+ spinner10.stop("");
8865
9001
  if (!result.added) {
8866
9002
  p10.log.error(result.error ?? "Could not add provider.");
8867
9003
  if (result.hint) p10.log.info(result.hint);
@@ -8909,8 +9045,8 @@ async function runCustomEndpointAddFlow() {
8909
9045
  message: "API key (leave empty for local servers without auth):"
8910
9046
  });
8911
9047
  if (p10.isCancel(apiKey)) return 0;
8912
- const spinner9 = p10.spinner();
8913
- spinner9.start("Testing connection...");
9048
+ const spinner10 = p10.spinner();
9049
+ spinner10.start("Testing connection...");
8914
9050
  const result = await addCustomEndpointProvider({
8915
9051
  displayName: String(displayName).trim(),
8916
9052
  baseUrl: String(baseUrl).trim(),
@@ -8918,7 +9054,7 @@ async function runCustomEndpointAddFlow() {
8918
9054
  kind: kindChoice,
8919
9055
  allowInsecureLocal: allowLocal === true
8920
9056
  });
8921
- spinner9.stop("");
9057
+ spinner10.stop("");
8922
9058
  if (!result.added) {
8923
9059
  p10.log.error(result.error ?? "Could not add custom provider.");
8924
9060
  if (result.hint) p10.log.info(result.hint);
@@ -8937,7 +9073,7 @@ async function runProvidersAdd() {
8937
9073
  hint: hasOpencode ? "Import Groq, OpenAI, etc. from your OpenCode config" : "Requires OpenCode CLI"
8938
9074
  }
8939
9075
  ];
8940
- const addableTemplates = listAddableTemplates(registry.providers.map((p19) => p19.id));
9076
+ const addableTemplates = listAddableTemplates(registry.providers.map((p21) => p21.id));
8941
9077
  if (addableTemplates.length > 0) {
8942
9078
  options.push({
8943
9079
  value: "templates",
@@ -8974,11 +9110,11 @@ async function runProvidersRemove(id, interactive = false) {
8974
9110
  return 1;
8975
9111
  }
8976
9112
  if (interactive) {
8977
- const confirm9 = await p10.confirm({
9113
+ const confirm10 = await p10.confirm({
8978
9114
  message: `Remove ${provider.name} (${id})?`,
8979
9115
  initialValue: false
8980
9116
  });
8981
- if (p10.isCancel(confirm9) || !confirm9) {
9117
+ if (p10.isCancel(confirm10) || !confirm10) {
8982
9118
  p10.cancel("Cancelled.");
8983
9119
  return 0;
8984
9120
  }
@@ -9174,7 +9310,7 @@ import { createServer as createServer3 } from "http";
9174
9310
  import { streamText as streamText3, generateText as generateText3, tool as tool3, jsonSchema as jsonSchema3 } from "ai";
9175
9311
  function messageText(content) {
9176
9312
  if (typeof content === "string") return content;
9177
- return (content ?? []).map((p19) => p19.type === "output_text" || p19.type === "input_text" || p19.type === "text" ? p19.text ?? "" : "").join("");
9313
+ return (content ?? []).map((p21) => p21.type === "output_text" || p21.type === "input_text" || p21.type === "text" ? p21.text ?? "" : "").join("");
9178
9314
  }
9179
9315
  function extractDeveloperAndInstructions(items, instructions) {
9180
9316
  const developerParts = [];
@@ -9540,24 +9676,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
9540
9676
  });
9541
9677
  outputItems.unshift(reasoningItem);
9542
9678
  }
9543
- for (const tool4 of toolStates) {
9679
+ for (const tool5 of toolStates) {
9544
9680
  emit("response.function_call_arguments.done", {
9545
9681
  type: "response.function_call_arguments.done",
9546
- item_id: tool4.itemId,
9547
- output_index: tool4.outputIndex,
9548
- arguments: tool4.args
9682
+ item_id: tool5.itemId,
9683
+ output_index: tool5.outputIndex,
9684
+ arguments: tool5.args
9549
9685
  });
9550
9686
  const fcItem = {
9551
9687
  type: "function_call",
9552
- id: tool4.itemId,
9553
- call_id: tool4.callId,
9554
- name: tool4.name,
9555
- arguments: tool4.args,
9688
+ id: tool5.itemId,
9689
+ call_id: tool5.callId,
9690
+ name: tool5.name,
9691
+ arguments: tool5.args,
9556
9692
  status: "completed"
9557
9693
  };
9558
9694
  emit("response.output_item.done", {
9559
9695
  type: "response.output_item.done",
9560
- output_index: tool4.outputIndex,
9696
+ output_index: tool5.outputIndex,
9561
9697
  item: fcItem
9562
9698
  });
9563
9699
  outputItems.push(fcItem);
@@ -9766,17 +9902,17 @@ async function startCodexProxy(routes, options = {}) {
9766
9902
  }));
9767
9903
  }
9768
9904
  return new Promise((resolve, reject2) => {
9769
- const log17 = debug ? makeTraceLogger(getCodexProxyDebugLogPath()) : () => {
9905
+ const log19 = debug ? makeTraceLogger(getCodexProxyDebugLogPath()) : () => {
9770
9906
  };
9771
9907
  const onRejection = (reason) => {
9772
9908
  logUpstreamError(reason);
9773
- if (debug) log17(formatUpstreamError(reason));
9909
+ if (debug) log19(formatUpstreamError(reason));
9774
9910
  };
9775
9911
  process.on("unhandledRejection", onRejection);
9776
9912
  const server = createServer3(async (req, res) => {
9777
9913
  const url = req.url ?? "/";
9778
9914
  if (debug) {
9779
- log17(`-> ${req.method} ${url} content-type=${req.headers["content-type"] ?? "(none)"} content-encoding=${req.headers["content-encoding"] ?? "(none)"} content-length=${req.headers["content-length"] ?? "(none)"}`);
9915
+ log19(`-> ${req.method} ${url} content-type=${req.headers["content-type"] ?? "(none)"} content-encoding=${req.headers["content-encoding"] ?? "(none)"} content-length=${req.headers["content-length"] ?? "(none)"}`);
9780
9916
  }
9781
9917
  if (!requireAuth && req.method === "POST") {
9782
9918
  const origin = req.headers.origin;
@@ -9854,7 +9990,7 @@ async function startCodexProxy(routes, options = {}) {
9854
9990
  rawBody = await readBody(req);
9855
9991
  } catch (err) {
9856
9992
  if (debug) {
9857
- log17(`Error: failed to read/decode request body on POST ${url}: ${formatUpstreamError(err)} content-encoding=${req.headers["content-encoding"] ?? "(none)"}`);
9993
+ log19(`Error: failed to read/decode request body on POST ${url}: ${formatUpstreamError(err)} content-encoding=${req.headers["content-encoding"] ?? "(none)"}`);
9858
9994
  }
9859
9995
  sendJson(res, 400, { error: { message: "Invalid request body", type: "invalid_request_error" } });
9860
9996
  return;
@@ -9865,19 +10001,28 @@ async function startCodexProxy(routes, options = {}) {
9865
10001
  } catch (err) {
9866
10002
  if (debug) {
9867
10003
  const headers = JSON.stringify(req.headers);
9868
- log17(`Error: Invalid JSON body on POST ${url}: ${formatUpstreamError(err)} headers=${headers} rawBody=${JSON.stringify(rawBody.slice(0, 2e3))}`);
10004
+ log19(`Error: Invalid JSON body on POST ${url}: ${formatUpstreamError(err)} headers=${headers} rawBody=${JSON.stringify(rawBody.slice(0, 2e3))}`);
9869
10005
  }
9870
10006
  sendJson(res, 400, { error: { message: "Invalid JSON body", type: "invalid_request_error" } });
9871
10007
  return;
9872
10008
  }
9873
10009
  const modelId = String(body.model ?? "");
9874
- const resolved = resolveModel(routes, models, modelId);
10010
+ let resolved = resolveModel(routes, models, modelId);
9875
10011
  if (!resolved) {
9876
- if (debug) {
9877
- log17(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
10012
+ const fallbackRoute = routes[0];
10013
+ const fallbackLm = fallbackRoute ? models.get(fallbackRoute.modelId) : void 0;
10014
+ if (fallbackRoute && fallbackLm) {
10015
+ if (debug) {
10016
+ log19(`resolveModel fallback: requested="${modelId}" \u2192 ${fallbackRoute.modelId}`);
10017
+ }
10018
+ resolved = { route: fallbackRoute, languageModel: fallbackLm };
10019
+ } else {
10020
+ if (debug) {
10021
+ log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
10022
+ }
10023
+ sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
10024
+ return;
9878
10025
  }
9879
- sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
9880
- return;
9881
10026
  }
9882
10027
  const { route, languageModel } = resolved;
9883
10028
  try {
@@ -9894,7 +10039,7 @@ async function startCodexProxy(routes, options = {}) {
9894
10039
  );
9895
10040
  if (debug) {
9896
10041
  const effort = body.reasoning?.effort;
9897
- log17(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
10042
+ log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
9898
10043
  }
9899
10044
  if (body.stream) {
9900
10045
  res.writeHead(200, {
@@ -9924,11 +10069,15 @@ async function startCodexProxy(routes, options = {}) {
9924
10069
  }
9925
10070
  } catch (err) {
9926
10071
  const msg = formatUpstreamError(err);
9927
- log17(`handler error: ${msg}`);
10072
+ log19(`handler error: ${msg}`);
9928
10073
  sendJson(res, 500, { error: { message: msg, type: "api_error" } });
9929
10074
  }
9930
10075
  return;
9931
10076
  }
10077
+ if (req.method === "GET" && url === "/v1/responses") {
10078
+ sendJson(res, 200, { object: "list", data: [] });
10079
+ return;
10080
+ }
9932
10081
  sendJson(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
9933
10082
  });
9934
10083
  server.on("error", reject2);
@@ -10150,7 +10299,7 @@ function restoreCodexOverlay(env = process.env) {
10150
10299
  return removed;
10151
10300
  }
10152
10301
  function remainingOverlayPaths(env = process.env) {
10153
- return ownedOverlayPaths(env).filter((p19) => existsSync11(p19));
10302
+ return ownedOverlayPaths(env).filter((p21) => existsSync11(p21));
10154
10303
  }
10155
10304
  function recoverInterruptedCodexSession(env = process.env) {
10156
10305
  const before = remainingOverlayPaths(env);
@@ -10547,8 +10696,8 @@ function resolveCodexFavorites(activeProvider, selectedModel, compatible, favori
10547
10696
  agent,
10548
10697
  localProviders: compatible,
10549
10698
  zenGoApiKey,
10550
- zenModels: compatible.find((p19) => p19.id === "zen")?.models,
10551
- goModels: compatible.find((p19) => p19.id === "go")?.models,
10699
+ zenModels: compatible.find((p21) => p21.id === "zen")?.models,
10700
+ goModels: compatible.find((p21) => p21.id === "go")?.models,
10552
10701
  findLocalModel: (pid, mid) => {
10553
10702
  const provider = compatible.find((lp) => lp.id === pid);
10554
10703
  const model = provider?.models.find((m) => m.id === mid);
@@ -10620,8 +10769,24 @@ function isClaudeMachineReadableOutput(args) {
10620
10769
  function isCodexMachineReadableOutput(args) {
10621
10770
  return args.includes("--json");
10622
10771
  }
10772
+ function isGeminiNonInteractive(args) {
10773
+ for (let i = 0; i < args.length; i++) {
10774
+ const arg = args[i];
10775
+ if (arg === "--") return false;
10776
+ if (arg === "-p" || arg === "--prompt" || arg === "-i" || arg === "--prompt-interactive") return true;
10777
+ if (arg.startsWith("-")) {
10778
+ i = skipAttachedFlagValue(args, i);
10779
+ continue;
10780
+ }
10781
+ return true;
10782
+ }
10783
+ return false;
10784
+ }
10623
10785
  function wantsCleanAgentStdout(agent, childArgs) {
10624
- return agent === "claude" ? isClaudeMachineReadableOutput(childArgs) : isCodexMachineReadableOutput(childArgs);
10786
+ if (agent === "claude") return isClaudeMachineReadableOutput(childArgs);
10787
+ if (agent === "codex") return isCodexMachineReadableOutput(childArgs);
10788
+ const outFmt = readFlagValue(childArgs, "-o") || readFlagValue(childArgs, "--output-format");
10789
+ return outFmt === "json" || outFmt === "stream-json";
10625
10790
  }
10626
10791
  function normalizeClaudeAgentArgs(args) {
10627
10792
  const out = [...args];
@@ -10653,14 +10818,14 @@ function isCodexNonInteractive(args) {
10653
10818
  }
10654
10819
  function resolveLaunchTarget(explicit, prefs, agent) {
10655
10820
  const slug = explicit.modelId ? parseModelSlug(explicit.modelId) : null;
10656
- const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : prefs.lastCodexProvider);
10657
- const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : prefs.lastCodexModel);
10821
+ const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : agent === "codex" ? prefs.lastCodexProvider : prefs.lastGeminiProvider);
10822
+ const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : agent === "codex" ? prefs.lastCodexModel : prefs.lastGeminiModel);
10658
10823
  if (!providerId || !modelId) return null;
10659
10824
  return { providerId, modelId };
10660
10825
  }
10661
10826
  function findProviderAndModel(providers, target) {
10662
10827
  if (!target.providerId || !target.modelId) return null;
10663
- const provider = providers.find((p19) => p19.id === target.providerId);
10828
+ const provider = providers.find((p21) => p21.id === target.providerId);
10664
10829
  if (!provider) return null;
10665
10830
  const model = provider.models.find((m) => m.id === target.modelId);
10666
10831
  if (!model) return null;
@@ -10677,7 +10842,7 @@ function hasCompleteExplicitLaunch(explicit) {
10677
10842
  function planLaunchWizard(opts) {
10678
10843
  const { explicit, childArgs, agent, prefs } = opts;
10679
10844
  const explicitComplete = hasCompleteExplicitLaunch(explicit);
10680
- const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : isCodexNonInteractive(childArgs);
10845
+ const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : agent === "codex" ? isCodexNonInteractive(childArgs) : isGeminiNonInteractive(childArgs);
10681
10846
  if (explicitComplete) {
10682
10847
  const target = resolveLaunchTarget(explicit, prefs, agent);
10683
10848
  if (!target) {
@@ -10710,7 +10875,9 @@ function planLaunchWizard(opts) {
10710
10875
  return { skip: false, target: null };
10711
10876
  }
10712
10877
  function nonInteractiveLaunchError(agent) {
10713
- return agent === "claude" ? "Print mode requires --provider and --model, or saved preferences from a prior launch." : "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
10878
+ if (agent === "claude") return "Print mode requires --provider and --model, or saved preferences from a prior launch.";
10879
+ if (agent === "codex") return "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
10880
+ return "Non-interactive Gemini launch requires --provider and --model, or saved preferences from a prior launch.";
10714
10881
  }
10715
10882
 
10716
10883
  // src/codex.ts
@@ -11121,7 +11288,7 @@ Error: ${launchPlan.error}
11121
11288
  resolvedFavorites = res.resolvedFavorites;
11122
11289
  providersById = res.providersById;
11123
11290
  }
11124
- const regEntry = loadRegistry().providers.find((p19) => p19.id === activeProvider.id);
11291
+ const regEntry = loadRegistry().providers.find((p21) => p21.id === activeProvider.id);
11125
11292
  const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
11126
11293
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
11127
11294
  if (!apiKey) {
@@ -11175,7 +11342,7 @@ Error: ${launchPlan.error}
11175
11342
  });
11176
11343
  if (configOnly) {
11177
11344
  const home = process.env["HOME"] ?? "";
11178
- const shortenPath = (p19) => home ? p19.replace(home, "~") : p19;
11345
+ const shortenPath = (p21) => home ? p21.replace(home, "~") : p21;
11179
11346
  console.log("");
11180
11347
  console.log(pc13.bold(pc13.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
11181
11348
  console.log("");
@@ -11239,120 +11406,1122 @@ Error: ${launchPlan.error}
11239
11406
  }
11240
11407
  }
11241
11408
 
11242
- // src/codex-app.ts
11409
+ // src/gemini.ts
11243
11410
  import pc14 from "picocolors";
11244
11411
  import * as p15 from "@clack/prompts";
11245
11412
 
11246
- // src/codex/app-config.ts
11247
- import { existsSync as existsSync13, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
11248
- import { dirname as dirname6, join as join14 } from "path";
11249
- import { parse, stringify } from "smol-toml";
11250
- function getCodexConfigPath() {
11251
- return join14(getCodexHome(), "config.toml");
11252
- }
11253
- function getCodexAppSidecarProfilePath() {
11254
- return join14(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
11255
- }
11256
- function asRecord(value) {
11257
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11413
+ // src/gemini/launch.ts
11414
+ import { execSync as execSync4, spawn as spawn5 } from "child_process";
11415
+ import { existsSync as existsSync13 } from "fs";
11416
+ import { homedir as homedir10 } from "os";
11417
+ import { join as join14 } from "path";
11418
+ var isWindows4 = process.platform === "win32";
11419
+ var GEMINI_FALLBACK_PATHS = isWindows4 ? [
11420
+ join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini.cmd"),
11421
+ join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini")
11422
+ ] : [
11423
+ join14(homedir10(), ".local", "bin", "gemini"),
11424
+ join14(homedir10(), ".npm", "bin", "gemini"),
11425
+ "/usr/local/bin/gemini",
11426
+ "/opt/homebrew/bin/gemini"
11427
+ ];
11428
+ function findGeminiBinary() {
11429
+ try {
11430
+ const result = execSync4(isWindows4 ? "where.exe gemini" : "which gemini", {
11431
+ encoding: "utf8",
11432
+ stdio: ["pipe", "pipe", "pipe"]
11433
+ });
11434
+ const path = result.trim().split("\n")[0]?.trim();
11435
+ if (path) return path;
11436
+ } catch {
11437
+ }
11438
+ for (const path of GEMINI_FALLBACK_PATHS) {
11439
+ if (existsSync13(path)) return path;
11440
+ }
11441
+ return null;
11258
11442
  }
11259
- function rootString(config, key) {
11260
- if (!(key in config)) return { had: false, value: "" };
11261
- const v = config[key];
11262
- return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
11443
+ function buildGeminiChildEnv(proxyPort, proxyToken) {
11444
+ const env = { ...process.env };
11445
+ delete env["GOOGLE_GEMINI_BASE_URL"];
11446
+ delete env["GEMINI_API_KEY"];
11447
+ delete env["GOOGLE_API_KEY"];
11448
+ delete env["GOOGLE_GENAI_API_KEY"];
11449
+ env["GOOGLE_GEMINI_BASE_URL"] = `http://127.0.0.1:${proxyPort}`;
11450
+ env["GEMINI_API_KEY"] = proxyToken;
11451
+ return env;
11263
11452
  }
11264
- function readCodexConfigText(path = getCodexConfigPath()) {
11265
- if (!existsSync13(path)) return "";
11266
- return readFileSync11(path, "utf8");
11453
+ function launchGemini(geminiPath, modelId, env, extraArgs) {
11454
+ return new Promise((resolve) => {
11455
+ const args = ["-m", modelId, ...extraArgs];
11456
+ const child = spawn5(geminiPath, args, {
11457
+ stdio: "inherit",
11458
+ env,
11459
+ shell: isWindows4
11460
+ });
11461
+ const onSigInt = () => child.kill("SIGINT");
11462
+ const onSigTerm = () => child.kill("SIGTERM");
11463
+ process.once("SIGINT", onSigInt);
11464
+ process.once("SIGTERM", onSigTerm);
11465
+ const done = (code) => {
11466
+ process.off("SIGINT", onSigInt);
11467
+ process.off("SIGTERM", onSigTerm);
11468
+ resolve(code);
11469
+ };
11470
+ child.on("error", () => done(1));
11471
+ child.on("exit", (code) => done(code ?? 0));
11472
+ });
11267
11473
  }
11268
- function parseCodexConfig(text5) {
11269
- if (!text5.trim()) return {};
11270
- return asRecord(parse(text5));
11474
+
11475
+ // src/gemini/prompts.ts
11476
+ import * as p14 from "@clack/prompts";
11477
+ async function pickGeminiProvider(providers, prefs, hasFavorites = false, initialProviderId) {
11478
+ if (providers.length === 0 && !hasFavorites) return null;
11479
+ const options = providers.map((lp) => providerSelectOption(lp));
11480
+ if (hasFavorites) {
11481
+ options.unshift({
11482
+ value: "__favorites__",
11483
+ label: "\u2B50 Favorites Catalog",
11484
+ hint: `${prefs.favoriteModels?.length ?? 0} saved favorites`
11485
+ });
11486
+ }
11487
+ const initial = initialProviderId && options.some((o) => o.value === initialProviderId) ? initialProviderId : prefs.lastGeminiProvider && options.some((o) => o.value === prefs.lastGeminiProvider) ? prefs.lastGeminiProvider : options[0].value;
11488
+ const chosen = await p14.select({
11489
+ message: "Which provider for Gemini CLI?",
11490
+ options,
11491
+ initialValue: initial
11492
+ });
11493
+ if (p14.isCancel(chosen)) {
11494
+ p14.cancel("Cancelled.");
11495
+ return null;
11496
+ }
11497
+ if (chosen === "__favorites__") return "__favorites__";
11498
+ return providers.find((lp) => lp.id === chosen) ?? null;
11271
11499
  }
11272
- function captureRestoreState(text5) {
11273
- const config = parseCodexConfig(text5);
11274
- const profile = rootString(config, "profile");
11275
- const model = rootString(config, "model");
11276
- const modelProvider = rootString(config, "model_provider");
11277
- const modelCatalog = rootString(config, "model_catalog_json");
11278
- const openAIBaseUrl = rootString(config, "openai_base_url");
11279
- const reasoning = rootString(config, "model_reasoning_effort");
11280
- return {
11281
- hadProfile: profile.had,
11282
- profile: profile.value,
11283
- hadModel: model.had,
11284
- model: model.value,
11285
- hadModelProvider: modelProvider.had,
11286
- modelProvider: modelProvider.value,
11287
- hadModelCatalogJson: modelCatalog.had,
11288
- modelCatalogJson: modelCatalog.value,
11289
- hadOpenAIBaseUrl: openAIBaseUrl.had,
11290
- openAIBaseUrl: openAIBaseUrl.value,
11291
- hadModelReasoningEffort: reasoning.had,
11292
- modelReasoningEffort: reasoning.value
11293
- };
11500
+ async function pickGeminiModel(provider, prefs) {
11501
+ const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, 3);
11502
+ const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
11503
+ let selectedModel = null;
11504
+ while (true) {
11505
+ if (recentModels.length > 0) {
11506
+ const options = [
11507
+ ...recentModels.map((m) => modelSelectOption(m, "recent")),
11508
+ navOption("__browse_all__", "Browse all models \u2192", `${provider.models.length} available`),
11509
+ navOption("__back__", "\u2190 Go back", "Select a different provider")
11510
+ ];
11511
+ const picked = await p14.select({
11512
+ message: `Model for ${provider.name}?`,
11513
+ options,
11514
+ initialValue: recentModels[0].id
11515
+ });
11516
+ if (p14.isCancel(picked) || String(picked) === "__back__") {
11517
+ return "back";
11518
+ }
11519
+ if (String(picked) === "__browse_all__") {
11520
+ const browsed = await browseAllModels(provider, prefs);
11521
+ if (browsed === "back") {
11522
+ continue;
11523
+ }
11524
+ if (!browsed) return null;
11525
+ selectedModel = browsed;
11526
+ break;
11527
+ } else {
11528
+ selectedModel = recentModels.find((m) => m.id === String(picked));
11529
+ break;
11530
+ }
11531
+ } else {
11532
+ const browsed = await browseAllModels(provider, prefs);
11533
+ if (browsed === "back") {
11534
+ return "back";
11535
+ }
11536
+ if (!browsed) return null;
11537
+ selectedModel = browsed;
11538
+ break;
11539
+ }
11540
+ }
11541
+ return selectedModel;
11294
11542
  }
11295
- function isAppManagedConfig(text5) {
11296
- const config = parseCodexConfig(text5);
11297
- const mp = rootString(config, "model_provider");
11298
- if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
11299
- const baseUrl = rootString(config, "openai_base_url");
11300
- const catalog = rootString(config, "model_catalog_json");
11301
- return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
11543
+ function confirmGeminiLaunch(providerName, modelLabel, modelId) {
11544
+ return p14.confirm({
11545
+ message: confirmLaunchMessage("Gemini CLI", modelLabel, modelId, providerName),
11546
+ initialValue: true
11547
+ }).then((answer) => {
11548
+ if (p14.isCancel(answer)) {
11549
+ p14.cancel("Cancelled.");
11550
+ return false;
11551
+ }
11552
+ return answer;
11553
+ });
11302
11554
  }
11303
- function mergeAppConfig(existing, spec) {
11304
- const patch = buildCodexAppRootConfig(spec);
11305
- const out = { ...existing };
11306
- delete out.profile;
11307
- out.model = patch.model;
11308
- out.model_provider = patch.model_provider;
11309
- out.openai_base_url = patch.openai_base_url;
11310
- out.model_catalog_json = patch.model_catalog_json;
11311
- const providers = asRecord(out.model_providers);
11312
- delete providers[CODEX_APP_PROVIDER_ID];
11313
- const profiles = asRecord(out.profiles);
11314
- delete profiles[CODEX_APP_PROVIDER_ID];
11315
- if (Object.keys(profiles).length === 0) {
11316
- delete out.profiles;
11317
- } else {
11318
- out.profiles = profiles;
11555
+ async function pickGeminiFavoriteModel(providers, favorites) {
11556
+ const favList = [];
11557
+ for (const fav of favorites) {
11558
+ const provider2 = providers.find((lp) => lp.id === fav.providerId);
11559
+ const model2 = provider2?.models.find((m) => m.id === fav.modelId);
11560
+ if (provider2 && model2) favList.push({ provider: provider2, model: model2 });
11319
11561
  }
11320
- if (Object.keys(providers).length === 0) {
11321
- delete out.model_providers;
11322
- } else {
11323
- out.model_providers = providers;
11562
+ if (favList.length === 0) {
11563
+ p14.log.warn("None of your saved favorites are available in the current registry.");
11564
+ return null;
11324
11565
  }
11325
- const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
11326
- if (existingEffort !== void 0) {
11327
- const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
11328
- providerId: spec.route.providerId,
11329
- apiBaseUrl: spec.route.baseURL,
11330
- supportedParameters: spec.route.supportedParameters,
11331
- reasoning: spec.route.reasoning,
11332
- interleavedReasoningField: spec.route.interleavedReasoningField
11333
- });
11334
- if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
11335
- if (caps.levels.length > 0 && caps.defaultLevel) {
11336
- out.model_reasoning_effort = caps.defaultLevel;
11337
- } else {
11338
- delete out.model_reasoning_effort;
11339
- }
11566
+ const options = [
11567
+ ...favList.map(({ provider: provider2, model: model2 }) => ({
11568
+ value: `${provider2.id}::${model2.id}`,
11569
+ label: model2.name || model2.id,
11570
+ hint: provider2.name
11571
+ })),
11572
+ { value: "__back__", label: "\u2190 Go back", hint: "Select a different provider" }
11573
+ ];
11574
+ const picked = await p14.select({
11575
+ message: "Pick a favorite model for Gemini CLI:",
11576
+ options,
11577
+ initialValue: options[0].value
11578
+ });
11579
+ if (p14.isCancel(picked) || String(picked) === "__back__") return "back";
11580
+ const [pickedProviderId, pickedModelId] = picked.split("::");
11581
+ const provider = providers.find((lp) => lp.id === pickedProviderId);
11582
+ const model = provider?.models.find((m) => m.id === pickedModelId);
11583
+ if (!provider || !model) return null;
11584
+ return { provider, model };
11585
+ }
11586
+ function rejectGeminiManagedFlags(geminiArgs) {
11587
+ const blocked = /* @__PURE__ */ new Set(["--provider", "--model", "-m", "--trace"]);
11588
+ const takesValue = /* @__PURE__ */ new Set(["--provider", "--model", "-m"]);
11589
+ const out = [];
11590
+ for (let i = 0; i < geminiArgs.length; i++) {
11591
+ const arg = geminiArgs[i];
11592
+ if (blocked.has(arg)) {
11593
+ if (takesValue.has(arg)) i++;
11594
+ continue;
11340
11595
  }
11596
+ if (arg.startsWith("--model=") || arg.startsWith("--provider=") || arg.startsWith("-m=")) continue;
11597
+ out.push(arg);
11341
11598
  }
11342
11599
  return out;
11343
11600
  }
11344
- function validateAppConfigText(text5, spec) {
11345
- const config = parseCodexConfig(text5);
11346
- if ("profile" in config) {
11347
- throw new Error("Generated config still contains legacy root profile key");
11601
+
11602
+ // src/gemini-proxy.ts
11603
+ import { createServer as createServer4 } from "http";
11604
+ import { randomUUID as randomUUID2 } from "crypto";
11605
+ import { streamText as streamText4, generateText as generateText4, tool as tool4, jsonSchema as jsonSchema4 } from "ai";
11606
+ function mapFinishReason(reason) {
11607
+ if (reason === "stop" || reason === "tool-calls") return "STOP";
11608
+ if (reason === "length") return "MAX_TOKENS";
11609
+ if (reason === "content-filter") return "SAFETY";
11610
+ return "OTHER";
11611
+ }
11612
+ function lookupGeminiRoute(routes, requestedModel) {
11613
+ const ids = [requestedModel, ...routeLookupIds(requestedModel)];
11614
+ const slashIdx = requestedModel.indexOf("/");
11615
+ if (slashIdx >= 0) {
11616
+ const after = requestedModel.slice(slashIdx + 1);
11617
+ ids.push(after, ...routeLookupIds(after));
11348
11618
  }
11349
- const profiles = asRecord(config.profiles);
11350
- if (profiles[CODEX_APP_PROVIDER_ID]) {
11351
- throw new Error("Generated config still contains legacy profiles table");
11619
+ const doubleUnderscore = requestedModel.indexOf("__");
11620
+ if (doubleUnderscore >= 0) {
11621
+ const after = requestedModel.slice(doubleUnderscore + 2);
11622
+ ids.push(after, ...routeLookupIds(after));
11352
11623
  }
11353
- const mp = rootString(config, "model_provider");
11354
- if (mp.value !== "openai") {
11355
- throw new Error("Generated config must keep the built-in OpenAI model_provider");
11624
+ const uniqueIds = [...new Set(ids)];
11625
+ for (const id of uniqueIds) {
11626
+ const route = routes.find((r) => r.aliasId === id || r.realModelId === id);
11627
+ if (route) return route;
11628
+ }
11629
+ return void 0;
11630
+ }
11631
+ function mergeConsecutiveMessages2(messages) {
11632
+ const merged = [];
11633
+ for (const msg of messages) {
11634
+ if (merged.length === 0) {
11635
+ merged.push(msg);
11636
+ continue;
11637
+ }
11638
+ const last = merged[merged.length - 1];
11639
+ if (last.role === msg.role) {
11640
+ const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
11641
+ const nextContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
11642
+ last.content = [...lastContent, ...nextContent];
11643
+ } else {
11644
+ merged.push(msg);
11645
+ }
11646
+ }
11647
+ return merged;
11648
+ }
11649
+ function stripGeminiIdentity(text5) {
11650
+ return text5.replace(/You are Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/I'm Gemini CLI[\s\S]*?(?=\n\n|$)/gi, "").replace(/Gemini CLI/gi, "AI CLI");
11651
+ }
11652
+ function translateGeminiRequest(body) {
11653
+ let system;
11654
+ if (body.systemInstruction?.parts) {
11655
+ const rawSystem = body.systemInstruction.parts.map((p21) => p21.text || "").join("\n");
11656
+ system = stripGeminiIdentity(rawSystem).trim();
11657
+ }
11658
+ const messages = [];
11659
+ const nameToIdList = /* @__PURE__ */ new Map();
11660
+ const contents = body.contents || [];
11661
+ for (const turn of contents) {
11662
+ const role = turn.role === "model" ? "assistant" : "user";
11663
+ const parts = [];
11664
+ const toolResults = [];
11665
+ const turnParts = turn.parts || [];
11666
+ for (const p21 of turnParts) {
11667
+ if (p21.text !== void 0) {
11668
+ const text5 = stripGeminiIdentity(p21.text);
11669
+ if (text5.includes("<thinking>")) {
11670
+ const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
11671
+ for (let i = 0; i < tokens.length; i++) {
11672
+ const token = tokens[i].trim();
11673
+ if (!token) continue;
11674
+ parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
11675
+ }
11676
+ } else {
11677
+ parts.push({ type: "text", text: text5 });
11678
+ }
11679
+ } else if (p21.inlineData) {
11680
+ parts.push({
11681
+ type: "image",
11682
+ image: Buffer.from(p21.inlineData.data, "base64"),
11683
+ mediaType: p21.inlineData.mimeType
11684
+ });
11685
+ } else if (p21.functionCall) {
11686
+ const id = "call_" + randomUUID2().replace(/-/g, "");
11687
+ const name = p21.functionCall.name;
11688
+ if (!nameToIdList.has(name)) nameToIdList.set(name, []);
11689
+ nameToIdList.get(name).push(id);
11690
+ parts.push({
11691
+ type: "tool-call",
11692
+ toolCallId: id,
11693
+ toolName: name,
11694
+ input: p21.functionCall.args || {}
11695
+ });
11696
+ } else if (p21.functionResponse) {
11697
+ const name = p21.functionResponse.name;
11698
+ const idList = nameToIdList.get(name) || [];
11699
+ const id = idList.shift() || "call_" + randomUUID2().replace(/-/g, "");
11700
+ toolResults.push({
11701
+ type: "tool-result",
11702
+ toolCallId: id,
11703
+ toolName: name,
11704
+ output: {
11705
+ type: "text",
11706
+ value: typeof p21.functionResponse.response === "string" ? p21.functionResponse.response : JSON.stringify(p21.functionResponse.response || {})
11707
+ }
11708
+ });
11709
+ }
11710
+ }
11711
+ if (toolResults.length > 0) {
11712
+ messages.push({
11713
+ role: "tool",
11714
+ content: toolResults
11715
+ });
11716
+ }
11717
+ if (parts.length > 0) {
11718
+ messages.push({
11719
+ role,
11720
+ content: parts
11721
+ });
11722
+ }
11723
+ }
11724
+ const mergedMessages = mergeConsecutiveMessages2(messages);
11725
+ let tools;
11726
+ if (body.tools) {
11727
+ tools = {};
11728
+ for (const t of body.tools) {
11729
+ if (t.functionDeclarations) {
11730
+ for (const fd of t.functionDeclarations) {
11731
+ tools[fd.name] = tool4({
11732
+ description: fd.description || "",
11733
+ inputSchema: jsonSchema4(fd.parameters || { type: "object", properties: {} })
11734
+ });
11735
+ }
11736
+ }
11737
+ }
11738
+ }
11739
+ let toolChoice;
11740
+ const mode = body.toolConfig?.functionCallingConfig?.mode;
11741
+ if (mode === "ANY") {
11742
+ toolChoice = "required";
11743
+ } else if (mode === "AUTO") {
11744
+ toolChoice = "auto";
11745
+ }
11746
+ const generationConfig = body.generationConfig || {};
11747
+ let responseFormat;
11748
+ if (generationConfig.responseMimeType === "application/json") {
11749
+ responseFormat = { type: "json" };
11750
+ }
11751
+ return {
11752
+ system,
11753
+ messages: mergedMessages,
11754
+ tools: tools && Object.keys(tools).length > 0 ? tools : void 0,
11755
+ toolChoice,
11756
+ maxOutputTokens: generationConfig.maxOutputTokens,
11757
+ temperature: generationConfig.temperature,
11758
+ responseFormat
11759
+ };
11760
+ }
11761
+ async function startGeminiProxy(routes, debug = false) {
11762
+ const proxyToken = randomUUID2();
11763
+ silenceSdkWarnings();
11764
+ if (routes.length === 0) {
11765
+ return Promise.reject(new Error("Gemini proxy requires at least one route"));
11766
+ }
11767
+ const defaultRoute = routes[0];
11768
+ const models = /* @__PURE__ */ new Map();
11769
+ const plog = debug ? makeTraceLogger(getGeminiProxyDebugLogPath()) : () => {
11770
+ };
11771
+ const onRejection = (reason) => {
11772
+ plog(`Unhandled Rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
11773
+ };
11774
+ const onException = (error) => {
11775
+ plog(`Uncaught Exception: ${error.stack || error.message}`);
11776
+ };
11777
+ const getOrInitModel = async (route) => {
11778
+ let m = models.get(route.aliasId);
11779
+ if (!m) {
11780
+ m = await createLanguageModel({
11781
+ npm: route.npm || "@ai-sdk/openai-compatible",
11782
+ modelId: route.realModelId,
11783
+ apiKey: route.apiKey,
11784
+ baseURL: route.baseURL,
11785
+ providerId: route.aliasId,
11786
+ authType: route.authType,
11787
+ oauthAccountId: route.oauthAccountId
11788
+ });
11789
+ models.set(route.aliasId, m);
11790
+ }
11791
+ return m;
11792
+ };
11793
+ const formatGeminiModel = (route) => ({
11794
+ name: `models/${route.aliasId}`,
11795
+ version: "1.0",
11796
+ displayName: route.displayName,
11797
+ description: "Registry model routed through relay-ai proxy",
11798
+ inputTokenLimit: route.contextWindow || 1e6,
11799
+ outputTokenLimit: 8192,
11800
+ supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
11801
+ });
11802
+ let sessionRouteOverride = void 0;
11803
+ const server = createServer4(async (req, res) => {
11804
+ try {
11805
+ const url = req.url ?? "";
11806
+ plog(`${req.method} ${url}`);
11807
+ if (req.method === "GET" && (url.endsWith("/models") || url.includes("/models?"))) {
11808
+ plog("GET models list");
11809
+ res.writeHead(200, { "Content-Type": "application/json" });
11810
+ const payload = JSON.stringify({
11811
+ models: routes.map(formatGeminiModel)
11812
+ });
11813
+ plog(`Response: ${payload}`);
11814
+ res.end(payload);
11815
+ return;
11816
+ }
11817
+ if (req.method === "GET" && url.includes("/models/")) {
11818
+ const modelMatch = url.match(/\/models\/([^?]+)/);
11819
+ if (modelMatch) {
11820
+ const modelId = decodeURIComponent(modelMatch[1]);
11821
+ const route = lookupGeminiRoute(routes, modelId) ?? defaultRoute;
11822
+ plog(`GET model details: ${modelId} -> mapped to route ${route.aliasId}`);
11823
+ res.writeHead(200, { "Content-Type": "application/json" });
11824
+ const payload = JSON.stringify(formatGeminiModel(route));
11825
+ plog(`Response: ${payload}`);
11826
+ res.end(payload);
11827
+ return;
11828
+ }
11829
+ }
11830
+ if (req.method === "POST" && url.includes(":")) {
11831
+ const isStream = url.includes("streamGenerateContent");
11832
+ const rawBody = await readBody(req);
11833
+ plog(`Request body:
11834
+ ${rawBody}`);
11835
+ let body;
11836
+ try {
11837
+ body = JSON.parse(rawBody);
11838
+ } catch {
11839
+ plog("Error: Invalid JSON body");
11840
+ res.writeHead(400);
11841
+ res.end("Invalid JSON");
11842
+ return;
11843
+ }
11844
+ const modelMatch = url.match(/\/models\/([^:]+)/);
11845
+ const requestedModel = modelMatch ? decodeURIComponent(modelMatch[1]) : defaultRoute.aliasId;
11846
+ const lastUserTurn = findLastUserTurn(body.contents || []);
11847
+ const modelCommand = parseModelCommand(lastUserTurn);
11848
+ if (modelCommand !== null) {
11849
+ if (modelCommand === "") {
11850
+ const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
11851
+ const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
11852
+ const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
11853
+ const text5 = `Current model: ${current.displayName} (${current.aliasId})
11854
+
11855
+ Available models:
11856
+ ${availableList}
11857
+
11858
+ \u{1F4A1} To switch models, type: .model <id>
11859
+ Example: .model ${exampleId}`;
11860
+ sendMockGeminiResponse(res, text5, isStream);
11861
+ return;
11862
+ }
11863
+ const targetRoute = lookupGeminiRoute(routes, modelCommand);
11864
+ if (targetRoute) {
11865
+ sessionRouteOverride = targetRoute;
11866
+ plog(`.model switch: ${targetRoute.aliasId} (${targetRoute.realModelId})`);
11867
+ sendMockGeminiResponse(res, `\u2705 Switched model to ${targetRoute.displayName} (${targetRoute.aliasId})`, isStream);
11868
+ } else {
11869
+ const available = routes.map((r) => r.aliasId).join(", ");
11870
+ sendMockGeminiResponse(res, `\u274C Model '${modelCommand}' not found.
11871
+
11872
+ Available: ${available}`, isStream);
11873
+ }
11874
+ return;
11875
+ }
11876
+ const route = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
11877
+ plog(`Route selected: ${route.aliasId} (upstream model: ${route.realModelId})`);
11878
+ body.contents = sanitizeModelSwitchTurns(body.contents || []);
11879
+ const languageModel = await getOrInitModel(route);
11880
+ const params = translateGeminiRequest(body);
11881
+ plog(`Translated SDK params:
11882
+ ${JSON.stringify(params, null, 2)}`);
11883
+ if (isStream) {
11884
+ res.writeHead(200, {
11885
+ "Content-Type": "text/event-stream",
11886
+ "Cache-Control": "no-cache",
11887
+ "Connection": "keep-alive"
11888
+ });
11889
+ plog("Starting streamText...");
11890
+ const { fullStream } = streamText4({
11891
+ model: languageModel,
11892
+ ...params
11893
+ });
11894
+ const toolCallBuffers = /* @__PURE__ */ new Map();
11895
+ let isThinking = false;
11896
+ for await (const part of fullStream) {
11897
+ const p21 = part;
11898
+ plog(`Stream chunk type: ${p21.type}`);
11899
+ if (isThinking && (p21.type === "tool-input-start" || p21.type === "tool-call" || p21.type === "finish")) {
11900
+ isThinking = false;
11901
+ const chunk = {
11902
+ candidates: [{ content: { role: "model", parts: [{ text: `
11903
+ </thinking>
11904
+
11905
+ ` }] } }]
11906
+ };
11907
+ res.write(`data: ${JSON.stringify(chunk)}
11908
+
11909
+ `);
11910
+ }
11911
+ if (p21.type === "reasoning") {
11912
+ let text5 = p21.textDelta ?? p21.text ?? "";
11913
+ if (!isThinking) {
11914
+ isThinking = true;
11915
+ text5 = `<thinking>
11916
+ ` + text5;
11917
+ }
11918
+ const chunk = {
11919
+ candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }]
11920
+ };
11921
+ res.write(`data: ${JSON.stringify(chunk)}
11922
+
11923
+ `);
11924
+ } else if (p21.type === "text-delta") {
11925
+ let text5 = p21.textDelta ?? p21.text ?? "";
11926
+ if (isThinking) {
11927
+ isThinking = false;
11928
+ text5 = `
11929
+ </thinking>
11930
+
11931
+ ` + text5;
11932
+ }
11933
+ const chunk = {
11934
+ candidates: [{
11935
+ content: {
11936
+ role: "model",
11937
+ parts: [{ text: text5 }]
11938
+ }
11939
+ }]
11940
+ };
11941
+ const data = `data: ${JSON.stringify(chunk)}
11942
+
11943
+ `;
11944
+ plog(`Streaming text delta: ${p21.textDelta}`);
11945
+ res.write(data);
11946
+ } else if (p21.type === "tool-input-start") {
11947
+ toolCallBuffers.set(p21.toolCallId, { name: p21.toolName, json: "" });
11948
+ } else if (p21.type === "tool-input-delta") {
11949
+ const buf = toolCallBuffers.get(p21.toolCallId);
11950
+ if (buf) buf.json += p21.delta;
11951
+ } else if (p21.type === "tool-call") {
11952
+ const buf = toolCallBuffers.get(p21.toolCallId);
11953
+ const args = buf ? JSON.parse(buf.json || "{}") : p21.input || {};
11954
+ const name = buf ? buf.name : p21.toolName;
11955
+ plog(`Streaming tool call: ${name} with args: ${JSON.stringify(args)}`);
11956
+ const chunk = {
11957
+ candidates: [{
11958
+ content: {
11959
+ role: "model",
11960
+ parts: [{
11961
+ functionCall: { name, args }
11962
+ }]
11963
+ }
11964
+ }]
11965
+ };
11966
+ res.write(`data: ${JSON.stringify(chunk)}
11967
+
11968
+ `);
11969
+ } else if (p21.type === "finish") {
11970
+ const chunk = {
11971
+ candidates: [{
11972
+ finishReason: mapFinishReason(p21.finishReason ?? "")
11973
+ }],
11974
+ usageMetadata: {
11975
+ promptTokenCount: p21.totalUsage?.inputTokens || 0,
11976
+ candidatesTokenCount: p21.totalUsage?.outputTokens || 0
11977
+ }
11978
+ };
11979
+ plog(`Stream finish. Reason: ${p21.finishReason}`);
11980
+ res.write(`data: ${JSON.stringify(chunk)}
11981
+
11982
+ `);
11983
+ }
11984
+ }
11985
+ res.end();
11986
+ plog("Stream ended.");
11987
+ } else {
11988
+ plog("Starting generateText...");
11989
+ const result = await generateText4({
11990
+ model: languageModel,
11991
+ ...params
11992
+ });
11993
+ plog("generateText finished.");
11994
+ const parts = [];
11995
+ if (result.reasoning) {
11996
+ parts.push({ text: `<thinking>
11997
+ ${result.reasoning}
11998
+ </thinking>
11999
+
12000
+ ` });
12001
+ }
12002
+ if (result.text) {
12003
+ parts.push({ text: result.text });
12004
+ }
12005
+ if (result.toolCalls?.length) {
12006
+ for (const tc of result.toolCalls) {
12007
+ parts.push({
12008
+ functionCall: { name: tc.toolName, args: tc.args }
12009
+ });
12010
+ }
12011
+ }
12012
+ const response = {
12013
+ candidates: [{
12014
+ content: {
12015
+ role: "model",
12016
+ parts
12017
+ },
12018
+ finishReason: mapFinishReason(result.finishReason ?? "")
12019
+ }],
12020
+ usageMetadata: {
12021
+ promptTokenCount: result.usage?.inputTokens || 0,
12022
+ candidatesTokenCount: result.usage?.outputTokens || 0
12023
+ }
12024
+ };
12025
+ plog(`Response:
12026
+ ${JSON.stringify(response, null, 2)}`);
12027
+ res.writeHead(200, { "Content-Type": "application/json" });
12028
+ res.end(JSON.stringify(response));
12029
+ }
12030
+ return;
12031
+ }
12032
+ plog(`404 Not Found: ${url}`);
12033
+ res.writeHead(404);
12034
+ res.end("Not Found");
12035
+ } catch (err) {
12036
+ plog(`Error handling request: ${err instanceof Error ? err.stack || err.message : String(err)}`);
12037
+ if (debug) {
12038
+ console.error("[Gemini Proxy] Critical error in handler:", err);
12039
+ }
12040
+ const errMsg = err instanceof Error ? err.message : String(err);
12041
+ if (!res.headersSent) {
12042
+ res.writeHead(500, { "Content-Type": "application/json" });
12043
+ res.end(JSON.stringify({ error: { message: errMsg } }));
12044
+ } else {
12045
+ try {
12046
+ res.write(`data: ${JSON.stringify({ error: { message: errMsg } })}
12047
+
12048
+ `);
12049
+ } catch {
12050
+ }
12051
+ res.end();
12052
+ }
12053
+ }
12054
+ });
12055
+ process.on("unhandledRejection", onRejection);
12056
+ process.on("uncaughtException", onException);
12057
+ const cleanup = () => {
12058
+ process.off("unhandledRejection", onRejection);
12059
+ process.off("uncaughtException", onException);
12060
+ };
12061
+ return new Promise((resolve, reject2) => {
12062
+ server.on("error", (err) => {
12063
+ cleanup();
12064
+ reject2(err);
12065
+ });
12066
+ server.listen(0, "127.0.0.1", () => {
12067
+ const addr = server.address();
12068
+ if (!addr || typeof addr === "string") {
12069
+ cleanup();
12070
+ reject2(new Error("Failed to bind gemini proxy"));
12071
+ return;
12072
+ }
12073
+ resolve({
12074
+ port: addr.port,
12075
+ token: proxyToken,
12076
+ close: () => {
12077
+ cleanup();
12078
+ server.close();
12079
+ }
12080
+ });
12081
+ });
12082
+ });
12083
+ }
12084
+ function sanitizeModelSwitchTurns(contents) {
12085
+ const cleaned = [];
12086
+ let i = 0;
12087
+ while (i < contents.length) {
12088
+ const turn = contents[i];
12089
+ if (isModelSwitchTurn(turn)) {
12090
+ i += 1;
12091
+ if (i < contents.length && contents[i]?.role === "model") {
12092
+ i += 1;
12093
+ }
12094
+ continue;
12095
+ }
12096
+ cleaned.push(turn);
12097
+ i += 1;
12098
+ }
12099
+ return cleaned;
12100
+ }
12101
+ function isModelSwitchTurn(turn) {
12102
+ if (turn?.role !== "user") return false;
12103
+ const parts = turn.parts || [];
12104
+ if (parts.length === 0) return false;
12105
+ const firstText = parts[0]?.text;
12106
+ if (typeof firstText !== "string") return false;
12107
+ return firstText.trim().startsWith(".model");
12108
+ }
12109
+ function findLastUserTurn(contents) {
12110
+ for (let i = contents.length - 1; i >= 0; i--) {
12111
+ if (contents[i]?.role === "user") return contents[i];
12112
+ }
12113
+ return void 0;
12114
+ }
12115
+ function parseModelCommand(turn) {
12116
+ if (!turn || turn.role !== "user") return null;
12117
+ const parts = turn.parts || [];
12118
+ if (parts.length !== 1) return null;
12119
+ const text5 = parts[0]?.text;
12120
+ if (typeof text5 !== "string") return null;
12121
+ const trimmed = text5.trim();
12122
+ if (!trimmed.startsWith(".model")) return null;
12123
+ if (trimmed === ".model") return "";
12124
+ if (trimmed.charAt(6) !== " ") return null;
12125
+ return trimmed.slice(7).trim();
12126
+ }
12127
+ function sendMockGeminiResponse(res, text5, isStream) {
12128
+ if (isStream) {
12129
+ res.writeHead(200, {
12130
+ "Content-Type": "text/event-stream",
12131
+ "Cache-Control": "no-cache",
12132
+ "Connection": "keep-alive"
12133
+ });
12134
+ const chunk = {
12135
+ candidates: [{
12136
+ content: { role: "model", parts: [{ text: text5 }] },
12137
+ finishReason: "STOP"
12138
+ }],
12139
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12140
+ };
12141
+ res.write(`data: ${JSON.stringify(chunk)}
12142
+
12143
+ `);
12144
+ const finishChunk = {
12145
+ candidates: [{ finishReason: "STOP" }],
12146
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12147
+ };
12148
+ res.write(`data: ${JSON.stringify(finishChunk)}
12149
+
12150
+ `);
12151
+ res.end();
12152
+ } else {
12153
+ res.writeHead(200, { "Content-Type": "application/json" });
12154
+ res.end(JSON.stringify({
12155
+ candidates: [{
12156
+ content: { role: "model", parts: [{ text: text5 }] },
12157
+ finishReason: "STOP"
12158
+ }],
12159
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12160
+ }));
12161
+ }
12162
+ }
12163
+
12164
+ // src/gemini.ts
12165
+ function geminiHelpText() {
12166
+ return `${pc14.bold("relay-ai gemini")} v${VERSION}
12167
+ Launch Google Gemini CLI with OpenCode Zen / Go or local registry providers.
12168
+
12169
+ ${pc14.bold("Usage:")}
12170
+ relay-ai gemini [options] [gemini-flags]
12171
+ relay-ai gemini --help
12172
+ relay-ai gemini --version
12173
+
12174
+ ${pc14.bold("Options:")}
12175
+ --trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
12176
+ --provider Boot provider id (skip wizard when paired with --model or non-interactive)
12177
+ --model Boot model id (skip wizard when paired with --provider or non-interactive)
12178
+ --help Show this command help
12179
+ --version Show version
12180
+
12181
+ ${pc14.bold("Description:")}
12182
+ Picks a provider and model from ~/.relay-ai/providers.json, starts a local Gemini-to-SDK translation
12183
+ proxy, and launches the Gemini CLI.
12184
+ All registry models (Anthropic, OpenAI, custom endpoints, etc.) route through the local translation proxy.
12185
+
12186
+ ${pc14.bold("Prerequisites:")}
12187
+ npm install -g @google/gemini-cli
12188
+
12189
+ ${pc14.bold("Passing flags to Gemini CLI:")}
12190
+ Add Gemini flags directly \u2014 no "--" separator needed.
12191
+ relay-ai manages -m / --model and -p / --prompt; other flags go to Gemini CLI.
12192
+
12193
+ ${pc14.bold("Examples:")}
12194
+ relay-ai gemini
12195
+ relay-ai gemini --trace
12196
+ relay-ai gemini --provider zen --model gemini-2.5-flash
12197
+ relay-ai gemini -p "review this file"`;
12198
+ }
12199
+ async function runGeminiCommand(geminiArgs, trace = false, launch = {}) {
12200
+ if (geminiArgs.includes("--help") || geminiArgs.includes("-h")) {
12201
+ console.log(geminiHelpText());
12202
+ return 0;
12203
+ }
12204
+ const geminiPath = findGeminiBinary();
12205
+ if (!geminiPath) {
12206
+ console.error(pc14.red("\nError: gemini binary not found on PATH.\n"));
12207
+ console.error("Install Google Gemini CLI:");
12208
+ console.error(" npm install -g @google/gemini-cli\n");
12209
+ return 1;
12210
+ }
12211
+ const passthroughArgs = rejectGeminiManagedFlags(geminiArgs);
12212
+ const agentStdout = wantsCleanAgentStdout("gemini", passthroughArgs);
12213
+ setAgentStdoutMode(agentStdout);
12214
+ const prefs = loadPreferences();
12215
+ const launchPlan = planLaunchWizard({
12216
+ explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
12217
+ childArgs: passthroughArgs,
12218
+ agent: "gemini",
12219
+ prefs
12220
+ });
12221
+ if (launchPlan.error) {
12222
+ console.error(pc14.red(`
12223
+ Error: ${launchPlan.error}
12224
+ `));
12225
+ return 1;
12226
+ }
12227
+ let catalog;
12228
+ if (agentStdout) {
12229
+ try {
12230
+ catalog = await fetchProviderCatalog({ agent: "gemini" });
12231
+ } catch (err) {
12232
+ console.error(pc14.red(String(err instanceof Error ? err.message : err)));
12233
+ return 1;
12234
+ }
12235
+ } else {
12236
+ const catalogSpinner = p15.spinner();
12237
+ catalogSpinner.start("Loading your providers...");
12238
+ try {
12239
+ catalog = await fetchProviderCatalog({ agent: "gemini" });
12240
+ } catch (err) {
12241
+ catalogSpinner.stop("");
12242
+ console.error(pc14.red(String(err instanceof Error ? err.message : err)));
12243
+ return 1;
12244
+ }
12245
+ catalogSpinner.stop("");
12246
+ }
12247
+ const compatible = providersForPicker(catalog);
12248
+ if (compatible.length === 0) {
12249
+ p15.log.warn("No Gemini-compatible providers in your registry.");
12250
+ p15.log.info("Add a provider with relay-ai providers add, or sign in with relay-ai providers auth openai.");
12251
+ return 0;
12252
+ }
12253
+ let activeProvider = compatible.find((lp) => lp.id === prefs.lastGeminiProvider) ?? compatible[0];
12254
+ let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastGeminiModel) ?? activeProvider.models[0];
12255
+ if (!selectedModel) {
12256
+ p15.log.error(`Provider "${activeProvider.name}" has no models available.`);
12257
+ return 1;
12258
+ }
12259
+ ;
12260
+ if (launchPlan.skip && launchPlan.target) {
12261
+ const resolved = findProviderAndModel(compatible, launchPlan.target);
12262
+ if (!resolved) {
12263
+ p15.log.error(
12264
+ `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
12265
+ );
12266
+ return 1;
12267
+ }
12268
+ activeProvider = resolved.provider;
12269
+ selectedModel = resolved.model;
12270
+ if (!agentStdout) {
12271
+ p15.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12272
+ }
12273
+ } else {
12274
+ if (!agentStdout) {
12275
+ console.log("");
12276
+ p15.log.info(`Launching ${pc14.bold("Gemini CLI")} with relay-ai`);
12277
+ }
12278
+ const chosenProvider = await pickGeminiProvider(
12279
+ compatible,
12280
+ prefs,
12281
+ (prefs.favoriteModels ?? []).length > 0,
12282
+ launch.launchProvider
12283
+ );
12284
+ if (!chosenProvider) return 0;
12285
+ if (chosenProvider === "__favorites__") {
12286
+ const favPick = await pickGeminiFavoriteModel(compatible, prefs.favoriteModels ?? []);
12287
+ if (!favPick || favPick === "back") return 0;
12288
+ activeProvider = favPick.provider;
12289
+ selectedModel = favPick.model;
12290
+ } else {
12291
+ activeProvider = chosenProvider;
12292
+ const chosenModel = await pickGeminiModel(activeProvider, prefs);
12293
+ if (!chosenModel || chosenModel === "back") return 0;
12294
+ selectedModel = chosenModel;
12295
+ }
12296
+ if (!agentStdout) {
12297
+ const ok = await confirmGeminiLaunch(
12298
+ activeProvider.name,
12299
+ selectedModel.name || selectedModel.id,
12300
+ selectedModel.id
12301
+ );
12302
+ if (!ok) return 0;
12303
+ }
12304
+ }
12305
+ recordLaunchSelection("gemini", activeProvider.id, selectedModel.id, prefs);
12306
+ const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
12307
+ if (!launchApiKey?.trim()) {
12308
+ p15.log.error(
12309
+ `No API key found for ${activeProvider.name}. Set it with relay-ai providers add.`
12310
+ );
12311
+ return 1;
12312
+ }
12313
+ const providerRoutes = activeProvider.models.map((m) => ({
12314
+ aliasId: m.id,
12315
+ realModelId: m.upstreamModelId || m.id,
12316
+ displayName: m.name || m.id,
12317
+ upstreamUrl: m.baseUrl || m.apiBaseUrl || "",
12318
+ apiKey: launchApiKey,
12319
+ modelFormat: m.modelFormat,
12320
+ contextWindow: m.contextWindow,
12321
+ npm: m.npm,
12322
+ baseURL: m.apiBaseUrl,
12323
+ providerId: activeProvider.id,
12324
+ authType: activeProvider.authType,
12325
+ oauthAccountId: activeProvider.oauthAccountId,
12326
+ supportedParameters: m.supportedParameters,
12327
+ reasoning: m.reasoning,
12328
+ interleavedReasoningField: m.interleavedReasoningField
12329
+ }));
12330
+ const resolvedFavs = [];
12331
+ const favorites = prefs.favoriteModels ?? [];
12332
+ for (const fav of favorites) {
12333
+ const provider = compatible.find((lp) => lp.id === fav.providerId);
12334
+ const model = provider?.models.find((m) => m.id === fav.modelId);
12335
+ if (provider && model) {
12336
+ const apiKey = await resolveLocalProviderApiKey(provider);
12337
+ if (apiKey) {
12338
+ resolvedFavs.push({
12339
+ aliasId: model.id,
12340
+ realModelId: model.upstreamModelId || model.id,
12341
+ displayName: model.name || model.id,
12342
+ upstreamUrl: model.baseUrl || model.apiBaseUrl || "",
12343
+ apiKey,
12344
+ modelFormat: model.modelFormat,
12345
+ contextWindow: model.contextWindow,
12346
+ npm: model.npm,
12347
+ baseURL: model.apiBaseUrl,
12348
+ providerId: provider.id,
12349
+ authType: provider.authType,
12350
+ oauthAccountId: provider.oauthAccountId,
12351
+ supportedParameters: model.supportedParameters,
12352
+ reasoning: model.reasoning,
12353
+ interleavedReasoningField: model.interleavedReasoningField
12354
+ });
12355
+ }
12356
+ }
12357
+ }
12358
+ const routesMap = /* @__PURE__ */ new Map();
12359
+ for (const route of providerRoutes) {
12360
+ routesMap.set(route.aliasId, route);
12361
+ }
12362
+ for (const route of resolvedFavs) {
12363
+ if (!routesMap.has(route.aliasId)) {
12364
+ routesMap.set(route.aliasId, route);
12365
+ }
12366
+ }
12367
+ const startingRoute = routesMap.get(selectedModel.id);
12368
+ if (!startingRoute) {
12369
+ routesMap.set(selectedModel.id, {
12370
+ aliasId: selectedModel.id,
12371
+ realModelId: selectedModel.upstreamModelId || selectedModel.id,
12372
+ displayName: selectedModel.name || selectedModel.id,
12373
+ upstreamUrl: selectedModel.baseUrl || selectedModel.apiBaseUrl || "",
12374
+ apiKey: launchApiKey,
12375
+ modelFormat: selectedModel.modelFormat,
12376
+ contextWindow: selectedModel.contextWindow,
12377
+ npm: selectedModel.npm,
12378
+ baseURL: selectedModel.apiBaseUrl,
12379
+ providerId: activeProvider.id,
12380
+ authType: activeProvider.authType,
12381
+ oauthAccountId: activeProvider.oauthAccountId,
12382
+ supportedParameters: selectedModel.supportedParameters,
12383
+ reasoning: selectedModel.reasoning,
12384
+ interleavedReasoningField: selectedModel.interleavedReasoningField
12385
+ });
12386
+ }
12387
+ const finalRoutes = [...routesMap.values()];
12388
+ let proxyHandle = null;
12389
+ try {
12390
+ proxyHandle = await startGeminiProxy(finalRoutes, trace);
12391
+ } catch (err) {
12392
+ p15.log.error(`Failed to start Gemini proxy: ${err instanceof Error ? err.message : String(err)}`);
12393
+ return 1;
12394
+ }
12395
+ const childEnv = buildGeminiChildEnv(proxyHandle.port, proxyHandle.token);
12396
+ if (!agentStdout) {
12397
+ p15.log.info(`Gemini proxy started on port ${proxyHandle.port}`);
12398
+ p15.log.info(`\u{1F4A1} Type ${pc14.bold(".model <id>")} in the chat to switch models mid-session.`);
12399
+ }
12400
+ const exitCode = await launchGemini(geminiPath, selectedModel.id, childEnv, passthroughArgs);
12401
+ proxyHandle.close();
12402
+ if (!agentStdout) {
12403
+ p15.log.info("Gemini proxy stopped.");
12404
+ }
12405
+ if (trace) {
12406
+ printTraceLog(getGeminiProxyDebugLogPath());
12407
+ }
12408
+ return exitCode;
12409
+ }
12410
+
12411
+ // src/codex-app.ts
12412
+ import pc15 from "picocolors";
12413
+ import * as p17 from "@clack/prompts";
12414
+
12415
+ // src/codex/app-config.ts
12416
+ import { existsSync as existsSync14, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
12417
+ import { dirname as dirname6, join as join15 } from "path";
12418
+ import { parse, stringify } from "smol-toml";
12419
+ function getCodexConfigPath() {
12420
+ return join15(getCodexHome(), "config.toml");
12421
+ }
12422
+ function getCodexAppSidecarProfilePath() {
12423
+ return join15(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
12424
+ }
12425
+ function asRecord(value) {
12426
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12427
+ }
12428
+ function rootString(config, key) {
12429
+ if (!(key in config)) return { had: false, value: "" };
12430
+ const v = config[key];
12431
+ return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
12432
+ }
12433
+ function readCodexConfigText(path = getCodexConfigPath()) {
12434
+ if (!existsSync14(path)) return "";
12435
+ return readFileSync11(path, "utf8");
12436
+ }
12437
+ function parseCodexConfig(text5) {
12438
+ if (!text5.trim()) return {};
12439
+ return asRecord(parse(text5));
12440
+ }
12441
+ function captureRestoreState(text5) {
12442
+ const config = parseCodexConfig(text5);
12443
+ const profile = rootString(config, "profile");
12444
+ const model = rootString(config, "model");
12445
+ const modelProvider = rootString(config, "model_provider");
12446
+ const modelCatalog = rootString(config, "model_catalog_json");
12447
+ const openAIBaseUrl = rootString(config, "openai_base_url");
12448
+ const reasoning = rootString(config, "model_reasoning_effort");
12449
+ return {
12450
+ hadProfile: profile.had,
12451
+ profile: profile.value,
12452
+ hadModel: model.had,
12453
+ model: model.value,
12454
+ hadModelProvider: modelProvider.had,
12455
+ modelProvider: modelProvider.value,
12456
+ hadModelCatalogJson: modelCatalog.had,
12457
+ modelCatalogJson: modelCatalog.value,
12458
+ hadOpenAIBaseUrl: openAIBaseUrl.had,
12459
+ openAIBaseUrl: openAIBaseUrl.value,
12460
+ hadModelReasoningEffort: reasoning.had,
12461
+ modelReasoningEffort: reasoning.value
12462
+ };
12463
+ }
12464
+ function isAppManagedConfig(text5) {
12465
+ const config = parseCodexConfig(text5);
12466
+ const mp = rootString(config, "model_provider");
12467
+ if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
12468
+ const baseUrl = rootString(config, "openai_base_url");
12469
+ const catalog = rootString(config, "model_catalog_json");
12470
+ return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
12471
+ }
12472
+ function mergeAppConfig(existing, spec) {
12473
+ const patch = buildCodexAppRootConfig(spec);
12474
+ const out = { ...existing };
12475
+ delete out.profile;
12476
+ out.model = patch.model;
12477
+ out.model_provider = patch.model_provider;
12478
+ out.openai_base_url = patch.openai_base_url;
12479
+ out.model_catalog_json = patch.model_catalog_json;
12480
+ const providers = asRecord(out.model_providers);
12481
+ delete providers[CODEX_APP_PROVIDER_ID];
12482
+ const profiles = asRecord(out.profiles);
12483
+ delete profiles[CODEX_APP_PROVIDER_ID];
12484
+ if (Object.keys(profiles).length === 0) {
12485
+ delete out.profiles;
12486
+ } else {
12487
+ out.profiles = profiles;
12488
+ }
12489
+ if (Object.keys(providers).length === 0) {
12490
+ delete out.model_providers;
12491
+ } else {
12492
+ out.model_providers = providers;
12493
+ }
12494
+ const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
12495
+ if (existingEffort !== void 0) {
12496
+ const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
12497
+ providerId: spec.route.providerId,
12498
+ apiBaseUrl: spec.route.baseURL,
12499
+ supportedParameters: spec.route.supportedParameters,
12500
+ reasoning: spec.route.reasoning,
12501
+ interleavedReasoningField: spec.route.interleavedReasoningField
12502
+ });
12503
+ if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
12504
+ if (caps.levels.length > 0 && caps.defaultLevel) {
12505
+ out.model_reasoning_effort = caps.defaultLevel;
12506
+ } else {
12507
+ delete out.model_reasoning_effort;
12508
+ }
12509
+ }
12510
+ }
12511
+ return out;
12512
+ }
12513
+ function validateAppConfigText(text5, spec) {
12514
+ const config = parseCodexConfig(text5);
12515
+ if ("profile" in config) {
12516
+ throw new Error("Generated config still contains legacy root profile key");
12517
+ }
12518
+ const profiles = asRecord(config.profiles);
12519
+ if (profiles[CODEX_APP_PROVIDER_ID]) {
12520
+ throw new Error("Generated config still contains legacy profiles table");
12521
+ }
12522
+ const mp = rootString(config, "model_provider");
12523
+ if (mp.value !== "openai") {
12524
+ throw new Error("Generated config must keep the built-in OpenAI model_provider");
11356
12525
  }
11357
12526
  const baseUrl = rootString(config, "openai_base_url");
11358
12527
  if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
@@ -11409,13 +12578,13 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
11409
12578
  }
11410
12579
  applyRestoreKey(config, "model_reasoning_effort", state.hadModelReasoningEffort, state.modelReasoningEffort);
11411
12580
  const sidecar = getCodexAppSidecarProfilePath();
11412
- if (existsSync13(sidecar)) {
12581
+ if (existsSync14(sidecar)) {
11413
12582
  try {
11414
12583
  rmSync2(sidecar, { force: true });
11415
12584
  } catch {
11416
12585
  }
11417
12586
  }
11418
- const hadFile = existsSync13(configPath);
12587
+ const hadFile = existsSync14(configPath);
11419
12588
  const empty = Object.keys(config).length === 0 || Object.keys(config).length === 1 && "model_providers" in config && Object.keys(asRecord(config.model_providers)).length === 0;
11420
12589
  if (!hadFile && empty) return false;
11421
12590
  if (empty) {
@@ -11436,25 +12605,25 @@ function previewAppConfigToml(spec) {
11436
12605
  // src/codex/app-session.ts
11437
12606
  import {
11438
12607
  copyFileSync as copyFileSync4,
11439
- existsSync as existsSync14,
12608
+ existsSync as existsSync15,
11440
12609
  mkdirSync as mkdirSync8,
11441
12610
  readdirSync as readdirSync2,
11442
12611
  readFileSync as readFileSync12,
11443
12612
  rmSync as rmSync3
11444
12613
  } from "fs";
11445
- import { basename as basename2, join as join15 } from "path";
12614
+ import { basename as basename2, join as join16 } from "path";
11446
12615
  function getAppSessionLockPath(env = process.env) {
11447
- return join15(getRelayAiCodexDir(env), "session-app.json");
12616
+ return join16(getRelayAiCodexDir(env), "session-app.json");
11448
12617
  }
11449
12618
  function getAppRestoreStatePath(env = process.env) {
11450
- return join15(getRelayAiCodexDir(env), "app-restore-state.json");
12619
+ return join16(getRelayAiCodexDir(env), "app-restore-state.json");
11451
12620
  }
11452
12621
  function getAppCatalogPath(providerId, env = process.env) {
11453
- return join15(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
12622
+ return join16(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11454
12623
  }
11455
12624
  function readAppSessionLock(env = process.env) {
11456
12625
  const path = getAppSessionLockPath(env);
11457
- if (!existsSync14(path)) return null;
12626
+ if (!existsSync15(path)) return null;
11458
12627
  try {
11459
12628
  const parsed = JSON.parse(readFileSync12(path, "utf8"));
11460
12629
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
@@ -11468,11 +12637,11 @@ function writeAppSessionLock(lock, env = process.env) {
11468
12637
  }
11469
12638
  function clearAppSessionLock(env = process.env) {
11470
12639
  const path = getAppSessionLockPath(env);
11471
- if (existsSync14(path)) rmSync3(path, { force: true });
12640
+ if (existsSync15(path)) rmSync3(path, { force: true });
11472
12641
  }
11473
12642
  function readAppRestoreState(env = process.env) {
11474
12643
  const path = getAppRestoreStatePath(env);
11475
- if (!existsSync14(path)) return null;
12644
+ if (!existsSync15(path)) return null;
11476
12645
  try {
11477
12646
  return JSON.parse(readFileSync12(path, "utf8"));
11478
12647
  } catch {
@@ -11486,16 +12655,16 @@ function writeAppRestoreState(state, env = process.env) {
11486
12655
  }
11487
12656
  function clearAppRestoreState(env = process.env) {
11488
12657
  const path = getAppRestoreStatePath(env);
11489
- if (existsSync14(path)) rmSync3(path, { force: true });
12658
+ if (existsSync15(path)) rmSync3(path, { force: true });
11490
12659
  }
11491
12660
  function backupConfigToml(env = process.env) {
11492
12661
  const configPath = getCodexConfigPath();
11493
- if (!existsSync14(configPath)) return void 0;
12662
+ if (!existsSync15(configPath)) return void 0;
11494
12663
  rotateBackups(configPath, env);
11495
12664
  const backupsDir = getBackupsDir(env);
11496
12665
  mkdirSync8(backupsDir, { recursive: true });
11497
12666
  const base = basename2(configPath);
11498
- const backupPath = join15(backupsDir, `${base}.${Date.now()}.bak`);
12667
+ const backupPath = join16(backupsDir, `${base}.${Date.now()}.bak`);
11499
12668
  copyFileSync4(configPath, backupPath);
11500
12669
  return backupPath;
11501
12670
  }
@@ -11511,8 +12680,8 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
11511
12680
  }
11512
12681
  function ownedAppCatalogPaths(env = process.env) {
11513
12682
  const codexDir = getRelayAiCodexDir(env);
11514
- if (!existsSync14(codexDir)) return [];
11515
- return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join15(codexDir, n));
12683
+ if (!existsSync15(codexDir)) return [];
12684
+ return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join16(codexDir, n));
11516
12685
  }
11517
12686
  function removeAppCatalogs(env = process.env) {
11518
12687
  const removed = [];
@@ -11544,7 +12713,7 @@ function restoreCodexAppOverlay(env = process.env) {
11544
12713
  }
11545
12714
  if (restoreState) {
11546
12715
  restoreConfigFromState(restoreState);
11547
- } else if (lock?.backupPath && existsSync14(lock.backupPath)) {
12716
+ } else if (lock?.backupPath && existsSync15(lock.backupPath)) {
11548
12717
  copyFileSync4(lock.backupPath, getCodexConfigPath());
11549
12718
  }
11550
12719
  removeAppCatalogs(env);
@@ -11590,11 +12759,11 @@ function waitForShutdown2() {
11590
12759
  }
11591
12760
 
11592
12761
  // src/codex/app-launch.ts
11593
- import { execSync as execSync4 } from "child_process";
11594
- import { existsSync as existsSync15, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
11595
- import { homedir as homedir10 } from "os";
11596
- import { join as join16 } from "path";
11597
- import * as p14 from "@clack/prompts";
12762
+ import { execSync as execSync5 } from "child_process";
12763
+ import { existsSync as existsSync16, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
12764
+ import { homedir as homedir11 } from "os";
12765
+ import { join as join17 } from "path";
12766
+ import * as p16 from "@clack/prompts";
11598
12767
  var CODEX_BUNDLE_ID = "com.openai.codex";
11599
12768
  function codexAppSupported() {
11600
12769
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -11602,7 +12771,7 @@ function codexAppSupported() {
11602
12771
  }
11603
12772
  }
11604
12773
  function run(cmd, encoding = "utf8") {
11605
- return execSync4(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
12774
+ return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
11606
12775
  }
11607
12776
  function runPowerShell(script) {
11608
12777
  return run(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
@@ -11610,30 +12779,30 @@ function runPowerShell(script) {
11610
12779
  function darwinAppCandidates() {
11611
12780
  return [
11612
12781
  "/Applications/Codex.app",
11613
- join16(homedir10(), "Applications", "Codex.app")
12782
+ join17(homedir11(), "Applications", "Codex.app")
11614
12783
  ];
11615
12784
  }
11616
12785
  function winLocalAppData() {
11617
- return process.env.LOCALAPPDATA ?? join16(homedir10(), "AppData", "Local");
12786
+ return process.env.LOCALAPPDATA ?? join17(homedir11(), "AppData", "Local");
11618
12787
  }
11619
12788
  function winCodexExeCandidates() {
11620
12789
  const local = winLocalAppData();
11621
12790
  const bases = [
11622
- join16(local, "Programs", "Codex"),
11623
- join16(local, "Programs", "OpenAI Codex"),
11624
- join16(local, "Codex"),
11625
- join16(local, "OpenAI Codex"),
11626
- join16(local, "OpenAI", "Codex"),
11627
- join16(local, "openai-codex-electron")
12791
+ join17(local, "Programs", "Codex"),
12792
+ join17(local, "Programs", "OpenAI Codex"),
12793
+ join17(local, "Codex"),
12794
+ join17(local, "OpenAI Codex"),
12795
+ join17(local, "OpenAI", "Codex"),
12796
+ join17(local, "openai-codex-electron")
11628
12797
  ];
11629
12798
  const out = [];
11630
12799
  for (const base of bases) {
11631
- out.push(join16(base, "Codex.exe"));
12800
+ out.push(join17(base, "Codex.exe"));
11632
12801
  try {
11633
- if (existsSync15(base)) {
12802
+ if (existsSync16(base)) {
11634
12803
  for (const name of readdirSync3(base)) {
11635
12804
  if (name.startsWith("app-")) {
11636
- out.push(join16(base, name, "Codex.exe"));
12805
+ out.push(join17(base, name, "Codex.exe"));
11637
12806
  }
11638
12807
  }
11639
12808
  }
@@ -11646,7 +12815,7 @@ function mdfindCodexApp() {
11646
12815
  try {
11647
12816
  const out = run(`mdfind "kMDItemCFBundleIdentifier == '${CODEX_BUNDLE_ID}'"`);
11648
12817
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
11649
- return first && existsSync15(first) ? first : null;
12818
+ return first && existsSync16(first) ? first : null;
11650
12819
  } catch {
11651
12820
  return null;
11652
12821
  }
@@ -11654,14 +12823,14 @@ function mdfindCodexApp() {
11654
12823
  function findCodexApp() {
11655
12824
  if (process.platform === "darwin") {
11656
12825
  for (const path of darwinAppCandidates()) {
11657
- if (existsSync15(path)) return path;
12826
+ if (existsSync16(path)) return path;
11658
12827
  }
11659
12828
  return mdfindCodexApp();
11660
12829
  }
11661
12830
  if (process.platform === "win32") {
11662
12831
  for (const path of winCodexExeCandidates()) {
11663
12832
  try {
11664
- if (existsSync15(path) && statSync4(path).isFile()) return path;
12833
+ if (existsSync16(path) && statSync4(path).isFile()) return path;
11665
12834
  } catch {
11666
12835
  }
11667
12836
  }
@@ -11725,9 +12894,9 @@ async function waitForQuit(timeoutMs) {
11725
12894
  function openCodexAppAt(path) {
11726
12895
  if (process.platform === "darwin") {
11727
12896
  if (path.endsWith(".app")) {
11728
- execSync4(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
12897
+ execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
11729
12898
  } else {
11730
- execSync4(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
12899
+ execSync5(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
11731
12900
  }
11732
12901
  return;
11733
12902
  }
@@ -11750,9 +12919,9 @@ function openCodexApp() {
11750
12919
  }
11751
12920
  function darwinQuit() {
11752
12921
  try {
11753
- execSync4(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
12922
+ execSync5(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
11754
12923
  } catch {
11755
- execSync4(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
12924
+ execSync5(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
11756
12925
  }
11757
12926
  }
11758
12927
  function winQuitGraceful() {
@@ -11780,9 +12949,9 @@ async function launchOrRestartCodexApp(prompt = "Restart Codex to apply relay-ai
11780
12949
  openCodexAppAt(appPath);
11781
12950
  return;
11782
12951
  }
11783
- const restart = await p14.confirm({ message: prompt, initialValue: true });
11784
- if (p14.isCancel(restart) || !restart) {
11785
- p14.log.info("Quit and reopen Codex when you are ready for the new model to take effect.");
12952
+ const restart = await p16.confirm({ message: prompt, initialValue: true });
12953
+ if (p16.isCancel(restart) || !restart) {
12954
+ p16.log.info("Quit and reopen Codex when you are ready for the new model to take effect.");
11786
12955
  return;
11787
12956
  }
11788
12957
  if (process.platform === "darwin") darwinQuit();
@@ -11800,9 +12969,9 @@ function codexAppInstallHint() {
11800
12969
 
11801
12970
  // src/codex-app.ts
11802
12971
  function codexAppHelpText() {
11803
- return `${pc14.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
12972
+ return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
11804
12973
 
11805
- ${pc14.bold("Usage:")}
12974
+ ${pc15.bold("Usage:")}
11806
12975
  relay-ai codex-app [options]
11807
12976
  relay-ai codex-app --vertex
11808
12977
  relay-ai codex-app --restore
@@ -11810,7 +12979,7 @@ ${pc14.bold("Usage:")}
11810
12979
  relay-ai codex-app --help
11811
12980
  relay-ai codex-app --version
11812
12981
 
11813
- ${pc14.bold("Options:")}
12982
+ ${pc15.bold("Options:")}
11814
12983
  --vertex Use Claude models through Google Vertex AI
11815
12984
  --restore Restore Codex config after an interrupted app session
11816
12985
  --config Preview the generated Codex app configuration without launching
@@ -11818,31 +12987,31 @@ ${pc14.bold("Options:")}
11818
12987
  --help Show this command help
11819
12988
  --version Show version
11820
12989
 
11821
- ${pc14.bold("Description:")}
12990
+ ${pc15.bold("Description:")}
11822
12991
  Picks a provider and model from ~/.relay-ai/providers.json, patches ~/.codex/config.toml
11823
12992
  (with backup + restore on Ctrl+C), starts a local Responses proxy, and opens the
11824
12993
  Codex desktop app. Keep this terminal open while using Codex.
11825
12994
 
11826
- ${pc14.bold("Platforms:")}
12995
+ ${pc15.bold("Platforms:")}
11827
12996
  macOS and Windows. Linux is not supported (no Codex desktop app).
11828
12997
 
11829
- ${pc14.bold("Cleanup:")}
12998
+ ${pc15.bold("Cleanup:")}
11830
12999
  Ctrl+C stops the proxy and restores your previous Codex config.
11831
13000
  After crash: relay-ai codex-app --restore
11832
13001
 
11833
- ${pc14.bold("Preview (no writes):")}
13002
+ ${pc15.bold("Preview (no writes):")}
11834
13003
  relay-ai codex-app --config
11835
13004
 
11836
13005
  See docs/CODEX.md for CLI vs app, files touched, and restore.
11837
13006
 
11838
- ${pc14.bold("Examples:")}
13007
+ ${pc15.bold("Examples:")}
11839
13008
  relay-ai codex-app
11840
13009
  relay-ai codex-app --vertex
11841
13010
  relay-ai codex-app --config
11842
13011
  relay-ai codex-app --restore
11843
13012
 
11844
- ${pc14.bold("Favorites:")}
11845
- When you have saved favorites via ${pc14.cyan("relay-ai models")}, the Codex App
13013
+ ${pc15.bold("Favorites:")}
13014
+ When you have saved favorites via ${pc15.cyan("relay-ai models")}, the Codex App
11846
13015
  picker will show your starting model + favorites for mid-session switching.
11847
13016
  Zen/Go favorites are included when an OpenCode API key is available.`;
11848
13017
  }
@@ -11864,26 +13033,26 @@ function vertexEntryToLocalModel2(entry) {
11864
13033
  }
11865
13034
  async function runCodexAppVertexLaunch(configOnly, trace = false) {
11866
13035
  if (!hasApplicationDefaultCredentials()) {
11867
- p15.log.error("Google Application Default Credentials not found.");
11868
- p15.log.info("Run: gcloud auth application-default login");
13036
+ p17.log.error("Google Application Default Credentials not found.");
13037
+ p17.log.info("Run: gcloud auth application-default login");
11869
13038
  return 1;
11870
13039
  }
11871
13040
  const config = buildVertexRuntimeConfig();
11872
13041
  if (!config) {
11873
- p15.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
11874
- p15.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
13042
+ p17.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
13043
+ p17.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
11875
13044
  return 1;
11876
13045
  }
11877
13046
  let selectedEntry;
11878
13047
  if (config.models.length === 1) {
11879
13048
  selectedEntry = config.models[0];
11880
13049
  } else {
11881
- const choice = await p15.select({
13050
+ const choice = await p17.select({
11882
13051
  message: "Select a starting Vertex AI model:",
11883
13052
  options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
11884
13053
  });
11885
- if (p15.isCancel(choice)) {
11886
- p15.cancel("Cancelled.");
13054
+ if (p17.isCancel(choice)) {
13055
+ p17.cancel("Cancelled.");
11887
13056
  return 0;
11888
13057
  }
11889
13058
  selectedEntry = choice;
@@ -11905,19 +13074,19 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11905
13074
  const home = process.env["HOME"] ?? "";
11906
13075
  const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
11907
13076
  console.log("");
11908
- console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
13077
+ console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
11909
13078
  console.log("");
11910
- console.log(` ${pc14.bold("Mode:")} Vertex AI`);
11911
- console.log(` ${pc14.bold("Project:")} ${config.project}`);
11912
- console.log(` ${pc14.bold("Location:")} ${config.location}`);
11913
- console.log(` ${pc14.bold("Model:")} ${selectedEntry.display_name}`);
11914
- console.log(` ${pc14.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
13079
+ console.log(` ${pc15.bold("Mode:")} Vertex AI`);
13080
+ console.log(` ${pc15.bold("Project:")} ${config.project}`);
13081
+ console.log(` ${pc15.bold("Location:")} ${config.location}`);
13082
+ console.log(` ${pc15.bold("Model:")} ${selectedEntry.display_name}`);
13083
+ console.log(` ${pc15.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
11915
13084
  console.log("");
11916
- console.log(` ${pc14.bold("Catalog file:")}`);
11917
- console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
13085
+ console.log(` ${pc15.bold("Catalog file:")}`);
13086
+ console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
11918
13087
  console.log("");
11919
- console.log(pc14.dim(" No app was launched."));
11920
- console.log(pc14.dim(" Run ") + pc14.cyan("relay-ai codex-app --vertex") + pc14.dim(" to launch."));
13088
+ console.log(pc15.dim(" No app was launched."));
13089
+ console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app --vertex") + pc15.dim(" to launch."));
11921
13090
  console.log("");
11922
13091
  return 0;
11923
13092
  }
@@ -11956,14 +13125,14 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11956
13125
  proxyPort
11957
13126
  });
11958
13127
  sessionActive = true;
11959
- p15.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
13128
+ p17.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11960
13129
  logProxy(proxyPort);
11961
13130
  logActiveModel(selectedEntry.display_name, selectedEntry.id);
11962
13131
  try {
11963
13132
  await launchOrRestartCodexApp();
11964
13133
  } catch (err) {
11965
- p15.log.warn(String(err instanceof Error ? err.message : err));
11966
- p15.log.info(codexAppInstallHint());
13134
+ p17.log.warn(String(err instanceof Error ? err.message : err));
13135
+ p17.log.info(codexAppInstallHint());
11967
13136
  }
11968
13137
  printCodexAppSessionPanel({
11969
13138
  modelLabel: selectedEntry.display_name,
@@ -11979,8 +13148,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11979
13148
  sessionActive = false;
11980
13149
  }
11981
13150
  if (isCodexAppRunning()) {
11982
- const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
11983
- if (shouldClose && !p15.isCancel(shouldClose)) {
13151
+ const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13152
+ if (shouldClose && !p17.isCancel(shouldClose)) {
11984
13153
  quitCodexAppGracefully();
11985
13154
  }
11986
13155
  }
@@ -12003,7 +13172,7 @@ async function runCodexAppCommand(args, opts = {}) {
12003
13172
  try {
12004
13173
  codexAppSupported();
12005
13174
  } catch (err) {
12006
- console.error(pc14.red(String(err instanceof Error ? err.message : err)));
13175
+ console.error(pc15.red(String(err instanceof Error ? err.message : err)));
12007
13176
  return 1;
12008
13177
  }
12009
13178
  const interrupted = recoverInterruptedCodexAppSession();
@@ -12011,17 +13180,17 @@ async function runCodexAppCommand(args, opts = {}) {
12011
13180
  const trace = args.includes("--trace");
12012
13181
  const debugLogPath = getCodexProxyDebugLogPath();
12013
13182
  if (trace && !configOnly) {
12014
- p15.log.info(`Debug log: ${debugLogPath}`);
13183
+ p17.log.info(`Debug log: ${debugLogPath}`);
12015
13184
  }
12016
13185
  const isTty = Boolean(process.stdin.isTTY);
12017
13186
  if (!configOnly) {
12018
13187
  const sessionCheck = checkAppSessionLock(isTty);
12019
13188
  if (!sessionCheck.ok) {
12020
13189
  if (sessionCheck.reason === "non_tty") {
12021
- console.error(pc14.red("relay-ai codex-app requires an interactive terminal."));
13190
+ console.error(pc15.red("relay-ai codex-app requires an interactive terminal."));
12022
13191
  return 1;
12023
13192
  }
12024
- console.error(pc14.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
13193
+ console.error(pc15.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
12025
13194
  console.error("Stop it with Ctrl+C in that terminal, or run relay-ai codex-app --restore after it exits.");
12026
13195
  return 1;
12027
13196
  }
@@ -12029,28 +13198,28 @@ async function runCodexAppCommand(args, opts = {}) {
12029
13198
  if (!configOnly) {
12030
13199
  codexAppIntro();
12031
13200
  if (interrupted.recovered) {
12032
- p15.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
13201
+ p17.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
12033
13202
  }
12034
13203
  }
12035
13204
  if (opts.vertex) {
12036
13205
  return runCodexAppVertexLaunch(configOnly, trace);
12037
13206
  }
12038
- const catalogSpinner = p15.spinner();
13207
+ const catalogSpinner = p17.spinner();
12039
13208
  catalogSpinner.start("Loading your providers...");
12040
13209
  let catalog;
12041
13210
  try {
12042
13211
  catalog = await fetchProviderCatalog({ agent: "codex-app" });
12043
13212
  } catch (err) {
12044
13213
  catalogSpinner.stop("");
12045
- console.error(pc14.red(String(err instanceof Error ? err.message : err)));
13214
+ console.error(pc15.red(String(err instanceof Error ? err.message : err)));
12046
13215
  return 1;
12047
13216
  }
12048
13217
  catalogSpinner.stop("");
12049
13218
  const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
12050
13219
  if (compatible.length === 0) {
12051
13220
  if (!configOnly) {
12052
- p15.log.warn("No Codex-compatible providers in your registry.");
12053
- p15.log.info("Add a provider with relay-ai providers add.");
13221
+ p17.log.warn("No Codex-compatible providers in your registry.");
13222
+ p17.log.info("Add a provider with relay-ai providers add.");
12054
13223
  }
12055
13224
  return 0;
12056
13225
  }
@@ -12058,10 +13227,10 @@ async function runCodexAppCommand(args, opts = {}) {
12058
13227
  const favorites = prefs.favoriteModels ?? [];
12059
13228
  const favoritesActive = favorites.length > 0;
12060
13229
  if (favoritesActive && !configOnly) {
12061
- p15.log.info(
13230
+ p17.log.info(
12062
13231
  `Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
12063
13232
  );
12064
- p15.log.info("Edit with `relay-ai models`.");
13233
+ p17.log.info("Edit with `relay-ai models`.");
12065
13234
  }
12066
13235
  let activeProvider = providerForCodexPicker(
12067
13236
  compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
@@ -12076,12 +13245,12 @@ async function runCodexAppCommand(args, opts = {}) {
12076
13245
  const favoriteProviders = compatible.map(providerForCodexPicker);
12077
13246
  const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
12078
13247
  if (!favoriteStart) {
12079
- p15.log.warn("No saved Codex App favorites are currently available.");
13248
+ p17.log.warn("No saved Codex App favorites are currently available.");
12080
13249
  return 0;
12081
13250
  }
12082
13251
  activeProvider = favoriteStart.provider;
12083
13252
  selectedModel = favoriteStart.model;
12084
- p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13253
+ p17.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12085
13254
  break;
12086
13255
  } else {
12087
13256
  activeProvider = providerForCodexPicker(pickedProvider);
@@ -12101,7 +13270,7 @@ async function runCodexAppCommand(args, opts = {}) {
12101
13270
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
12102
13271
  if (!apiKey) {
12103
13272
  if (!configOnly) {
12104
- p15.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
13273
+ p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
12105
13274
  }
12106
13275
  return 1;
12107
13276
  }
@@ -12161,36 +13330,36 @@ async function runCodexAppCommand(args, opts = {}) {
12161
13330
  const home = process.env["HOME"] ?? "";
12162
13331
  const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
12163
13332
  console.log("");
12164
- console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
13333
+ console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
12165
13334
  console.log("");
12166
13335
  if (favoritesActive) {
12167
- console.log(` ${pc14.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
13336
+ console.log(` ${pc15.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
12168
13337
  console.log("");
12169
- console.log(` ${pc14.bold("Models:")}`);
13338
+ console.log(` ${pc15.bold("Models:")}`);
12170
13339
  for (const r of resolvedFavorites) {
12171
- console.log(` ${pc14.cyan(r.model.id)} ${pc14.dim(`(${r.providerName})`)}`);
13340
+ console.log(` ${pc15.cyan(r.model.id)} ${pc15.dim(`(${r.providerName})`)}`);
12172
13341
  }
12173
13342
  } else {
12174
- console.log(` ${pc14.bold("Mode:")} Single model`);
12175
- console.log(` ${pc14.bold("Provider:")} ${activeProvider.name}`);
12176
- console.log(` ${pc14.bold("Model:")} ${formatCodexModelLabel(selectedModel)}`);
12177
- console.log(` ${pc14.bold("Catalog:")} ${routable.length} model${routable.length !== 1 ? "s" : ""} available`);
13343
+ console.log(` ${pc15.bold("Mode:")} Single model`);
13344
+ console.log(` ${pc15.bold("Provider:")} ${activeProvider.name}`);
13345
+ console.log(` ${pc15.bold("Model:")} ${formatCodexModelLabel(selectedModel)}`);
13346
+ console.log(` ${pc15.bold("Catalog:")} ${routable.length} model${routable.length !== 1 ? "s" : ""} available`);
12178
13347
  }
12179
13348
  console.log("");
12180
- console.log(` ${pc14.bold("config.toml patch preview:")}`);
13349
+ console.log(` ${pc15.bold("config.toml patch preview:")}`);
12181
13350
  const tomlPreview = previewAppConfigToml({
12182
13351
  ...specBase,
12183
13352
  proxyPort: PREVIEW_PROXY_PORT
12184
13353
  });
12185
13354
  for (const line of tomlPreview.split("\n")) {
12186
- console.log(` ${pc14.dim(line)}`);
13355
+ console.log(` ${pc15.dim(line)}`);
12187
13356
  }
12188
13357
  console.log("");
12189
- console.log(` ${pc14.bold("Catalog file:")}`);
12190
- console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
13358
+ console.log(` ${pc15.bold("Catalog file:")}`);
13359
+ console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
12191
13360
  console.log("");
12192
- console.log(pc14.dim(" No app was launched."));
12193
- console.log(pc14.dim(" Run ") + pc14.cyan("relay-ai codex-app") + pc14.dim(" to launch."));
13361
+ console.log(pc15.dim(" No app was launched."));
13362
+ console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app") + pc15.dim(" to launch."));
12194
13363
  console.log("");
12195
13364
  return 0;
12196
13365
  }
@@ -12234,8 +13403,8 @@ async function runCodexAppCommand(args, opts = {}) {
12234
13403
  try {
12235
13404
  await launchOrRestartCodexApp();
12236
13405
  } catch (err) {
12237
- p15.log.warn(String(err instanceof Error ? err.message : err));
12238
- p15.log.info(codexAppInstallHint());
13406
+ p17.log.warn(String(err instanceof Error ? err.message : err));
13407
+ p17.log.info(codexAppInstallHint());
12239
13408
  }
12240
13409
  printCodexAppSessionPanel({
12241
13410
  modelLabel,
@@ -12252,8 +13421,8 @@ async function runCodexAppCommand(args, opts = {}) {
12252
13421
  sessionActive = false;
12253
13422
  }
12254
13423
  if (isCodexAppRunning()) {
12255
- const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
12256
- if (shouldClose && !p15.isCancel(shouldClose)) {
13424
+ const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13425
+ if (shouldClose && !p17.isCancel(shouldClose)) {
12257
13426
  quitCodexAppGracefully();
12258
13427
  }
12259
13428
  }
@@ -12265,29 +13434,29 @@ async function runCodexAppCommand(args, opts = {}) {
12265
13434
  }
12266
13435
 
12267
13436
  // src/claude-app.ts
12268
- import pc15 from "picocolors";
12269
- import * as p17 from "@clack/prompts";
13437
+ import pc16 from "picocolors";
13438
+ import * as p19 from "@clack/prompts";
12270
13439
 
12271
13440
  // src/claude-desktop/app-config.ts
12272
- import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
12273
- import { homedir as homedir11 } from "os";
12274
- import { join as join17, dirname as dirname7 } from "path";
12275
- import { randomUUID as randomUUID2 } from "crypto";
13441
+ import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
13442
+ import { homedir as homedir12 } from "os";
13443
+ import { join as join18, dirname as dirname7 } from "path";
13444
+ import { randomUUID as randomUUID3 } from "crypto";
12276
13445
  function getClaudeDesktopHome() {
12277
13446
  if (process.platform === "win32") {
12278
- return join17(process.env.APPDATA || join17(homedir11(), "AppData", "Roaming"), "Claude-3p");
13447
+ return join18(process.env.APPDATA || join18(homedir12(), "AppData", "Roaming"), "Claude-3p");
12279
13448
  }
12280
- return join17(homedir11(), "Library", "Application Support", "Claude-3p");
13449
+ return join18(homedir12(), "Library", "Application Support", "Claude-3p");
12281
13450
  }
12282
13451
  function getConfigLibraryPath() {
12283
- return join17(getClaudeDesktopHome(), "configLibrary");
13452
+ return join18(getClaudeDesktopHome(), "configLibrary");
12284
13453
  }
12285
13454
  function getMetaJsonPath() {
12286
- return join17(getConfigLibraryPath(), "_meta.json");
13455
+ return join18(getConfigLibraryPath(), "_meta.json");
12287
13456
  }
12288
13457
  function readMetaJson() {
12289
13458
  const metaPath = getMetaJsonPath();
12290
- if (!existsSync16(metaPath)) return null;
13459
+ if (!existsSync17(metaPath)) return null;
12291
13460
  try {
12292
13461
  return JSON.parse(readFileSync13(metaPath, "utf8"));
12293
13462
  } catch {
@@ -12310,8 +13479,8 @@ function buildRelayAiConfig(proxyPort) {
12310
13479
  };
12311
13480
  }
12312
13481
  function writeRelayAiConfig(proxyPort) {
12313
- const uuid = randomUUID2();
12314
- const configPath = join17(getConfigLibraryPath(), `${uuid}.json`);
13482
+ const uuid = randomUUID3();
13483
+ const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
12315
13484
  const config = buildRelayAiConfig(proxyPort);
12316
13485
  mkdirSync9(dirname7(configPath), { recursive: true });
12317
13486
  writeFileSync7(configPath, `${JSON.stringify(config, null, 2)}
@@ -12326,14 +13495,14 @@ function writeRelayAiConfig(proxyPort) {
12326
13495
  }
12327
13496
 
12328
13497
  // src/claude-desktop/app-session.ts
12329
- import { existsSync as existsSync17, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync8, copyFileSync as copyFileSync5, unlinkSync as unlinkSync3 } from "fs";
12330
- import { join as join18 } from "path";
13498
+ import { existsSync as existsSync18, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync8, copyFileSync as copyFileSync5, unlinkSync as unlinkSync3 } from "fs";
13499
+ import { join as join19 } from "path";
12331
13500
  function getSessionLockPath2() {
12332
- return join18(getClaudeDesktopHome(), ".relay-ai.lock");
13501
+ return join19(getClaudeDesktopHome(), ".relay-ai.lock");
12333
13502
  }
12334
13503
  function readSessionLock2() {
12335
13504
  const path = getSessionLockPath2();
12336
- if (!existsSync17(path)) return null;
13505
+ if (!existsSync18(path)) return null;
12337
13506
  try {
12338
13507
  const parsed = JSON.parse(readFileSync14(path, "utf8"));
12339
13508
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
@@ -12358,21 +13527,21 @@ function isProcessAlive3(pid) {
12358
13527
  function backupMetaJson() {
12359
13528
  const metaPath = getMetaJsonPath();
12360
13529
  const backupPath = `${metaPath}.bak`;
12361
- if (existsSync17(metaPath)) {
13530
+ if (existsSync18(metaPath)) {
12362
13531
  copyFileSync5(metaPath, backupPath);
12363
13532
  }
12364
13533
  }
12365
13534
  function restoreMetaJson() {
12366
13535
  const metaPath = getMetaJsonPath();
12367
13536
  const backupPath = `${metaPath}.bak`;
12368
- if (existsSync17(backupPath)) {
13537
+ if (existsSync18(backupPath)) {
12369
13538
  copyFileSync5(backupPath, metaPath);
12370
13539
  unlinkSync3(backupPath);
12371
13540
  }
12372
13541
  }
12373
13542
  function removeRelayAiConfig(uuid) {
12374
- const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
12375
- if (existsSync17(configPath)) {
13543
+ const configPath = join19(getConfigLibraryPath(), `${uuid}.json`);
13544
+ if (existsSync18(configPath)) {
12376
13545
  try {
12377
13546
  rmSync4(configPath, { force: true });
12378
13547
  } catch {
@@ -12436,11 +13605,11 @@ function setupExitCleanup(uuid) {
12436
13605
  }
12437
13606
 
12438
13607
  // src/claude-desktop/app-launch.ts
12439
- import { execSync as execSync5 } from "child_process";
12440
- import { existsSync as existsSync18, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
12441
- import { homedir as homedir12 } from "os";
12442
- import { join as join19 } from "path";
12443
- import * as p16 from "@clack/prompts";
13608
+ import { execSync as execSync6 } from "child_process";
13609
+ import { existsSync as existsSync19, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
13610
+ import { homedir as homedir13 } from "os";
13611
+ import { join as join20 } from "path";
13612
+ import * as p18 from "@clack/prompts";
12444
13613
  var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
12445
13614
  function claudeAppSupported() {
12446
13615
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -12448,7 +13617,7 @@ function claudeAppSupported() {
12448
13617
  }
12449
13618
  }
12450
13619
  function run2(cmd, encoding = "utf8") {
12451
- return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
13620
+ return execSync6(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
12452
13621
  }
12453
13622
  function runPowerShell2(script) {
12454
13623
  return run2(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
@@ -12456,26 +13625,26 @@ function runPowerShell2(script) {
12456
13625
  function darwinAppCandidates2() {
12457
13626
  return [
12458
13627
  "/Applications/Claude.app",
12459
- join19(homedir12(), "Applications", "Claude.app")
13628
+ join20(homedir13(), "Applications", "Claude.app")
12460
13629
  ];
12461
13630
  }
12462
13631
  function winLocalAppData2() {
12463
- return process.env.LOCALAPPDATA ?? join19(homedir12(), "AppData", "Local");
13632
+ return process.env.LOCALAPPDATA ?? join20(homedir13(), "AppData", "Local");
12464
13633
  }
12465
13634
  function winClaudeExeCandidates() {
12466
13635
  const local = winLocalAppData2();
12467
13636
  const bases = [
12468
- join19(local, "Programs", "Claude"),
12469
- join19(local, "Claude")
13637
+ join20(local, "Programs", "Claude"),
13638
+ join20(local, "Claude")
12470
13639
  ];
12471
13640
  const out = [];
12472
13641
  for (const base of bases) {
12473
- out.push(join19(base, "Claude.exe"));
13642
+ out.push(join20(base, "Claude.exe"));
12474
13643
  try {
12475
- if (existsSync18(base)) {
13644
+ if (existsSync19(base)) {
12476
13645
  for (const name of readdirSync4(base)) {
12477
13646
  if (name.startsWith("app-")) {
12478
- out.push(join19(base, name, "Claude.exe"));
13647
+ out.push(join20(base, name, "Claude.exe"));
12479
13648
  }
12480
13649
  }
12481
13650
  }
@@ -12488,7 +13657,7 @@ function mdfindClaudeApp() {
12488
13657
  try {
12489
13658
  const out = run2(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
12490
13659
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
12491
- return first && existsSync18(first) ? first : null;
13660
+ return first && existsSync19(first) ? first : null;
12492
13661
  } catch {
12493
13662
  return null;
12494
13663
  }
@@ -12496,14 +13665,14 @@ function mdfindClaudeApp() {
12496
13665
  function findClaudeApp() {
12497
13666
  if (process.platform === "darwin") {
12498
13667
  for (const path of darwinAppCandidates2()) {
12499
- if (existsSync18(path)) return path;
13668
+ if (existsSync19(path)) return path;
12500
13669
  }
12501
13670
  return mdfindClaudeApp();
12502
13671
  }
12503
13672
  if (process.platform === "win32") {
12504
13673
  for (const path of winClaudeExeCandidates()) {
12505
13674
  try {
12506
- if (existsSync18(path) && statSync5(path).isFile()) return path;
13675
+ if (existsSync19(path) && statSync5(path).isFile()) return path;
12507
13676
  } catch {
12508
13677
  }
12509
13678
  }
@@ -12567,9 +13736,9 @@ async function waitForQuit2(timeoutMs) {
12567
13736
  function openClaudeAppAt(path) {
12568
13737
  if (process.platform === "darwin") {
12569
13738
  if (path.endsWith(".app")) {
12570
- execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
13739
+ execSync6(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
12571
13740
  } else {
12572
- execSync5(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
13741
+ execSync6(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
12573
13742
  }
12574
13743
  return;
12575
13744
  }
@@ -12592,9 +13761,9 @@ function openClaudeApp() {
12592
13761
  }
12593
13762
  function darwinQuit2() {
12594
13763
  try {
12595
- execSync5(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
13764
+ execSync6(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
12596
13765
  } catch {
12597
- execSync5(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
13766
+ execSync6(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
12598
13767
  }
12599
13768
  }
12600
13769
  function winQuitGraceful2() {
@@ -12620,9 +13789,9 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
12620
13789
  openClaudeAppAt(appPath);
12621
13790
  return;
12622
13791
  }
12623
- const restart = await p16.confirm({ message: prompt, initialValue: true });
12624
- if (p16.isCancel(restart) || !restart) {
12625
- p16.log.info("Quit and reopen Claude Desktop when you are ready for the new model to take effect.");
13792
+ const restart = await p18.confirm({ message: prompt, initialValue: true });
13793
+ if (p18.isCancel(restart) || !restart) {
13794
+ p18.log.info("Quit and reopen Claude Desktop when you are ready for the new model to take effect.");
12626
13795
  return;
12627
13796
  }
12628
13797
  if (process.platform === "darwin") darwinQuit2();
@@ -12637,30 +13806,30 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
12637
13806
 
12638
13807
  // src/claude-app.ts
12639
13808
  function claudeAppHelpText() {
12640
- return `${pc15.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
13809
+ return `${pc16.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
12641
13810
 
12642
- ${pc15.bold("Usage:")}
13811
+ ${pc16.bold("Usage:")}
12643
13812
  relay-ai claude-app [options]
12644
13813
  relay-ai claude-app --trace
12645
13814
  relay-ai claude-app --restore
12646
13815
  relay-ai claude-app --help
12647
13816
  relay-ai claude-app --version
12648
13817
 
12649
- ${pc15.bold("Options:")}
13818
+ ${pc16.bold("Options:")}
12650
13819
  --trace Write proxy debug logs to ~/.relay-ai/logs/
12651
13820
  --restore Restore Claude Desktop config after an interrupted app session
12652
13821
  --help Show this command help
12653
13822
  --version Show version
12654
13823
 
12655
- ${pc15.bold("Description:")}
13824
+ ${pc16.bold("Description:")}
12656
13825
  Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
12657
13826
  (with backup + restore on Ctrl+C), starts a local Responses proxy, and opens
12658
13827
  the Claude Desktop app. Keep this terminal open while using Claude.
12659
13828
 
12660
- ${pc15.bold("Platforms:")}
13829
+ ${pc16.bold("Platforms:")}
12661
13830
  macOS and Windows. Linux is not supported.
12662
13831
 
12663
- ${pc15.bold("Cleanup:")}
13832
+ ${pc16.bold("Cleanup:")}
12664
13833
  Ctrl+C stops the proxy and restores your previous Claude config.
12665
13834
  After a crash: relay-ai claude-app --restore
12666
13835
  `;
@@ -12684,37 +13853,37 @@ async function runClaudeAppCommand(args) {
12684
13853
  try {
12685
13854
  claudeAppSupported();
12686
13855
  } catch (err) {
12687
- console.error(pc15.red(String(err instanceof Error ? err.message : err)));
13856
+ console.error(pc16.red(String(err instanceof Error ? err.message : err)));
12688
13857
  return 1;
12689
13858
  }
12690
13859
  const isTty = Boolean(process.stdin.isTTY);
12691
13860
  if (!isTty) {
12692
- console.error(pc15.red("relay-ai claude-app requires an interactive terminal."));
13861
+ console.error(pc16.red("relay-ai claude-app requires an interactive terminal."));
12693
13862
  return 1;
12694
13863
  }
12695
13864
  if (isConcurrentLiveSession()) {
12696
- console.error(pc15.yellow(`Another relay-ai claude-app session may be running.`));
13865
+ console.error(pc16.yellow(`Another relay-ai claude-app session may be running.`));
12697
13866
  console.error("Stop it with Ctrl+C in that terminal.");
12698
13867
  return 1;
12699
13868
  }
12700
13869
  if (hasStaleSession()) {
12701
- p17.log.warn("Recovered from an interrupted claude-app session.");
13870
+ p19.log.warn("Recovered from an interrupted claude-app session.");
12702
13871
  recoverSession();
12703
13872
  }
12704
- const catalogSpinner = p17.spinner();
13873
+ const catalogSpinner = p19.spinner();
12705
13874
  catalogSpinner.start("Loading your providers...");
12706
13875
  let catalog;
12707
13876
  try {
12708
13877
  catalog = await fetchProviderCatalog({ agent: "codex-app" });
12709
13878
  } catch (err) {
12710
13879
  catalogSpinner.stop("");
12711
- console.error(pc15.red(String(err instanceof Error ? err.message : err)));
13880
+ console.error(pc16.red(String(err instanceof Error ? err.message : err)));
12712
13881
  return 1;
12713
13882
  }
12714
13883
  catalogSpinner.stop("");
12715
13884
  const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
12716
13885
  if (compatible.length === 0) {
12717
- p17.log.warn("No compatible providers in your registry.");
13886
+ p19.log.warn("No compatible providers in your registry.");
12718
13887
  return 0;
12719
13888
  }
12720
13889
  const prefs = loadPreferences();
@@ -12736,7 +13905,7 @@ async function runClaudeAppCommand(args) {
12736
13905
  const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
12737
13906
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
12738
13907
  if (!apiKey) {
12739
- p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
13908
+ p19.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
12740
13909
  return 1;
12741
13910
  }
12742
13911
  activeProvider.apiKey = apiKey;
@@ -12799,28 +13968,28 @@ async function runClaudeAppCommand(args) {
12799
13968
  });
12800
13969
  }
12801
13970
  console.log(`
12802
- ${pc15.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
13971
+ ${pc16.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
12803
13972
  try {
12804
13973
  await launchOrRestartClaudeApp();
12805
13974
  } catch (err) {
12806
- p17.log.warn(String(err instanceof Error ? err.message : err));
13975
+ p19.log.warn(String(err instanceof Error ? err.message : err));
12807
13976
  }
12808
13977
  console.log(`
12809
- ${pc15.bold("Claude Desktop 3P Mode Active")}`);
13978
+ ${pc16.bold("Claude Desktop 3P Mode Active")}`);
12810
13979
  if (useFavorites) {
12811
- console.log(`${pc15.dim("Catalog:")} Favorite models only`);
13980
+ console.log(`${pc16.dim("Catalog:")} Favorite models only`);
12812
13981
  } else {
12813
- console.log(`${pc15.dim("Model:")} ${selectedModel.id}`);
12814
- console.log(`${pc15.dim("Provider:")} ${activeProvider.name}`);
13982
+ console.log(`${pc16.dim("Model:")} ${selectedModel.id}`);
13983
+ console.log(`${pc16.dim("Provider:")} ${activeProvider.name}`);
12815
13984
  }
12816
- console.log(`${pc15.cyan("Press Ctrl+C to stop and restore config.")}`);
13985
+ console.log(`${pc16.cyan("Press Ctrl+C to stop and restore config.")}`);
12817
13986
  await waitForShutdown3();
12818
13987
  console.log("");
12819
13988
  cleanupSession(uuid);
12820
13989
  sessionActive = false;
12821
13990
  if (isClaudeAppRunning()) {
12822
- const shouldClose = await p17.confirm({ message: "Claude Desktop is still running. Close it?" });
12823
- if (shouldClose && !p17.isCancel(shouldClose)) {
13991
+ const shouldClose = await p19.confirm({ message: "Claude Desktop is still running. Close it?" });
13992
+ if (shouldClose && !p19.isCancel(shouldClose)) {
12824
13993
  quitClaudeAppGracefully();
12825
13994
  }
12826
13995
  }
@@ -12835,17 +14004,17 @@ ${pc15.bold("Claude Desktop 3P Mode Active")}`);
12835
14004
  }
12836
14005
 
12837
14006
  // src/ai-doc.ts
12838
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
12839
- import { homedir as homedir13 } from "os";
12840
- import { join as join20 } from "path";
14007
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
14008
+ import { homedir as homedir14 } from "os";
14009
+ import { join as join21 } from "path";
12841
14010
  var SKILL_DIR_NAME = "relay-ai-cli";
12842
14011
  var SKILL_INSTALL_DIRS = [
12843
- join20(getAppHome(), "skills"),
12844
- join20(homedir13(), ".claude", "skills"),
12845
- join20(homedir13(), ".agents", "skills"),
12846
- join20(homedir13(), ".codex", "skills"),
12847
- join20(homedir13(), ".cursor", "skills"),
12848
- join20(homedir13(), ".cursor", "skills-cursor")
14012
+ join21(getAppHome(), "skills"),
14013
+ join21(homedir14(), ".claude", "skills"),
14014
+ join21(homedir14(), ".agents", "skills"),
14015
+ join21(homedir14(), ".codex", "skills"),
14016
+ join21(homedir14(), ".cursor", "skills"),
14017
+ join21(homedir14(), ".cursor", "skills-cursor")
12849
14018
  ];
12850
14019
  function parseSkillVersion(content) {
12851
14020
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -12859,8 +14028,8 @@ function parseSkillVersion(content) {
12859
14028
  return null;
12860
14029
  }
12861
14030
  function readInstalledSkillVersion(skillDir) {
12862
- const skillPath = join20(skillDir, "SKILL.md");
12863
- if (!existsSync19(skillPath)) return null;
14031
+ const skillPath = join21(skillDir, "SKILL.md");
14032
+ if (!existsSync20(skillPath)) return null;
12864
14033
  try {
12865
14034
  const head = readFileSync15(skillPath, "utf-8").slice(0, 1024);
12866
14035
  return parseSkillVersion(head.includes("---", 4) ? head : `${head}
@@ -12872,8 +14041,8 @@ function readInstalledSkillVersion(skillDir) {
12872
14041
  }
12873
14042
  function skillInstallTargets() {
12874
14043
  return SKILL_INSTALL_DIRS.map((dir) => {
12875
- const skillDir = join20(dir, SKILL_DIR_NAME);
12876
- return { skillDir, skillPath: join20(skillDir, "SKILL.md") };
14044
+ const skillDir = join21(dir, SKILL_DIR_NAME);
14045
+ return { skillDir, skillPath: join21(skillDir, "SKILL.md") };
12877
14046
  });
12878
14047
  }
12879
14048
  function formatProviderModels(provider) {
@@ -12886,7 +14055,7 @@ function formatProviderModels(provider) {
12886
14055
  function buildLiveStateSection() {
12887
14056
  const prefs = loadPreferences();
12888
14057
  const registry = loadRegistry();
12889
- const enabled = registry.providers.filter((p19) => p19.enabled);
14058
+ const enabled = registry.providers.filter((p21) => p21.enabled);
12890
14059
  const prefLines = [];
12891
14060
  if (prefs.lastProvider || prefs.lastModel) {
12892
14061
  prefLines.push(` Claude last launch: provider=${prefs.lastProvider ?? "(none)"} model=${prefs.lastModel ?? "(none)"}`);
@@ -12894,15 +14063,18 @@ function buildLiveStateSection() {
12894
14063
  if (prefs.lastCodexProvider || prefs.lastCodexModel) {
12895
14064
  prefLines.push(` Codex last launch: provider=${prefs.lastCodexProvider ?? "(none)"} model=${prefs.lastCodexModel ?? "(none)"}`);
12896
14065
  }
14066
+ if (prefs.lastGeminiProvider || prefs.lastGeminiModel) {
14067
+ prefLines.push(` Gemini last launch: provider=${prefs.lastGeminiProvider ?? "(none)"} model=${prefs.lastGeminiModel ?? "(none)"}`);
14068
+ }
12897
14069
  if (prefs.favoriteModels?.length) {
12898
14070
  prefLines.push(` Favorites (${prefs.favoriteModels.length}/${MAX_MODEL_CATALOG}):`);
12899
14071
  for (const f of prefs.favoriteModels) {
12900
14072
  prefLines.push(` ${f.providerId} / ${f.modelId}`);
12901
14073
  }
12902
14074
  }
12903
- const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((p19) => [
12904
- ` ${p19.name} (${p19.id}) \u2014 ${p19.modelsCache?.models.length ?? 0} cached model(s)`,
12905
- formatProviderModels(p19)
14075
+ const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((p21) => [
14076
+ ` ${p21.name} (${p21.id}) \u2014 ${p21.modelsCache?.models.length ?? 0} cached model(s)`,
14077
+ formatProviderModels(p21)
12906
14078
  ].join("\n"));
12907
14079
  return `
12908
14080
  ================================================================================
@@ -12938,8 +14110,8 @@ function staticAiDocBody() {
12938
14110
  RELAY-AI \u2014 AI AGENT REFERENCE (v${VERSION})
12939
14111
  ================================================================================
12940
14112
 
12941
- relay-ai launches Claude Code, OpenAI Codex, and desktop apps against YOUR
12942
- provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
14113
+ relay-ai launches Claude Code, OpenAI Codex, Google Gemini CLI, and desktop apps
14114
+ against YOUR provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
12943
14115
  It handles API translation, local proxies, env isolation, and model routing.
12944
14116
 
12945
14117
  SKILL VERSIONING
@@ -12991,13 +14163,14 @@ PREVIEW LAUNCH WITHOUT STARTING A SESSION:
12991
14163
  INTERACTIVE BROWSE (requires TTY \u2014 avoid in agent scripts):
12992
14164
  relay-ai claude provider + model wizard
12993
14165
  relay-ai codex provider + model wizard
14166
+ relay-ai gemini provider + model wizard
12994
14167
  relay-ai providers provider management hub
12995
14168
 
12996
14169
  ================================================================================
12997
14170
  AGENT PLATFORM PATTERNS \u2014 MULTI-MODEL / ONE-SHOT QUERIES
12998
14171
  ================================================================================
12999
14172
 
13000
- relay-ai is designed so agents can use Claude Code or Codex as a PLATFORM:
14173
+ relay-ai is designed so agents can use Claude Code, Codex, or Gemini CLI as a PLATFORM:
13001
14174
  run many models sequentially or in parallel shell jobs, each with a focused
13002
14175
  prompt, without interactive wizards.
13003
14176
 
@@ -13045,6 +14218,24 @@ OPENAI CODEX \u2014 NON-INTERACTIVE (exec / positional prompt)
13045
14218
  --provider <id>
13046
14219
  --model <id> or provider__model-id slug
13047
14220
 
14221
+ GOOGLE GEMINI CLI \u2014 NON-INTERACTIVE (-p / --prompt)
14222
+ Skips the provider/model wizard when:
14223
+ \u2022 Both --provider and --model are set, OR
14224
+ \u2022 Non-interactive args (-p / --prompt, -i / --prompt-interactive, or positional query)
14225
+ and saved preferences exist
14226
+
14227
+ Examples:
14228
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "Review this file"
14229
+ relay-ai gemini -p "What is the capital of France?"
14230
+
14231
+ Machine-readable stdout:
14232
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o json
14233
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o stream-json
14234
+
14235
+ Boot flags (relay-ai \u2014 NOT passed to Gemini):
14236
+ --provider <id>
14237
+ --model <id> or provider__model-id slug
14238
+
13048
14239
  MULTI-MODEL LOOP (shell pattern):
13049
14240
  for model in llama-3.3-70b-versatile mixtral-8x7b-32768; do
13050
14241
  relay-ai claude --provider groq --model "$model" -p "Same prompt for all models"
@@ -13054,10 +14245,14 @@ MULTI-MODEL LOOP (shell pattern):
13054
14245
  relay-ai codex --provider zen --model "$model" exec "Same task"
13055
14246
  done
13056
14247
 
14248
+ for model in gemini-2.5-flash gemini-2.5-pro; do
14249
+ relay-ai gemini --provider google --model "$model" -p "Same task"
14250
+ done
14251
+
13057
14252
  FAVORITES / MID-SESSION SWITCHING:
13058
14253
  relay-ai models interactive favorites manager (max ${MAX_MODEL_CATALOG})
13059
- When favorites exist, interactive claude/codex launches expose /model switching.
13060
- Boot flags (--provider + --model) or print/exec mode use SINGLE-MODEL launch
14254
+ When favorites exist, interactive claude/codex/gemini launches expose /model switching.
14255
+ Boot flags (--provider + --model) or print/exec/-p mode use SINGLE-MODEL launch
13061
14256
  (favorites catalog is skipped \u2014 better for agent one-shots).
13062
14257
 
13063
14258
  ================================================================================
@@ -13092,6 +14287,18 @@ CLAUDE CODE
13092
14287
  relay-ai claude --provider anthropic --model claude-sonnet-4-6 -p "review file.ts"
13093
14288
  relay-ai claude --dry-run --provider groq --model llama-3.3-70b-versatile
13094
14289
 
14290
+ GOOGLE GEMINI CLI
14291
+ relay-ai gemini [relay-options] [gemini-flags]
14292
+
14293
+ Relay options:
14294
+ --provider <id> Boot provider (skip wizard with --model)
14295
+ --model <id> Boot model id or provider__model slug
14296
+ --trace Debug logs in ~/.relay-ai/logs/
14297
+
14298
+ Examples:
14299
+ relay-ai gemini
14300
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "What is the capital of France?"
14301
+
13095
14302
  OPENAI CODEX CLI
13096
14303
  relay-ai codex [relay-options] [codex-flags]
13097
14304
 
@@ -13125,7 +14332,7 @@ PROVIDERS REGISTRY
13125
14332
 
13126
14333
  MODELS / FAVORITES
13127
14334
  relay-ai models manage favoriteModels in config (alias: favorites)
13128
- Used for mid-session /model switching in interactive Claude/Codex sessions.
14335
+ Used for mid-session /model switching in interactive Claude/Codex/Gemini sessions.
13129
14336
 
13130
14337
  API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
13131
14338
  relay-ai server foreground gateway on port 17645
@@ -13161,10 +14368,10 @@ DO:
13161
14368
 
13162
14369
  DO NOT:
13163
14370
  \u2022 Rely on interactive wizards in CI, scripts, or headless agent loops
13164
- \u2022 Pass --provider / --model to Claude or Codex directly \u2014 relay-ai consumes them
14371
+ \u2022 Pass --provider / --model to Claude, Codex, or Gemini directly \u2014 relay-ai consumes them
13165
14372
  \u2022 Use Codex -p expecting print mode (it means --profile in Codex)
13166
- \u2022 Assume favorites catalog in print/exec mode \u2014 use explicit boot flags
13167
- \u2022 Mutate ~/.claude/settings.json or ~/.codex/config.toml \u2014 relay-ai uses env +
14373
+ \u2022 Assume favorites catalog in print/exec/-p mode \u2014 use explicit boot flags
14374
+ \u2022 Mutate settings files (e.g. ~/.claude/settings.json, ~/.codex/config.toml, ~/.gemini/config/config.json) \u2014 relay-ai uses env +
13168
14375
  temporary overlay profiles only
13169
14376
 
13170
14377
  NON-TTY:
@@ -13178,6 +14385,7 @@ TROUBLESHOOTING
13178
14385
  relay-ai codex --restore clean stale overlay after crash
13179
14386
  relay-ai claude --trace proxy + Claude debug logs
13180
14387
  relay-ai codex --trace proxy debug log on exit
14388
+ relay-ai gemini --trace proxy debug log on exit
13181
14389
  relay-ai providers list verify provider ids
13182
14390
  relay-ai providers refresh-models repopulate model cache
13183
14391
 
@@ -13196,7 +14404,7 @@ Human-readable guide: docs/AI-AGENTS.md in the relay-ai repo.
13196
14404
  ALEF AGENT INTEGRATION
13197
14405
  ================================================================================
13198
14406
 
13199
- alef-agent shells out to relay-ai to run Claude Code or Codex against any
14407
+ alef-agent shells out to relay-ai to run Claude Code, Codex, or Gemini CLI against any
13200
14408
  provider in ~/.relay-ai/providers.json. relay-ai is a launcher + proxy; the
13201
14409
  child CLI owns NDJSON/JSONL on stdout.
13202
14410
 
@@ -13253,7 +14461,8 @@ ALEF CHECKLIST
13253
14461
  \u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
13254
14462
  \u25A1 Claude: --output-format stream-json (or json) with -p
13255
14463
  \u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
13256
- \u25A1 Parse stdout only; ignore stderr for JSONL stream
14464
+ \u25A1 Gemini: -o json (or stream-json) with -p
14465
+ \u25A1 Parse stdout only; ignore stderr for JSONL/NDJSON stream
13257
14466
  \u25A1 Zen/Go: --provider zen explicitly + OPENCODE_API_KEY available
13258
14467
  \u25A1 Codex network: default danger-full-access \u2014 no extra -s needed for nlm/curl/npm
13259
14468
  \u25A1 MCP (Claude): --allowed-tools mcp__server__tool on claude args after relay-ai flags
@@ -13442,10 +14651,12 @@ function parseArgs(args) {
13442
14651
  }
13443
14652
  if (first === "providers") {
13444
14653
  const parsed2 = emptyParsed("providers");
13445
- parsed2.claudeArgs = rest;
14654
+ parsed2.claudeArgs = [];
13446
14655
  for (const arg of rest) {
13447
- if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
14656
+ if (arg === "--trace") parsed2.trace = true;
14657
+ else if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
13448
14658
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
14659
+ else parsed2.claudeArgs.push(arg);
13449
14660
  }
13450
14661
  return parsed2;
13451
14662
  }
@@ -13498,6 +14709,32 @@ function parseArgs(args) {
13498
14709
  }
13499
14710
  return parsed2;
13500
14711
  }
14712
+ if (first === "gemini") {
14713
+ const parsed2 = emptyParsed("gemini");
14714
+ for (let i = 0; i < rest.length; i += 1) {
14715
+ const arg = rest[i];
14716
+ if (arg === "--trace") {
14717
+ parsed2.trace = true;
14718
+ continue;
14719
+ }
14720
+ if (arg === "--help" || arg === "-h") {
14721
+ parsed2.showHelp = true;
14722
+ continue;
14723
+ }
14724
+ if (arg === "--version" || arg === "-v") {
14725
+ parsed2.showVersion = true;
14726
+ continue;
14727
+ }
14728
+ const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
14729
+ if (consumed !== null) {
14730
+ if ("error" in consumed) return parsed2;
14731
+ i = consumed.next;
14732
+ continue;
14733
+ }
14734
+ parsed2.claudeArgs.push(arg);
14735
+ }
14736
+ return parsed2;
14737
+ }
13501
14738
  if (first !== "claude") {
13502
14739
  return {
13503
14740
  ...emptyParsed("root"),
@@ -13530,15 +14767,16 @@ function parseArgs(args) {
13530
14767
  return parsed;
13531
14768
  }
13532
14769
  function rootHelpText() {
13533
- return `${pc16.bold("relay-ai")} v${VERSION}
14770
+ return `${pc17.bold("relay-ai")} v${VERSION}
13534
14771
  Launch AI coding tools with OpenCode Zen / Go or local providers (Groq, Mistral,
13535
14772
  OpenAI, Gemini, Ollama, and more).
13536
14773
 
13537
- ${pc16.bold("Usage:")}
14774
+ ${pc17.bold("Usage:")}
13538
14775
  relay-ai claude [options] [claude-flags]
13539
14776
  relay-ai claude-app [options]
13540
14777
  relay-ai codex [options] [codex-flags]
13541
14778
  relay-ai codex-app [options]
14779
+ relay-ai gemini [options] [gemini-flags]
13542
14780
  relay-ai server [options]
13543
14781
  relay-ai models
13544
14782
  relay-ai favorites
@@ -13549,32 +14787,34 @@ ${pc16.bold("Usage:")}
13549
14787
  relay-ai --ai --install Install or upgrade agent skill when version changed
13550
14788
  relay-ai --ai --install --force Reinstall skill even if already current
13551
14789
 
13552
- ${pc16.bold("Root options:")}
14790
+ ${pc17.bold("Root options:")}
13553
14791
  -h, --help Show this help
13554
14792
  -v, --version Show version
13555
14793
  --ai Print the full reference for AI agents
13556
14794
  --ai --install Install or upgrade the relay-ai agent skill
13557
14795
  --force Reinstall the agent skill when used with --ai --install
13558
14796
 
13559
- ${pc16.bold("Commands:")}
14797
+ ${pc17.bold("Commands:")}
13560
14798
  claude Launch Claude Code \u2014 pick a provider from your registry
13561
14799
  models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
13562
14800
  favorites Alias for models
13563
14801
  providers Add, import, and manage your AI providers
13564
14802
  server Run a foreground API gateway (OpenCode Zen / Go and local providers)
13565
14803
  codex Launch OpenAI Codex CLI with registry providers
14804
+ gemini Launch Google Gemini CLI with registry providers
13566
14805
  codex-app Launch Codex desktop app with registry providers (macOS + Windows)
13567
14806
  claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
13568
14807
 
13569
- ${pc16.bold("Migration:")}
14808
+ ${pc17.bold("Migration:")}
13570
14809
  Bare relay-ai prints this help instead of launching Claude Code.
13571
14810
  Use relay-ai claude for the wizard and launcher.
13572
14811
 
13573
- ${pc16.bold("Examples:")}
14812
+ ${pc17.bold("Examples:")}
13574
14813
  relay-ai claude
13575
14814
  relay-ai models
13576
14815
  relay-ai providers
13577
14816
  relay-ai codex
14817
+ relay-ai gemini
13578
14818
  relay-ai codex-app
13579
14819
  relay-ai claude-app
13580
14820
  relay-ai server
@@ -13583,15 +14823,15 @@ ${pc16.bold("Examples:")}
13583
14823
  relay-ai claude -- --print "hello"`;
13584
14824
  }
13585
14825
  function claudeHelpText() {
13586
- return `${pc16.bold("relay-ai claude")} v${VERSION}
14826
+ return `${pc17.bold("relay-ai claude")} v${VERSION}
13587
14827
  Launch Claude Code with OpenCode Zen, Go, or local providers as the API backend.
13588
14828
 
13589
- ${pc16.bold("Usage:")}
14829
+ ${pc17.bold("Usage:")}
13590
14830
  relay-ai claude [options] [claude-flags]
13591
14831
  relay-ai claude --help
13592
14832
  relay-ai claude --version
13593
14833
 
13594
- ${pc16.bold("Options:")}
14834
+ ${pc17.bold("Options:")}
13595
14835
  --dry-run Run the wizard but show a preview instead of launching Claude Code
13596
14836
  --setup Hint: use relay-ai providers to add or manage providers
13597
14837
  --trace Write debug logs to ~/.relay-ai/logs/ and show errors on exit
@@ -13600,22 +14840,22 @@ ${pc16.bold("Options:")}
13600
14840
  --help Show this command help
13601
14841
  --version Show version
13602
14842
 
13603
- ${pc16.bold("Providers:")}
14843
+ ${pc17.bold("Providers:")}
13604
14844
  Cloud (Zen/Go) Requires OPENCODE_API_KEY \u2014 get one at https://opencode.ai/auth
13605
14845
  Registry Configure with relay-ai providers add or import (Groq, Mistral,
13606
14846
  Nvidia, DeepSeek, OpenAI, custom endpoints, etc.).
13607
14847
 
13608
- ${pc16.bold("Model switching:")}
14848
+ ${pc17.bold("Model switching:")}
13609
14849
  Run relay-ai models to save favorites (max ${MAX_MODEL_CATALOG}).
13610
14850
  When favorites exist, launch starts a multi-route proxy and Claude Code /model
13611
14851
  lists your starting model plus favorites for live switching.
13612
14852
  With no favorites, launch uses a single model as before.
13613
14853
 
13614
- ${pc16.bold("Note:")}
14854
+ ${pc17.bold("Note:")}
13615
14855
  Claude Code may save the launched model to ~/.claude/settings.json.
13616
14856
  Bare claude later can still show that model \u2014 reset with claude --model sonnet.
13617
14857
 
13618
- ${pc16.bold("Examples:")}
14858
+ ${pc17.bold("Examples:")}
13619
14859
  relay-ai claude
13620
14860
  relay-ai claude -c
13621
14861
  relay-ai claude --resume abc-123
@@ -13629,54 +14869,54 @@ ${pc16.bold("Examples:")}
13629
14869
  relay-ai claude -- --dangerously-skip-permissions`;
13630
14870
  }
13631
14871
  function serverHelpText() {
13632
- return `${pc16.bold("relay-ai server")} v${VERSION}
14872
+ return `${pc17.bold("relay-ai server")} v${VERSION}
13633
14873
  Run a foreground API gateway for registry providers, Zen/Go, or Vertex AI.
13634
14874
 
13635
- ${pc16.bold("Usage:")}
14875
+ ${pc17.bold("Usage:")}
13636
14876
  relay-ai server
13637
14877
  relay-ai server --vertex
13638
14878
  relay-ai server --help
13639
14879
  relay-ai server --version
13640
14880
 
13641
- ${pc16.bold("Behavior:")}
14881
+ ${pc17.bold("Behavior:")}
13642
14882
  Default: interactive wizard for exposed providers, discovery id masking (for
13643
14883
  Claude Desktop / Cowork), optional favorites-only catalog, then listen mode.
13644
14884
  --vertex: Anthropic-compatible gateway to Claude on Google Vertex AI using
13645
14885
  local gcloud Application Default Credentials (no OpenCode API key).
13646
14886
  Binds to port 17645. Network mode asks for a server password.
13647
14887
 
13648
- ${pc16.bold("Vertex env:")}
14888
+ ${pc17.bold("Vertex env:")}
13649
14889
  ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
13650
14890
  GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
13651
14891
  Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
13652
14892
 
13653
- ${pc16.bold("Endpoints:")}
14893
+ ${pc17.bold("Endpoints:")}
13654
14894
  Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
13655
14895
  OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
13656
14896
  API key: use anything locally; use the server password in network mode.`;
13657
14897
  }
13658
14898
  function modelsHelpText() {
13659
- return `${pc16.bold("relay-ai models")} v${VERSION}
14899
+ return `${pc17.bold("relay-ai models")} v${VERSION}
13660
14900
  Manage favorite models for mid-session switching in Claude Code.
13661
14901
 
13662
- ${pc16.bold("Usage:")}
14902
+ ${pc17.bold("Usage:")}
13663
14903
  relay-ai models
13664
14904
  relay-ai models --help
13665
14905
  relay-ai models --version
13666
14906
 
13667
- ${pc16.bold("Behavior:")}
14907
+ ${pc17.bold("Behavior:")}
13668
14908
  Opens an interactive manager to add or remove favorites.
13669
14909
  Search all providers at once (paginated results) or browse one provider at a time.
13670
14910
  Pick from Zen, Go, or any provider in your registry.
13671
14911
  Favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
13672
14912
 
13673
- ${pc16.bold("How it works:")}
14913
+ ${pc17.bold("How it works:")}
13674
14914
  When favorites exist, relay-ai claude starts a multi-route catalog proxy.
13675
14915
  Claude Code /model lists your starting model plus favorites \u2014 switch live
13676
14916
  without restarting. Mix cloud and local favorites in one session.
13677
14917
  With no favorites, launch uses a single model as before.
13678
14918
 
13679
- ${pc16.bold("Examples:")}
14919
+ ${pc17.bold("Examples:")}
13680
14920
  relay-ai models
13681
14921
  relay-ai claude # switch menu active when favorites are set`;
13682
14922
  }
@@ -13689,11 +14929,11 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13689
14929
  let proxyHandle;
13690
14930
  try {
13691
14931
  proxyHandle = await startProxyCatalog(catalogRoutes, startingRoute.aliasId, trace);
13692
- p18.log.info(
13693
- `Switch menu active \u2014 proxy on port ${proxyHandle.port} ` + pc16.dim(`(${catalogRoutes.length} model${catalogRoutes.length !== 1 ? "s" : ""} in /model)`)
14932
+ p20.log.info(
14933
+ `Switch menu active \u2014 proxy on port ${proxyHandle.port} ` + pc17.dim(`(${catalogRoutes.length} model${catalogRoutes.length !== 1 ? "s" : ""} in /model)`)
13694
14934
  );
13695
14935
  } catch (err) {
13696
- p18.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
14936
+ p20.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
13697
14937
  return 1;
13698
14938
  }
13699
14939
  const childEnv = buildChildEnv(
@@ -13706,7 +14946,7 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13706
14946
  );
13707
14947
  const debugLogPath = prepareClaudeTraceLog();
13708
14948
  const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
13709
- if (trace) p18.log.info(`Debug log: ${debugLogPath}`);
14949
+ if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
13710
14950
  const exitCode = await launchClaude(
13711
14951
  childEnv,
13712
14952
  claudeCodeClientModelId(startingRoute.aliasId, contextWindow),
@@ -13718,14 +14958,14 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13718
14958
  }
13719
14959
  async function runModelsCommand() {
13720
14960
  relayIntro("Favorite Models");
13721
- const spinner9 = p18.spinner();
13722
- spinner9.start("Loading providers...");
14961
+ const spinner10 = p20.spinner();
14962
+ spinner10.start("Loading providers...");
13723
14963
  const catalog = await fetchProviderCatalog();
13724
- spinner9.stop("");
14964
+ spinner10.stop("");
13725
14965
  const allProviders = providersForPicker(catalog);
13726
14966
  if (allProviders.length === 0) {
13727
- p18.log.warn("No providers found.");
13728
- p18.log.info(`${pc16.dim("OpenCode Zen/Go is always available. Add providers with ")}${pc16.cyan("relay-ai providers")}${pc16.dim(".")}`);
14967
+ p20.log.warn("No providers found.");
14968
+ p20.log.info(`${pc17.dim("OpenCode Zen/Go is always available. Add providers with ")}${pc17.cyan("relay-ai providers")}${pc17.dim(".")}`);
13729
14969
  relayOutro("Done");
13730
14970
  return 0;
13731
14971
  }
@@ -13743,45 +14983,45 @@ async function runModelsCommand() {
13743
14983
  for (let i = 0; i < favorites.length; i++) {
13744
14984
  const fav = favorites[i];
13745
14985
  const entry = modelLookup.get(`${fav.providerId}:${fav.modelId}`);
13746
- const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${pc16.dim(`(${entry.providerName})`)}` : pc16.dim(`\u2605 ${fav.modelId} \u2014 provider gone`);
14986
+ const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${pc17.dim(`(${entry.providerName})`)}` : pc17.dim(`\u2605 ${fav.modelId} \u2014 provider gone`);
13747
14987
  options.push({ value: `fav-${i}`, label, hint: "select to remove" });
13748
14988
  }
13749
14989
  const atCap = favorites.length >= MAX_MODEL_CATALOG;
13750
14990
  options.push({
13751
14991
  value: "__add__",
13752
- label: atCap ? pc16.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc16.cyan("+ Add a model \u2192"),
14992
+ label: atCap ? pc17.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc17.cyan("+ Add a model \u2192"),
13753
14993
  hint: atCap ? "Remove a favorite first to make room" : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
13754
14994
  });
13755
14995
  options.push({ value: "__done__", label: "Done", hint: "" });
13756
14996
  const header = favorites.length === 0 ? `Favorites (0/${MAX_MODEL_CATALOG})` : `Favorites (${favorites.length}/${MAX_MODEL_CATALOG}) \u2014 select to remove`;
13757
- const choice = await p18.select({
14997
+ const choice = await p20.select({
13758
14998
  message: header,
13759
14999
  options,
13760
15000
  initialValue: "__done__"
13761
15001
  });
13762
- if (p18.isCancel(choice) || choice === "__done__") break;
15002
+ if (p20.isCancel(choice) || choice === "__done__") break;
13763
15003
  if (choice === "__add__") {
13764
15004
  if (atCap) {
13765
- p18.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
15005
+ p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
13766
15006
  continue;
13767
15007
  }
13768
15008
  const globalCount = buildGlobalFavoriteIndex(allProviders).length;
13769
- const addPath = await p18.select({
15009
+ const addPath = await p20.select({
13770
15010
  message: "Add a favorite",
13771
15011
  options: [
13772
15012
  {
13773
15013
  value: "global",
13774
- label: pc16.cyan("Search all providers"),
15014
+ label: pc17.cyan("Search all providers"),
13775
15015
  hint: `${globalCount} models \xB7 ${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""}`
13776
15016
  },
13777
15017
  {
13778
15018
  value: "provider",
13779
- label: pc16.cyan("Browse by provider \u2192"),
15019
+ label: pc17.cyan("Browse by provider \u2192"),
13780
15020
  hint: "Pick one provider first"
13781
15021
  }
13782
15022
  ]
13783
15023
  });
13784
- if (p18.isCancel(addPath)) continue;
15024
+ if (p20.isCancel(addPath)) continue;
13785
15025
  let provider;
13786
15026
  let browsedMultiple = [];
13787
15027
  if (addPath === "global") {
@@ -13796,12 +15036,12 @@ async function runModelsCommand() {
13796
15036
  let currentInitialProvider = void 0;
13797
15037
  while (true) {
13798
15038
  const providerOptions = allProviders.map((ap) => providerSelectOption(ap));
13799
- const pickedProviderId = await p18.select({
15039
+ const pickedProviderId = await p20.select({
13800
15040
  message: "Which provider?",
13801
15041
  options: providerOptions,
13802
15042
  initialValue: currentInitialProvider
13803
15043
  });
13804
- if (p18.isCancel(pickedProviderId)) break;
15044
+ if (p20.isCancel(pickedProviderId)) break;
13805
15045
  provider = allProviders.find((ap) => ap.id === pickedProviderId);
13806
15046
  const options2 = provider.models.map((m) => {
13807
15047
  const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
@@ -13809,15 +15049,15 @@ async function runModelsCommand() {
13809
15049
  return {
13810
15050
  value: m.id,
13811
15051
  label: fmtModel(label, m.id),
13812
- hint: favorited ? pc16.yellow("\u2605 already favorite") : ""
15052
+ hint: favorited ? pc17.yellow("\u2605 already favorite") : ""
13813
15053
  };
13814
15054
  });
13815
- const pickedModelIds = await p18.multiselect({
13816
- message: `Select models to add from ${provider.name} ${pc16.dim("(Space to select, Enter to confirm)")}`,
15055
+ const pickedModelIds = await p20.multiselect({
15056
+ message: `Select models to add from ${provider.name} ${pc17.dim("(Space to select, Enter to confirm)")}`,
13817
15057
  options: options2,
13818
15058
  required: false
13819
15059
  });
13820
- if (p18.isCancel(pickedModelIds)) {
15060
+ if (p20.isCancel(pickedModelIds)) {
13821
15061
  currentInitialProvider = provider.id;
13822
15062
  continue;
13823
15063
  }
@@ -13852,16 +15092,16 @@ async function runModelsCommand() {
13852
15092
  if (addedModels.length > 0) {
13853
15093
  if (addedModels.length === 1) {
13854
15094
  const modelName = addedModels[0].name || addedModels[0].id;
13855
- p18.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
15095
+ p20.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
13856
15096
  } else {
13857
- p18.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
15097
+ p20.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
13858
15098
  }
13859
15099
  }
13860
15100
  if (duplicateCount > 0) {
13861
- p18.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
15101
+ p20.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
13862
15102
  }
13863
15103
  if (limitReached) {
13864
- p18.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
15104
+ p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
13865
15105
  }
13866
15106
  } else if (choice.startsWith("fav-")) {
13867
15107
  const idx = parseInt(choice.slice(4), 10);
@@ -13870,7 +15110,7 @@ async function runModelsCommand() {
13870
15110
  const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
13871
15111
  favorites = removeFavorite(favorites, fav);
13872
15112
  favoritesDirty = true;
13873
- p18.log.success(`Removed ${label} from favorites.`);
15113
+ p20.log.success(`Removed ${label} from favorites.`);
13874
15114
  }
13875
15115
  }
13876
15116
  if (favoritesDirty) {
@@ -13878,7 +15118,7 @@ async function runModelsCommand() {
13878
15118
  }
13879
15119
  relayOutro(
13880
15120
  favorites.length === 0 ? "No favorites saved" : `${favorites.length} favorite${favorites.length !== 1 ? "s" : ""} saved`,
13881
- favorites.length === 0 ? pc16.dim("Launch uses single-model mode") : pc16.cyan("/model menu ready on next launch")
15121
+ favorites.length === 0 ? pc17.dim("Launch uses single-model mode") : pc17.cyan("/model menu ready on next launch")
13882
15122
  );
13883
15123
  return 0;
13884
15124
  }
@@ -13889,7 +15129,7 @@ async function runClaudeCommand(parsed) {
13889
15129
  setAgentStdoutMode(agentStdout);
13890
15130
  const claudePath = findClaudeBinary();
13891
15131
  if (!claudePath) {
13892
- console.error(pc16.red("\nError: claude binary not found on PATH.\n"));
15132
+ console.error(pc17.red("\nError: claude binary not found on PATH.\n"));
13893
15133
  console.error("Install Claude Code:");
13894
15134
  console.error(" npm install -g @anthropic-ai/claude-code\n");
13895
15135
  return 1;
@@ -13904,7 +15144,7 @@ async function runClaudeCommand(parsed) {
13904
15144
  prefs
13905
15145
  });
13906
15146
  if (launchPlan.error) {
13907
- console.error(pc16.red(`
15147
+ console.error(pc17.red(`
13908
15148
  Error: ${launchPlan.error}
13909
15149
  `));
13910
15150
  return 1;
@@ -13912,7 +15152,7 @@ Error: ${launchPlan.error}
13912
15152
  const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
13913
15153
  if (!agentStdout) relayIntro("Claude Code");
13914
15154
  if (setup && !dryRun && !agentStdout) {
13915
- p18.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
15155
+ p20.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
13916
15156
  }
13917
15157
  if (!dryRun && await needsFirstRunSetup()) {
13918
15158
  const firstRun = await runFirstRunWizard(trace);
@@ -13923,25 +15163,25 @@ Error: ${launchPlan.error}
13923
15163
  try {
13924
15164
  catalog = await fetchProviderCatalog();
13925
15165
  } catch (err) {
13926
- console.error(pc16.red(String(err instanceof Error ? err.message : err)));
15166
+ console.error(pc17.red(String(err instanceof Error ? err.message : err)));
13927
15167
  return 1;
13928
15168
  }
13929
15169
  } else {
13930
- const catalogSpinner = p18.spinner();
15170
+ const catalogSpinner = p20.spinner();
13931
15171
  catalogSpinner.start("Loading your providers...");
13932
15172
  try {
13933
15173
  catalog = await fetchProviderCatalog();
13934
15174
  } catch (err) {
13935
15175
  catalogSpinner.stop("");
13936
- console.error(pc16.red(String(err instanceof Error ? err.message : err)));
15176
+ console.error(pc17.red(String(err instanceof Error ? err.message : err)));
13937
15177
  return 1;
13938
15178
  }
13939
15179
  catalogSpinner.stop("");
13940
15180
  }
13941
15181
  const allProviders = providersForPicker(catalog);
13942
15182
  if (allProviders.length === 0) {
13943
- p18.log.warn("No providers available.");
13944
- p18.log.info(pc16.dim("Run relay-ai providers add or import to get started."));
15183
+ p20.log.warn("No providers available.");
15184
+ p20.log.info(pc17.dim("Run relay-ai providers add or import to get started."));
13945
15185
  return 0;
13946
15186
  }
13947
15187
  const providerOptions = allProviders.map((lp) => providerSelectOption(lp));
@@ -13958,7 +15198,7 @@ Error: ${launchPlan.error}
13958
15198
  if (launchPlan.skip && launchPlan.target) {
13959
15199
  const resolved = findProviderAndModel(allProviders, launchPlan.target);
13960
15200
  if (!resolved) {
13961
- p18.log.error(
15201
+ p20.log.error(
13962
15202
  `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
13963
15203
  );
13964
15204
  return 1;
@@ -13966,31 +15206,31 @@ Error: ${launchPlan.error}
13966
15206
  activeProvider = resolved.provider;
13967
15207
  selectedModel = resolved.model;
13968
15208
  if (!agentStdout) {
13969
- p18.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
15209
+ p20.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13970
15210
  }
13971
15211
  if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
13972
15212
  } else {
13973
15213
  let currentInitialProvider = initialProvider;
13974
15214
  while (true) {
13975
- const chosen = await p18.select({
15215
+ const chosen = await p20.select({
13976
15216
  message: "Which provider?",
13977
15217
  options: providerOptions,
13978
15218
  initialValue: currentInitialProvider
13979
15219
  });
13980
- if (p18.isCancel(chosen)) {
13981
- p18.cancel("Cancelled.");
15220
+ if (p20.isCancel(chosen)) {
15221
+ p20.cancel("Cancelled.");
13982
15222
  return 0;
13983
15223
  }
13984
15224
  const providerChoice = chosen;
13985
15225
  if (providerChoice === "__favorites__") {
13986
15226
  const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
13987
15227
  if (!favoriteStart) {
13988
- p18.log.warn("No saved favorites are currently available.");
15228
+ p20.log.warn("No saved favorites are currently available.");
13989
15229
  return 0;
13990
15230
  }
13991
15231
  activeProvider = favoriteStart.provider;
13992
15232
  selectedModel = favoriteStart.model;
13993
- p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
15233
+ p20.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13994
15234
  break;
13995
15235
  } else {
13996
15236
  activeProvider = allProviders.find((lp) => lp.id === providerChoice);
@@ -14017,27 +15257,27 @@ Error: ${launchPlan.error}
14017
15257
  );
14018
15258
  const startingRoute = resolveRoute(activeProvider.id, selectedModel.id) ?? null;
14019
15259
  if (!startingRoute) {
14020
- p18.log.error("Could not resolve a proxy route for the selected model.");
15260
+ p20.log.error("Could not resolve a proxy route for the selected model.");
14021
15261
  return 1;
14022
15262
  }
14023
15263
  const { routes: catalogRoutes, droppedFavorites } = buildCatalogRoutes(startingRoute, favorites, resolveRoute);
14024
15264
  if (droppedFavorites.length > 0) {
14025
- p18.log.warn(
15265
+ p20.log.warn(
14026
15266
  `Skipping ${droppedFavorites.length} favorite${droppedFavorites.length === 1 ? "" : "s"} that are no longer available in /model`
14027
15267
  );
14028
15268
  }
14029
15269
  if (dryRun) {
14030
15270
  const endpoint = selectedModel.baseUrl ?? selectedModel.completionsUrl ?? "(unknown)";
14031
15271
  console.log("");
14032
- console.log(pc16.bold(pc16.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
15272
+ console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
14033
15273
  console.log("");
14034
- console.log(` ${pc16.bold("Provider:")} ${activeProvider.name}`);
14035
- console.log(` ${pc16.bold("Starting model:")} ${selectedModel.id}`);
14036
- console.log(` ${pc16.bold("Endpoint:")} ${endpoint}`);
14037
- console.log(` ${pc16.bold("/model catalog:")} ${catalogRoutes.length} model(s)`);
14038
- catalogRoutes.forEach((r) => console.log(` ${pc16.dim(r.displayName)}`));
15274
+ console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
15275
+ console.log(` ${pc17.bold("Starting model:")} ${selectedModel.id}`);
15276
+ console.log(` ${pc17.bold("Endpoint:")} ${endpoint}`);
15277
+ console.log(` ${pc17.bold("/model catalog:")} ${catalogRoutes.length} model(s)`);
15278
+ catalogRoutes.forEach((r) => console.log(` ${pc17.dim(r.displayName)}`));
14039
15279
  console.log("");
14040
- console.log(pc16.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
15280
+ console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
14041
15281
  console.log("");
14042
15282
  return 0;
14043
15283
  }
@@ -14053,21 +15293,21 @@ Error: ${launchPlan.error}
14053
15293
  const formatDesc = selectedModel.modelFormat === "anthropic" ? "direct passthrough" : "via SDK adapter proxy";
14054
15294
  const endpoint = selectedModel.modelFormat === "anthropic" ? selectedModel.baseUrl ?? "(unknown)" : selectedModel.npm ?? "SDK";
14055
15295
  console.log("");
14056
- console.log(pc16.bold(pc16.cyan(" DRY RUN \u2014 would execute:")));
15296
+ console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute:")));
14057
15297
  console.log("");
14058
- console.log(` ${pc16.bold("Provider:")} ${activeProvider.name}`);
14059
- console.log(` ${pc16.bold("Model:")} ${selectedModel.id}`);
14060
- console.log(` ${pc16.bold("Format:")} ${selectedModel.modelFormat} (${formatDesc})`);
14061
- console.log(` ${pc16.bold(selectedModel.modelFormat === "anthropic" ? "Endpoint:" : "SDK npm:")} ${endpoint}`);
14062
- console.log(` ${pc16.bold("Key:")} ${activeProvider.name} provider key`);
15298
+ console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
15299
+ console.log(` ${pc17.bold("Model:")} ${selectedModel.id}`);
15300
+ console.log(` ${pc17.bold("Format:")} ${selectedModel.modelFormat} (${formatDesc})`);
15301
+ console.log(` ${pc17.bold(selectedModel.modelFormat === "anthropic" ? "Endpoint:" : "SDK npm:")} ${endpoint}`);
15302
+ console.log(` ${pc17.bold("Key:")} ${activeProvider.name} provider key`);
14063
15303
  console.log("");
14064
- console.log(pc16.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
15304
+ console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
14065
15305
  console.log("");
14066
15306
  return 0;
14067
15307
  }
14068
15308
  const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
14069
15309
  if (!launchApiKey?.trim()) {
14070
- p18.log.error(
15310
+ p20.log.error(
14071
15311
  `No credential found for ${activeProvider.name}. Add a key with relay-ai providers or set OPENCODE_API_KEY.`
14072
15312
  );
14073
15313
  return 1;
@@ -14103,12 +15343,12 @@ Error: ${launchPlan.error}
14103
15343
  launchApiKey
14104
15344
  );
14105
15345
  if (!isAgentStdoutMode()) {
14106
- p18.log.info(
14107
- `SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc16.dim(` (${selectedModel.npm})`) : "")
15346
+ p20.log.info(
15347
+ `SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc17.dim(` (${selectedModel.npm})`) : "")
14108
15348
  );
14109
15349
  }
14110
15350
  } catch (err) {
14111
- p18.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
15351
+ p20.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
14112
15352
  return 1;
14113
15353
  }
14114
15354
  childEnv = buildChildEnv(
@@ -14124,7 +15364,7 @@ Error: ${launchPlan.error}
14124
15364
  }
14125
15365
  const debugLogPath = prepareClaudeTraceLog();
14126
15366
  const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
14127
- if (trace) p18.log.info(`Debug log: ${debugLogPath}`);
15367
+ if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
14128
15368
  const exitCode = await launchClaude(
14129
15369
  childEnv,
14130
15370
  claudeCodeClientModelId(selectedModel.id, selectedModel.contextWindow),
@@ -14137,7 +15377,7 @@ Error: ${launchPlan.error}
14137
15377
  async function main(args = process.argv.slice(2)) {
14138
15378
  const parsed = parseArgs(args);
14139
15379
  if (parsed.error) {
14140
- console.error(pc16.red(`
15380
+ console.error(pc17.red(`
14141
15381
  Error: ${parsed.error}
14142
15382
  `));
14143
15383
  printHelp(rootHelpText());
@@ -14192,6 +15432,9 @@ Error: ${parsed.error}
14192
15432
  printHelp(providersHelpText());
14193
15433
  return 0;
14194
15434
  }
15435
+ if (parsed.trace) {
15436
+ process.env.RELAY_AI_TRACE = "1";
15437
+ }
14195
15438
  return runProvidersCommand(parsed.claudeArgs);
14196
15439
  }
14197
15440
  if (parsed.command === "codex-app") {
@@ -14223,6 +15466,20 @@ Error: ${parsed.error}
14223
15466
  vertex: parsed.vertex
14224
15467
  });
14225
15468
  }
15469
+ if (parsed.command === "gemini") {
15470
+ if (parsed.showVersion) {
15471
+ console.log(VERSION);
15472
+ return 0;
15473
+ }
15474
+ if (parsed.showHelp) {
15475
+ console.log(geminiHelpText());
15476
+ return 0;
15477
+ }
15478
+ return runGeminiCommand(parsed.claudeArgs, parsed.trace, {
15479
+ launchProvider: parsed.launchProvider,
15480
+ launchModel: parsed.launchModel
15481
+ });
15482
+ }
14226
15483
  if (parsed.showVersion) {
14227
15484
  console.log(VERSION);
14228
15485
  return 0;
@@ -14248,7 +15505,7 @@ if (isCliEntryPoint()) {
14248
15505
  if (err === /* @__PURE__ */ Symbol.for("clack:cancel")) {
14249
15506
  process.exit(0);
14250
15507
  }
14251
- console.error(pc16.red("\nUnexpected error:"), err);
15508
+ console.error(pc17.red("\nUnexpected error:"), err);
14252
15509
  process.exit(1);
14253
15510
  });
14254
15511
  }