@jacobbd/relay-ai 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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.0",
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")
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,7 +10001,7 @@ 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;
@@ -9874,7 +10010,7 @@ async function startCodexProxy(routes, options = {}) {
9874
10010
  const resolved = resolveModel(routes, models, modelId);
9875
10011
  if (!resolved) {
9876
10012
  if (debug) {
9877
- log17(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
10013
+ log19(`resolveModel failed: requested="${modelId}" known=[${routes.map((r) => r.modelId).join(", ")}]`);
9878
10014
  }
9879
10015
  sendJson(res, 404, { error: { message: `Unknown model: ${modelId}`, type: "invalid_request_error" } });
9880
10016
  return;
@@ -9894,7 +10030,7 @@ async function startCodexProxy(routes, options = {}) {
9894
10030
  );
9895
10031
  if (debug) {
9896
10032
  const effort = body.reasoning?.effort;
9897
- log17(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
10033
+ log19(`model=${route.modelId} effort=${effort ?? "(none)"} providerOptions=${JSON.stringify(params.providerOptions)}`);
9898
10034
  }
9899
10035
  if (body.stream) {
9900
10036
  res.writeHead(200, {
@@ -9924,7 +10060,7 @@ async function startCodexProxy(routes, options = {}) {
9924
10060
  }
9925
10061
  } catch (err) {
9926
10062
  const msg = formatUpstreamError(err);
9927
- log17(`handler error: ${msg}`);
10063
+ log19(`handler error: ${msg}`);
9928
10064
  sendJson(res, 500, { error: { message: msg, type: "api_error" } });
9929
10065
  }
9930
10066
  return;
@@ -10150,7 +10286,7 @@ function restoreCodexOverlay(env = process.env) {
10150
10286
  return removed;
10151
10287
  }
10152
10288
  function remainingOverlayPaths(env = process.env) {
10153
- return ownedOverlayPaths(env).filter((p19) => existsSync11(p19));
10289
+ return ownedOverlayPaths(env).filter((p21) => existsSync11(p21));
10154
10290
  }
10155
10291
  function recoverInterruptedCodexSession(env = process.env) {
10156
10292
  const before = remainingOverlayPaths(env);
@@ -10547,8 +10683,8 @@ function resolveCodexFavorites(activeProvider, selectedModel, compatible, favori
10547
10683
  agent,
10548
10684
  localProviders: compatible,
10549
10685
  zenGoApiKey,
10550
- zenModels: compatible.find((p19) => p19.id === "zen")?.models,
10551
- goModels: compatible.find((p19) => p19.id === "go")?.models,
10686
+ zenModels: compatible.find((p21) => p21.id === "zen")?.models,
10687
+ goModels: compatible.find((p21) => p21.id === "go")?.models,
10552
10688
  findLocalModel: (pid, mid) => {
10553
10689
  const provider = compatible.find((lp) => lp.id === pid);
10554
10690
  const model = provider?.models.find((m) => m.id === mid);
@@ -10620,8 +10756,24 @@ function isClaudeMachineReadableOutput(args) {
10620
10756
  function isCodexMachineReadableOutput(args) {
10621
10757
  return args.includes("--json");
10622
10758
  }
10759
+ function isGeminiNonInteractive(args) {
10760
+ for (let i = 0; i < args.length; i++) {
10761
+ const arg = args[i];
10762
+ if (arg === "--") return false;
10763
+ if (arg === "-p" || arg === "--prompt" || arg === "-i" || arg === "--prompt-interactive") return true;
10764
+ if (arg.startsWith("-")) {
10765
+ i = skipAttachedFlagValue(args, i);
10766
+ continue;
10767
+ }
10768
+ return true;
10769
+ }
10770
+ return false;
10771
+ }
10623
10772
  function wantsCleanAgentStdout(agent, childArgs) {
10624
- return agent === "claude" ? isClaudeMachineReadableOutput(childArgs) : isCodexMachineReadableOutput(childArgs);
10773
+ if (agent === "claude") return isClaudeMachineReadableOutput(childArgs);
10774
+ if (agent === "codex") return isCodexMachineReadableOutput(childArgs);
10775
+ const outFmt = readFlagValue(childArgs, "-o") || readFlagValue(childArgs, "--output-format");
10776
+ return outFmt === "json" || outFmt === "stream-json";
10625
10777
  }
10626
10778
  function normalizeClaudeAgentArgs(args) {
10627
10779
  const out = [...args];
@@ -10653,14 +10805,14 @@ function isCodexNonInteractive(args) {
10653
10805
  }
10654
10806
  function resolveLaunchTarget(explicit, prefs, agent) {
10655
10807
  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);
10808
+ const providerId = explicit.providerId ?? slug?.providerId ?? (agent === "claude" ? prefs.lastProvider : agent === "codex" ? prefs.lastCodexProvider : prefs.lastGeminiProvider);
10809
+ const modelId = slug?.modelId ?? explicit.modelId ?? (agent === "claude" ? prefs.lastModel : agent === "codex" ? prefs.lastCodexModel : prefs.lastGeminiModel);
10658
10810
  if (!providerId || !modelId) return null;
10659
10811
  return { providerId, modelId };
10660
10812
  }
10661
10813
  function findProviderAndModel(providers, target) {
10662
10814
  if (!target.providerId || !target.modelId) return null;
10663
- const provider = providers.find((p19) => p19.id === target.providerId);
10815
+ const provider = providers.find((p21) => p21.id === target.providerId);
10664
10816
  if (!provider) return null;
10665
10817
  const model = provider.models.find((m) => m.id === target.modelId);
10666
10818
  if (!model) return null;
@@ -10677,7 +10829,7 @@ function hasCompleteExplicitLaunch(explicit) {
10677
10829
  function planLaunchWizard(opts) {
10678
10830
  const { explicit, childArgs, agent, prefs } = opts;
10679
10831
  const explicitComplete = hasCompleteExplicitLaunch(explicit);
10680
- const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : isCodexNonInteractive(childArgs);
10832
+ const nonInteractive = agent === "claude" ? isClaudePrintMode(childArgs) : agent === "codex" ? isCodexNonInteractive(childArgs) : isGeminiNonInteractive(childArgs);
10681
10833
  if (explicitComplete) {
10682
10834
  const target = resolveLaunchTarget(explicit, prefs, agent);
10683
10835
  if (!target) {
@@ -10710,7 +10862,9 @@ function planLaunchWizard(opts) {
10710
10862
  return { skip: false, target: null };
10711
10863
  }
10712
10864
  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.";
10865
+ if (agent === "claude") return "Print mode requires --provider and --model, or saved preferences from a prior launch.";
10866
+ if (agent === "codex") return "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
10867
+ return "Non-interactive Gemini launch requires --provider and --model, or saved preferences from a prior launch.";
10714
10868
  }
10715
10869
 
10716
10870
  // src/codex.ts
@@ -11121,7 +11275,7 @@ Error: ${launchPlan.error}
11121
11275
  resolvedFavorites = res.resolvedFavorites;
11122
11276
  providersById = res.providersById;
11123
11277
  }
11124
- const regEntry = loadRegistry().providers.find((p19) => p19.id === activeProvider.id);
11278
+ const regEntry = loadRegistry().providers.find((p21) => p21.id === activeProvider.id);
11125
11279
  const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
11126
11280
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
11127
11281
  if (!apiKey) {
@@ -11175,7 +11329,7 @@ Error: ${launchPlan.error}
11175
11329
  });
11176
11330
  if (configOnly) {
11177
11331
  const home = process.env["HOME"] ?? "";
11178
- const shortenPath = (p19) => home ? p19.replace(home, "~") : p19;
11332
+ const shortenPath = (p21) => home ? p21.replace(home, "~") : p21;
11179
11333
  console.log("");
11180
11334
  console.log(pc13.bold(pc13.cyan(" CONFIG PREVIEW \u2014 relay-ai codex")));
11181
11335
  console.log("");
@@ -11239,126 +11393,1128 @@ Error: ${launchPlan.error}
11239
11393
  }
11240
11394
  }
11241
11395
 
11242
- // src/codex-app.ts
11396
+ // src/gemini.ts
11243
11397
  import pc14 from "picocolors";
11244
11398
  import * as p15 from "@clack/prompts";
11245
11399
 
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 : {};
11400
+ // src/gemini/launch.ts
11401
+ import { execSync as execSync4, spawn as spawn5 } from "child_process";
11402
+ import { existsSync as existsSync13 } from "fs";
11403
+ import { homedir as homedir10 } from "os";
11404
+ import { join as join14 } from "path";
11405
+ var isWindows4 = process.platform === "win32";
11406
+ var GEMINI_FALLBACK_PATHS = isWindows4 ? [
11407
+ join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini.cmd"),
11408
+ join14(process.env["APPDATA"] ?? homedir10(), "npm", "gemini")
11409
+ ] : [
11410
+ join14(homedir10(), ".local", "bin", "gemini"),
11411
+ join14(homedir10(), ".npm", "bin", "gemini"),
11412
+ "/usr/local/bin/gemini",
11413
+ "/opt/homebrew/bin/gemini"
11414
+ ];
11415
+ function findGeminiBinary() {
11416
+ try {
11417
+ const result = execSync4(isWindows4 ? "where.exe gemini" : "which gemini", {
11418
+ encoding: "utf8",
11419
+ stdio: ["pipe", "pipe", "pipe"]
11420
+ });
11421
+ const path = result.trim().split("\n")[0]?.trim();
11422
+ if (path) return path;
11423
+ } catch {
11424
+ }
11425
+ for (const path of GEMINI_FALLBACK_PATHS) {
11426
+ if (existsSync13(path)) return path;
11427
+ }
11428
+ return null;
11258
11429
  }
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 ?? "") };
11430
+ function buildGeminiChildEnv(proxyPort, proxyToken) {
11431
+ const env = { ...process.env };
11432
+ delete env["GOOGLE_GEMINI_BASE_URL"];
11433
+ delete env["GEMINI_API_KEY"];
11434
+ delete env["GOOGLE_API_KEY"];
11435
+ delete env["GOOGLE_GENAI_API_KEY"];
11436
+ env["GOOGLE_GEMINI_BASE_URL"] = `http://127.0.0.1:${proxyPort}`;
11437
+ env["GEMINI_API_KEY"] = proxyToken;
11438
+ return env;
11263
11439
  }
11264
- function readCodexConfigText(path = getCodexConfigPath()) {
11265
- if (!existsSync13(path)) return "";
11266
- return readFileSync11(path, "utf8");
11440
+ function launchGemini(geminiPath, modelId, env, extraArgs) {
11441
+ return new Promise((resolve) => {
11442
+ const args = ["-m", modelId, ...extraArgs];
11443
+ const child = spawn5(geminiPath, args, {
11444
+ stdio: "inherit",
11445
+ env,
11446
+ shell: isWindows4
11447
+ });
11448
+ const onSigInt = () => child.kill("SIGINT");
11449
+ const onSigTerm = () => child.kill("SIGTERM");
11450
+ process.once("SIGINT", onSigInt);
11451
+ process.once("SIGTERM", onSigTerm);
11452
+ const done = (code) => {
11453
+ process.off("SIGINT", onSigInt);
11454
+ process.off("SIGTERM", onSigTerm);
11455
+ resolve(code);
11456
+ };
11457
+ child.on("error", () => done(1));
11458
+ child.on("exit", (code) => done(code ?? 0));
11459
+ });
11267
11460
  }
11268
- function parseCodexConfig(text5) {
11269
- if (!text5.trim()) return {};
11270
- return asRecord(parse(text5));
11461
+
11462
+ // src/gemini/prompts.ts
11463
+ import * as p14 from "@clack/prompts";
11464
+ async function pickGeminiProvider(providers, prefs, hasFavorites = false, initialProviderId) {
11465
+ if (providers.length === 0 && !hasFavorites) return null;
11466
+ const options = providers.map((lp) => providerSelectOption(lp));
11467
+ if (hasFavorites) {
11468
+ options.unshift({
11469
+ value: "__favorites__",
11470
+ label: "\u2B50 Favorites Catalog",
11471
+ hint: `${prefs.favoriteModels?.length ?? 0} saved favorites`
11472
+ });
11473
+ }
11474
+ const initial = initialProviderId && options.some((o) => o.value === initialProviderId) ? initialProviderId : prefs.lastGeminiProvider && options.some((o) => o.value === prefs.lastGeminiProvider) ? prefs.lastGeminiProvider : options[0].value;
11475
+ const chosen = await p14.select({
11476
+ message: "Which provider for Gemini CLI?",
11477
+ options,
11478
+ initialValue: initial
11479
+ });
11480
+ if (p14.isCancel(chosen)) {
11481
+ p14.cancel("Cancelled.");
11482
+ return null;
11483
+ }
11484
+ if (chosen === "__favorites__") return "__favorites__";
11485
+ return providers.find((lp) => lp.id === chosen) ?? null;
11271
11486
  }
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
- };
11487
+ async function pickGeminiModel(provider, prefs) {
11488
+ const recentIds = (prefs.recentModelsByProvider?.[provider.id] ?? []).slice(0, 3);
11489
+ const recentModels = recentIds.map((id) => provider.models.find((m) => m.id === id)).filter((m) => m !== void 0);
11490
+ let selectedModel = null;
11491
+ while (true) {
11492
+ if (recentModels.length > 0) {
11493
+ const options = [
11494
+ ...recentModels.map((m) => modelSelectOption(m, "recent")),
11495
+ navOption("__browse_all__", "Browse all models \u2192", `${provider.models.length} available`),
11496
+ navOption("__back__", "\u2190 Go back", "Select a different provider")
11497
+ ];
11498
+ const picked = await p14.select({
11499
+ message: `Model for ${provider.name}?`,
11500
+ options,
11501
+ initialValue: recentModels[0].id
11502
+ });
11503
+ if (p14.isCancel(picked) || String(picked) === "__back__") {
11504
+ return "back";
11505
+ }
11506
+ if (String(picked) === "__browse_all__") {
11507
+ const browsed = await browseAllModels(provider, prefs);
11508
+ if (browsed === "back") {
11509
+ continue;
11510
+ }
11511
+ if (!browsed) return null;
11512
+ selectedModel = browsed;
11513
+ break;
11514
+ } else {
11515
+ selectedModel = recentModels.find((m) => m.id === String(picked));
11516
+ break;
11517
+ }
11518
+ } else {
11519
+ const browsed = await browseAllModels(provider, prefs);
11520
+ if (browsed === "back") {
11521
+ return "back";
11522
+ }
11523
+ if (!browsed) return null;
11524
+ selectedModel = browsed;
11525
+ break;
11526
+ }
11527
+ }
11528
+ return selectedModel;
11294
11529
  }
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);
11530
+ function confirmGeminiLaunch(providerName, modelLabel, modelId) {
11531
+ return p14.confirm({
11532
+ message: confirmLaunchMessage("Gemini CLI", modelLabel, modelId, providerName),
11533
+ initialValue: true
11534
+ }).then((answer) => {
11535
+ if (p14.isCancel(answer)) {
11536
+ p14.cancel("Cancelled.");
11537
+ return false;
11538
+ }
11539
+ return answer;
11540
+ });
11302
11541
  }
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;
11542
+ async function pickGeminiFavoriteModel(providers, favorites) {
11543
+ const favList = [];
11544
+ for (const fav of favorites) {
11545
+ const provider2 = providers.find((lp) => lp.id === fav.providerId);
11546
+ const model2 = provider2?.models.find((m) => m.id === fav.modelId);
11547
+ if (provider2 && model2) favList.push({ provider: provider2, model: model2 });
11319
11548
  }
11320
- if (Object.keys(providers).length === 0) {
11321
- delete out.model_providers;
11322
- } else {
11323
- out.model_providers = providers;
11549
+ if (favList.length === 0) {
11550
+ p14.log.warn("None of your saved favorites are available in the current registry.");
11551
+ return null;
11324
11552
  }
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
- }
11553
+ const options = [
11554
+ ...favList.map(({ provider: provider2, model: model2 }) => ({
11555
+ value: `${provider2.id}::${model2.id}`,
11556
+ label: model2.name || model2.id,
11557
+ hint: provider2.name
11558
+ })),
11559
+ { value: "__back__", label: "\u2190 Go back", hint: "Select a different provider" }
11560
+ ];
11561
+ const picked = await p14.select({
11562
+ message: "Pick a favorite model for Gemini CLI:",
11563
+ options,
11564
+ initialValue: options[0].value
11565
+ });
11566
+ if (p14.isCancel(picked) || String(picked) === "__back__") return "back";
11567
+ const [pickedProviderId, pickedModelId] = picked.split("::");
11568
+ const provider = providers.find((lp) => lp.id === pickedProviderId);
11569
+ const model = provider?.models.find((m) => m.id === pickedModelId);
11570
+ if (!provider || !model) return null;
11571
+ return { provider, model };
11572
+ }
11573
+ function rejectGeminiManagedFlags(geminiArgs) {
11574
+ const blocked = /* @__PURE__ */ new Set(["--provider", "--model", "-m", "--trace"]);
11575
+ const takesValue = /* @__PURE__ */ new Set(["--provider", "--model", "-m"]);
11576
+ const out = [];
11577
+ for (let i = 0; i < geminiArgs.length; i++) {
11578
+ const arg = geminiArgs[i];
11579
+ if (blocked.has(arg)) {
11580
+ if (takesValue.has(arg)) i++;
11581
+ continue;
11340
11582
  }
11583
+ if (arg.startsWith("--model=") || arg.startsWith("--provider=") || arg.startsWith("-m=")) continue;
11584
+ out.push(arg);
11341
11585
  }
11342
11586
  return out;
11343
11587
  }
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");
11348
- }
11349
- const profiles = asRecord(config.profiles);
11350
- if (profiles[CODEX_APP_PROVIDER_ID]) {
11351
- throw new Error("Generated config still contains legacy profiles table");
11588
+
11589
+ // src/gemini-proxy.ts
11590
+ import { createServer as createServer4 } from "http";
11591
+ import { randomUUID as randomUUID2 } from "crypto";
11592
+ import { streamText as streamText4, generateText as generateText4, tool as tool4, jsonSchema as jsonSchema4 } from "ai";
11593
+ function mapFinishReason(reason) {
11594
+ if (reason === "stop" || reason === "tool-calls") return "STOP";
11595
+ if (reason === "length") return "MAX_TOKENS";
11596
+ if (reason === "content-filter") return "SAFETY";
11597
+ return "OTHER";
11598
+ }
11599
+ function lookupGeminiRoute(routes, requestedModel) {
11600
+ const ids = [requestedModel, ...routeLookupIds(requestedModel)];
11601
+ const slashIdx = requestedModel.indexOf("/");
11602
+ if (slashIdx >= 0) {
11603
+ const after = requestedModel.slice(slashIdx + 1);
11604
+ ids.push(after, ...routeLookupIds(after));
11352
11605
  }
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");
11606
+ const doubleUnderscore = requestedModel.indexOf("__");
11607
+ if (doubleUnderscore >= 0) {
11608
+ const after = requestedModel.slice(doubleUnderscore + 2);
11609
+ ids.push(after, ...routeLookupIds(after));
11356
11610
  }
11357
- const baseUrl = rootString(config, "openai_base_url");
11358
- if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
11359
- throw new Error("Generated config openai_base_url mismatch");
11611
+ const uniqueIds = [...new Set(ids)];
11612
+ for (const id of uniqueIds) {
11613
+ const route = routes.find((r) => r.aliasId === id || r.realModelId === id);
11614
+ if (route) return route;
11360
11615
  }
11361
- const catalog = rootString(config, "model_catalog_json");
11616
+ return void 0;
11617
+ }
11618
+ function mergeConsecutiveMessages2(messages) {
11619
+ const merged = [];
11620
+ for (const msg of messages) {
11621
+ if (merged.length === 0) {
11622
+ merged.push(msg);
11623
+ continue;
11624
+ }
11625
+ const last = merged[merged.length - 1];
11626
+ if (last.role === msg.role) {
11627
+ const lastContent = Array.isArray(last.content) ? last.content : [{ type: "text", text: last.content }];
11628
+ const nextContent = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: msg.content }];
11629
+ last.content = [...lastContent, ...nextContent];
11630
+ } else {
11631
+ merged.push(msg);
11632
+ }
11633
+ }
11634
+ return merged;
11635
+ }
11636
+ function stripGeminiIdentity(text5) {
11637
+ 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");
11638
+ }
11639
+ function translateGeminiRequest(body) {
11640
+ let system;
11641
+ if (body.systemInstruction?.parts) {
11642
+ const rawSystem = body.systemInstruction.parts.map((p21) => p21.text || "").join("\n");
11643
+ system = stripGeminiIdentity(rawSystem).trim();
11644
+ }
11645
+ const messages = [];
11646
+ const nameToIdList = /* @__PURE__ */ new Map();
11647
+ const contents = body.contents || [];
11648
+ for (const turn of contents) {
11649
+ const role = turn.role === "model" ? "assistant" : "user";
11650
+ const parts = [];
11651
+ const toolResults = [];
11652
+ const turnParts = turn.parts || [];
11653
+ for (const p21 of turnParts) {
11654
+ if (p21.text !== void 0) {
11655
+ const text5 = stripGeminiIdentity(p21.text);
11656
+ if (text5.includes("<thinking>")) {
11657
+ const tokens = text5.split(/<thinking>([\s\S]*?)<\/thinking>/);
11658
+ for (let i = 0; i < tokens.length; i++) {
11659
+ const token = tokens[i].trim();
11660
+ if (!token) continue;
11661
+ parts.push({ type: i % 2 === 1 ? "reasoning" : "text", text: token });
11662
+ }
11663
+ } else {
11664
+ parts.push({ type: "text", text: text5 });
11665
+ }
11666
+ } else if (p21.inlineData) {
11667
+ parts.push({
11668
+ type: "image",
11669
+ image: Buffer.from(p21.inlineData.data, "base64"),
11670
+ mediaType: p21.inlineData.mimeType
11671
+ });
11672
+ } else if (p21.functionCall) {
11673
+ const id = "call_" + randomUUID2().replace(/-/g, "");
11674
+ const name = p21.functionCall.name;
11675
+ if (!nameToIdList.has(name)) nameToIdList.set(name, []);
11676
+ nameToIdList.get(name).push(id);
11677
+ parts.push({
11678
+ type: "tool-call",
11679
+ toolCallId: id,
11680
+ toolName: name,
11681
+ input: p21.functionCall.args || {}
11682
+ });
11683
+ } else if (p21.functionResponse) {
11684
+ const name = p21.functionResponse.name;
11685
+ const idList = nameToIdList.get(name) || [];
11686
+ const id = idList.shift() || "call_" + randomUUID2().replace(/-/g, "");
11687
+ toolResults.push({
11688
+ type: "tool-result",
11689
+ toolCallId: id,
11690
+ toolName: name,
11691
+ output: {
11692
+ type: "text",
11693
+ value: typeof p21.functionResponse.response === "string" ? p21.functionResponse.response : JSON.stringify(p21.functionResponse.response || {})
11694
+ }
11695
+ });
11696
+ }
11697
+ }
11698
+ if (toolResults.length > 0) {
11699
+ messages.push({
11700
+ role: "tool",
11701
+ content: toolResults
11702
+ });
11703
+ }
11704
+ if (parts.length > 0) {
11705
+ messages.push({
11706
+ role,
11707
+ content: parts
11708
+ });
11709
+ }
11710
+ }
11711
+ const mergedMessages = mergeConsecutiveMessages2(messages);
11712
+ let tools;
11713
+ if (body.tools) {
11714
+ tools = {};
11715
+ for (const t of body.tools) {
11716
+ if (t.functionDeclarations) {
11717
+ for (const fd of t.functionDeclarations) {
11718
+ tools[fd.name] = tool4({
11719
+ description: fd.description || "",
11720
+ inputSchema: jsonSchema4(fd.parameters || { type: "object", properties: {} })
11721
+ });
11722
+ }
11723
+ }
11724
+ }
11725
+ }
11726
+ let toolChoice;
11727
+ const mode = body.toolConfig?.functionCallingConfig?.mode;
11728
+ if (mode === "ANY") {
11729
+ toolChoice = "required";
11730
+ } else if (mode === "AUTO") {
11731
+ toolChoice = "auto";
11732
+ }
11733
+ const generationConfig = body.generationConfig || {};
11734
+ let responseFormat;
11735
+ if (generationConfig.responseMimeType === "application/json") {
11736
+ responseFormat = { type: "json" };
11737
+ }
11738
+ return {
11739
+ system,
11740
+ messages: mergedMessages,
11741
+ tools: tools && Object.keys(tools).length > 0 ? tools : void 0,
11742
+ toolChoice,
11743
+ maxOutputTokens: generationConfig.maxOutputTokens,
11744
+ temperature: generationConfig.temperature,
11745
+ responseFormat
11746
+ };
11747
+ }
11748
+ async function startGeminiProxy(routes, debug = false) {
11749
+ const proxyToken = randomUUID2();
11750
+ silenceSdkWarnings();
11751
+ if (routes.length === 0) {
11752
+ return Promise.reject(new Error("Gemini proxy requires at least one route"));
11753
+ }
11754
+ const defaultRoute = routes[0];
11755
+ const models = /* @__PURE__ */ new Map();
11756
+ const plog = debug ? makeTraceLogger(getGeminiProxyDebugLogPath()) : () => {
11757
+ };
11758
+ const onRejection = (reason) => {
11759
+ plog(`Unhandled Rejection: ${reason instanceof Error ? reason.stack || reason.message : String(reason)}`);
11760
+ };
11761
+ const onException = (error) => {
11762
+ plog(`Uncaught Exception: ${error.stack || error.message}`);
11763
+ };
11764
+ const getOrInitModel = async (route) => {
11765
+ let m = models.get(route.aliasId);
11766
+ if (!m) {
11767
+ m = await createLanguageModel({
11768
+ npm: route.npm || "@ai-sdk/openai-compatible",
11769
+ modelId: route.realModelId,
11770
+ apiKey: route.apiKey,
11771
+ baseURL: route.baseURL,
11772
+ providerId: route.aliasId,
11773
+ authType: route.authType,
11774
+ oauthAccountId: route.oauthAccountId
11775
+ });
11776
+ models.set(route.aliasId, m);
11777
+ }
11778
+ return m;
11779
+ };
11780
+ const formatGeminiModel = (route) => ({
11781
+ name: `models/${route.aliasId}`,
11782
+ version: "1.0",
11783
+ displayName: route.displayName,
11784
+ description: "Registry model routed through relay-ai proxy",
11785
+ inputTokenLimit: route.contextWindow || 1e6,
11786
+ outputTokenLimit: 8192,
11787
+ supportedGenerationMethods: ["generateContent", "streamGenerateContent"]
11788
+ });
11789
+ let sessionRouteOverride = void 0;
11790
+ const server = createServer4(async (req, res) => {
11791
+ try {
11792
+ const url = req.url ?? "";
11793
+ plog(`${req.method} ${url}`);
11794
+ if (req.method === "GET" && (url.endsWith("/models") || url.includes("/models?"))) {
11795
+ plog("GET models list");
11796
+ res.writeHead(200, { "Content-Type": "application/json" });
11797
+ const payload = JSON.stringify({
11798
+ models: routes.map(formatGeminiModel)
11799
+ });
11800
+ plog(`Response: ${payload}`);
11801
+ res.end(payload);
11802
+ return;
11803
+ }
11804
+ if (req.method === "GET" && url.includes("/models/")) {
11805
+ const modelMatch = url.match(/\/models\/([^?]+)/);
11806
+ if (modelMatch) {
11807
+ const modelId = decodeURIComponent(modelMatch[1]);
11808
+ const route = lookupGeminiRoute(routes, modelId) ?? defaultRoute;
11809
+ plog(`GET model details: ${modelId} -> mapped to route ${route.aliasId}`);
11810
+ res.writeHead(200, { "Content-Type": "application/json" });
11811
+ const payload = JSON.stringify(formatGeminiModel(route));
11812
+ plog(`Response: ${payload}`);
11813
+ res.end(payload);
11814
+ return;
11815
+ }
11816
+ }
11817
+ if (req.method === "POST" && url.includes(":")) {
11818
+ const isStream = url.includes("streamGenerateContent");
11819
+ const rawBody = await readBody(req);
11820
+ plog(`Request body:
11821
+ ${rawBody}`);
11822
+ let body;
11823
+ try {
11824
+ body = JSON.parse(rawBody);
11825
+ } catch {
11826
+ plog("Error: Invalid JSON body");
11827
+ res.writeHead(400);
11828
+ res.end("Invalid JSON");
11829
+ return;
11830
+ }
11831
+ const modelMatch = url.match(/\/models\/([^:]+)/);
11832
+ const requestedModel = modelMatch ? decodeURIComponent(modelMatch[1]) : defaultRoute.aliasId;
11833
+ const lastUserTurn = findLastUserTurn(body.contents || []);
11834
+ const modelCommand = parseModelCommand(lastUserTurn);
11835
+ if (modelCommand !== null) {
11836
+ if (modelCommand === "") {
11837
+ const current = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
11838
+ const availableList = routes.map((r) => ` - ${r.aliasId} (${r.displayName})`).join("\n");
11839
+ const exampleId = routes.length > 1 ? routes[1].aliasId : routes[0]?.aliasId ?? "deepseek-v4";
11840
+ const text5 = `Current model: ${current.displayName} (${current.aliasId})
11841
+
11842
+ Available models:
11843
+ ${availableList}
11844
+
11845
+ \u{1F4A1} To switch models, type: .model <id>
11846
+ Example: .model ${exampleId}`;
11847
+ sendMockGeminiResponse(res, text5, isStream);
11848
+ return;
11849
+ }
11850
+ const targetRoute = lookupGeminiRoute(routes, modelCommand);
11851
+ if (targetRoute) {
11852
+ sessionRouteOverride = targetRoute;
11853
+ plog(`.model switch: ${targetRoute.aliasId} (${targetRoute.realModelId})`);
11854
+ sendMockGeminiResponse(res, `\u2705 Switched model to ${targetRoute.displayName} (${targetRoute.aliasId})`, isStream);
11855
+ } else {
11856
+ const available = routes.map((r) => r.aliasId).join(", ");
11857
+ sendMockGeminiResponse(res, `\u274C Model '${modelCommand}' not found.
11858
+
11859
+ Available: ${available}`, isStream);
11860
+ }
11861
+ return;
11862
+ }
11863
+ const route = sessionRouteOverride ?? (lookupGeminiRoute(routes, requestedModel) ?? defaultRoute);
11864
+ plog(`Route selected: ${route.aliasId} (upstream model: ${route.realModelId})`);
11865
+ body.contents = sanitizeModelSwitchTurns(body.contents || []);
11866
+ const languageModel = await getOrInitModel(route);
11867
+ const params = translateGeminiRequest(body);
11868
+ plog(`Translated SDK params:
11869
+ ${JSON.stringify(params, null, 2)}`);
11870
+ if (isStream) {
11871
+ res.writeHead(200, {
11872
+ "Content-Type": "text/event-stream",
11873
+ "Cache-Control": "no-cache",
11874
+ "Connection": "keep-alive"
11875
+ });
11876
+ plog("Starting streamText...");
11877
+ const { fullStream } = streamText4({
11878
+ model: languageModel,
11879
+ ...params
11880
+ });
11881
+ const toolCallBuffers = /* @__PURE__ */ new Map();
11882
+ let isThinking = false;
11883
+ for await (const part of fullStream) {
11884
+ const p21 = part;
11885
+ plog(`Stream chunk type: ${p21.type}`);
11886
+ if (isThinking && (p21.type === "tool-input-start" || p21.type === "tool-call" || p21.type === "finish")) {
11887
+ isThinking = false;
11888
+ const chunk = {
11889
+ candidates: [{ content: { role: "model", parts: [{ text: `
11890
+ </thinking>
11891
+
11892
+ ` }] } }]
11893
+ };
11894
+ res.write(`data: ${JSON.stringify(chunk)}
11895
+
11896
+ `);
11897
+ }
11898
+ if (p21.type === "reasoning") {
11899
+ let text5 = p21.textDelta ?? p21.text ?? "";
11900
+ if (!isThinking) {
11901
+ isThinking = true;
11902
+ text5 = `<thinking>
11903
+ ` + text5;
11904
+ }
11905
+ const chunk = {
11906
+ candidates: [{ content: { role: "model", parts: [{ text: text5 }] } }]
11907
+ };
11908
+ res.write(`data: ${JSON.stringify(chunk)}
11909
+
11910
+ `);
11911
+ } else if (p21.type === "text-delta") {
11912
+ let text5 = p21.textDelta ?? p21.text ?? "";
11913
+ if (isThinking) {
11914
+ isThinking = false;
11915
+ text5 = `
11916
+ </thinking>
11917
+
11918
+ ` + text5;
11919
+ }
11920
+ const chunk = {
11921
+ candidates: [{
11922
+ content: {
11923
+ role: "model",
11924
+ parts: [{ text: text5 }]
11925
+ }
11926
+ }]
11927
+ };
11928
+ const data = `data: ${JSON.stringify(chunk)}
11929
+
11930
+ `;
11931
+ plog(`Streaming text delta: ${p21.textDelta}`);
11932
+ res.write(data);
11933
+ } else if (p21.type === "tool-input-start") {
11934
+ toolCallBuffers.set(p21.toolCallId, { name: p21.toolName, json: "" });
11935
+ } else if (p21.type === "tool-input-delta") {
11936
+ const buf = toolCallBuffers.get(p21.toolCallId);
11937
+ if (buf) buf.json += p21.delta;
11938
+ } else if (p21.type === "tool-call") {
11939
+ const buf = toolCallBuffers.get(p21.toolCallId);
11940
+ const args = buf ? JSON.parse(buf.json || "{}") : p21.input || {};
11941
+ const name = buf ? buf.name : p21.toolName;
11942
+ plog(`Streaming tool call: ${name} with args: ${JSON.stringify(args)}`);
11943
+ const chunk = {
11944
+ candidates: [{
11945
+ content: {
11946
+ role: "model",
11947
+ parts: [{
11948
+ functionCall: { name, args }
11949
+ }]
11950
+ }
11951
+ }]
11952
+ };
11953
+ res.write(`data: ${JSON.stringify(chunk)}
11954
+
11955
+ `);
11956
+ } else if (p21.type === "finish") {
11957
+ const chunk = {
11958
+ candidates: [{
11959
+ finishReason: mapFinishReason(p21.finishReason ?? "")
11960
+ }],
11961
+ usageMetadata: {
11962
+ promptTokenCount: p21.totalUsage?.inputTokens || 0,
11963
+ candidatesTokenCount: p21.totalUsage?.outputTokens || 0
11964
+ }
11965
+ };
11966
+ plog(`Stream finish. Reason: ${p21.finishReason}`);
11967
+ res.write(`data: ${JSON.stringify(chunk)}
11968
+
11969
+ `);
11970
+ }
11971
+ }
11972
+ res.end();
11973
+ plog("Stream ended.");
11974
+ } else {
11975
+ plog("Starting generateText...");
11976
+ const result = await generateText4({
11977
+ model: languageModel,
11978
+ ...params
11979
+ });
11980
+ plog("generateText finished.");
11981
+ const parts = [];
11982
+ if (result.reasoning) {
11983
+ parts.push({ text: `<thinking>
11984
+ ${result.reasoning}
11985
+ </thinking>
11986
+
11987
+ ` });
11988
+ }
11989
+ if (result.text) {
11990
+ parts.push({ text: result.text });
11991
+ }
11992
+ if (result.toolCalls?.length) {
11993
+ for (const tc of result.toolCalls) {
11994
+ parts.push({
11995
+ functionCall: { name: tc.toolName, args: tc.args }
11996
+ });
11997
+ }
11998
+ }
11999
+ const response = {
12000
+ candidates: [{
12001
+ content: {
12002
+ role: "model",
12003
+ parts
12004
+ },
12005
+ finishReason: mapFinishReason(result.finishReason ?? "")
12006
+ }],
12007
+ usageMetadata: {
12008
+ promptTokenCount: result.usage?.inputTokens || 0,
12009
+ candidatesTokenCount: result.usage?.outputTokens || 0
12010
+ }
12011
+ };
12012
+ plog(`Response:
12013
+ ${JSON.stringify(response, null, 2)}`);
12014
+ res.writeHead(200, { "Content-Type": "application/json" });
12015
+ res.end(JSON.stringify(response));
12016
+ }
12017
+ return;
12018
+ }
12019
+ plog(`404 Not Found: ${url}`);
12020
+ res.writeHead(404);
12021
+ res.end("Not Found");
12022
+ } catch (err) {
12023
+ plog(`Error handling request: ${err instanceof Error ? err.stack || err.message : String(err)}`);
12024
+ if (debug) {
12025
+ console.error("[Gemini Proxy] Critical error in handler:", err);
12026
+ }
12027
+ const errMsg = err instanceof Error ? err.message : String(err);
12028
+ if (!res.headersSent) {
12029
+ res.writeHead(500, { "Content-Type": "application/json" });
12030
+ res.end(JSON.stringify({ error: { message: errMsg } }));
12031
+ } else {
12032
+ try {
12033
+ res.write(`data: ${JSON.stringify({ error: { message: errMsg } })}
12034
+
12035
+ `);
12036
+ } catch {
12037
+ }
12038
+ res.end();
12039
+ }
12040
+ }
12041
+ });
12042
+ process.on("unhandledRejection", onRejection);
12043
+ process.on("uncaughtException", onException);
12044
+ const cleanup = () => {
12045
+ process.off("unhandledRejection", onRejection);
12046
+ process.off("uncaughtException", onException);
12047
+ };
12048
+ return new Promise((resolve, reject2) => {
12049
+ server.on("error", (err) => {
12050
+ cleanup();
12051
+ reject2(err);
12052
+ });
12053
+ server.listen(0, "127.0.0.1", () => {
12054
+ const addr = server.address();
12055
+ if (!addr || typeof addr === "string") {
12056
+ cleanup();
12057
+ reject2(new Error("Failed to bind gemini proxy"));
12058
+ return;
12059
+ }
12060
+ resolve({
12061
+ port: addr.port,
12062
+ token: proxyToken,
12063
+ close: () => {
12064
+ cleanup();
12065
+ server.close();
12066
+ }
12067
+ });
12068
+ });
12069
+ });
12070
+ }
12071
+ function sanitizeModelSwitchTurns(contents) {
12072
+ const cleaned = [];
12073
+ let i = 0;
12074
+ while (i < contents.length) {
12075
+ const turn = contents[i];
12076
+ if (isModelSwitchTurn(turn)) {
12077
+ i += 1;
12078
+ if (i < contents.length && contents[i]?.role === "model") {
12079
+ i += 1;
12080
+ }
12081
+ continue;
12082
+ }
12083
+ cleaned.push(turn);
12084
+ i += 1;
12085
+ }
12086
+ return cleaned;
12087
+ }
12088
+ function isModelSwitchTurn(turn) {
12089
+ if (turn?.role !== "user") return false;
12090
+ const parts = turn.parts || [];
12091
+ if (parts.length === 0) return false;
12092
+ const firstText = parts[0]?.text;
12093
+ if (typeof firstText !== "string") return false;
12094
+ return firstText.trim().startsWith(".model");
12095
+ }
12096
+ function findLastUserTurn(contents) {
12097
+ for (let i = contents.length - 1; i >= 0; i--) {
12098
+ if (contents[i]?.role === "user") return contents[i];
12099
+ }
12100
+ return void 0;
12101
+ }
12102
+ function parseModelCommand(turn) {
12103
+ if (!turn || turn.role !== "user") return null;
12104
+ const parts = turn.parts || [];
12105
+ if (parts.length !== 1) return null;
12106
+ const text5 = parts[0]?.text;
12107
+ if (typeof text5 !== "string") return null;
12108
+ const trimmed = text5.trim();
12109
+ if (!trimmed.startsWith(".model")) return null;
12110
+ if (trimmed === ".model") return "";
12111
+ if (trimmed.charAt(6) !== " ") return null;
12112
+ return trimmed.slice(7).trim();
12113
+ }
12114
+ function sendMockGeminiResponse(res, text5, isStream) {
12115
+ if (isStream) {
12116
+ res.writeHead(200, {
12117
+ "Content-Type": "text/event-stream",
12118
+ "Cache-Control": "no-cache",
12119
+ "Connection": "keep-alive"
12120
+ });
12121
+ const chunk = {
12122
+ candidates: [{
12123
+ content: { role: "model", parts: [{ text: text5 }] },
12124
+ finishReason: "STOP"
12125
+ }],
12126
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12127
+ };
12128
+ res.write(`data: ${JSON.stringify(chunk)}
12129
+
12130
+ `);
12131
+ const finishChunk = {
12132
+ candidates: [{ finishReason: "STOP" }],
12133
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12134
+ };
12135
+ res.write(`data: ${JSON.stringify(finishChunk)}
12136
+
12137
+ `);
12138
+ res.end();
12139
+ } else {
12140
+ res.writeHead(200, { "Content-Type": "application/json" });
12141
+ res.end(JSON.stringify({
12142
+ candidates: [{
12143
+ content: { role: "model", parts: [{ text: text5 }] },
12144
+ finishReason: "STOP"
12145
+ }],
12146
+ usageMetadata: { promptTokenCount: 0, candidatesTokenCount: 0 }
12147
+ }));
12148
+ }
12149
+ }
12150
+
12151
+ // src/gemini.ts
12152
+ function geminiHelpText() {
12153
+ return `${pc14.bold("relay-ai gemini")} v${VERSION}
12154
+ Launch Google Gemini CLI with OpenCode Zen / Go or local registry providers.
12155
+
12156
+ ${pc14.bold("Usage:")}
12157
+ relay-ai gemini [options] [gemini-flags]
12158
+ relay-ai gemini --help
12159
+ relay-ai gemini --version
12160
+
12161
+ ${pc14.bold("Options:")}
12162
+ --trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
12163
+ --provider Boot provider id (skip wizard when paired with --model or non-interactive)
12164
+ --model Boot model id (skip wizard when paired with --provider or non-interactive)
12165
+ --help Show this command help
12166
+ --version Show version
12167
+
12168
+ ${pc14.bold("Description:")}
12169
+ Picks a provider and model from ~/.relay-ai/providers.json, starts a local Gemini-to-SDK translation
12170
+ proxy, and launches the Gemini CLI.
12171
+ All registry models (Anthropic, OpenAI, custom endpoints, etc.) route through the local translation proxy.
12172
+
12173
+ ${pc14.bold("Prerequisites:")}
12174
+ npm install -g @google/gemini-cli
12175
+
12176
+ ${pc14.bold("Passing flags to Gemini CLI:")}
12177
+ Add Gemini flags directly \u2014 no "--" separator needed.
12178
+ relay-ai manages -m / --model and -p / --prompt; other flags go to Gemini CLI.
12179
+
12180
+ ${pc14.bold("Examples:")}
12181
+ relay-ai gemini
12182
+ relay-ai gemini --trace
12183
+ relay-ai gemini --provider zen --model gemini-2.5-flash
12184
+ relay-ai gemini -p "review this file"`;
12185
+ }
12186
+ async function runGeminiCommand(geminiArgs, trace = false, launch = {}) {
12187
+ if (geminiArgs.includes("--help") || geminiArgs.includes("-h")) {
12188
+ console.log(geminiHelpText());
12189
+ return 0;
12190
+ }
12191
+ const geminiPath = findGeminiBinary();
12192
+ if (!geminiPath) {
12193
+ console.error(pc14.red("\nError: gemini binary not found on PATH.\n"));
12194
+ console.error("Install Google Gemini CLI:");
12195
+ console.error(" npm install -g @google/gemini-cli\n");
12196
+ return 1;
12197
+ }
12198
+ const passthroughArgs = rejectGeminiManagedFlags(geminiArgs);
12199
+ const agentStdout = wantsCleanAgentStdout("gemini", passthroughArgs);
12200
+ setAgentStdoutMode(agentStdout);
12201
+ const prefs = loadPreferences();
12202
+ const launchPlan = planLaunchWizard({
12203
+ explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
12204
+ childArgs: passthroughArgs,
12205
+ agent: "gemini",
12206
+ prefs
12207
+ });
12208
+ if (launchPlan.error) {
12209
+ console.error(pc14.red(`
12210
+ Error: ${launchPlan.error}
12211
+ `));
12212
+ return 1;
12213
+ }
12214
+ let catalog;
12215
+ if (agentStdout) {
12216
+ try {
12217
+ catalog = await fetchProviderCatalog({ agent: "gemini" });
12218
+ } catch (err) {
12219
+ console.error(pc14.red(String(err instanceof Error ? err.message : err)));
12220
+ return 1;
12221
+ }
12222
+ } else {
12223
+ const catalogSpinner = p15.spinner();
12224
+ catalogSpinner.start("Loading your providers...");
12225
+ try {
12226
+ catalog = await fetchProviderCatalog({ agent: "gemini" });
12227
+ } catch (err) {
12228
+ catalogSpinner.stop("");
12229
+ console.error(pc14.red(String(err instanceof Error ? err.message : err)));
12230
+ return 1;
12231
+ }
12232
+ catalogSpinner.stop("");
12233
+ }
12234
+ const compatible = providersForPicker(catalog);
12235
+ if (compatible.length === 0) {
12236
+ p15.log.warn("No Gemini-compatible providers in your registry.");
12237
+ p15.log.info("Add a provider with relay-ai providers add, or sign in with relay-ai providers auth openai.");
12238
+ return 0;
12239
+ }
12240
+ let activeProvider = compatible.find((lp) => lp.id === prefs.lastGeminiProvider) ?? compatible[0];
12241
+ let selectedModel = activeProvider.models.find((m) => m.id === prefs.lastGeminiModel) ?? activeProvider.models[0];
12242
+ if (!selectedModel) {
12243
+ p15.log.error(`Provider "${activeProvider.name}" has no models available.`);
12244
+ return 1;
12245
+ }
12246
+ ;
12247
+ if (launchPlan.skip && launchPlan.target) {
12248
+ const resolved = findProviderAndModel(compatible, launchPlan.target);
12249
+ if (!resolved) {
12250
+ p15.log.error(
12251
+ `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
12252
+ );
12253
+ return 1;
12254
+ }
12255
+ activeProvider = resolved.provider;
12256
+ selectedModel = resolved.model;
12257
+ if (!agentStdout) {
12258
+ p15.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12259
+ }
12260
+ } else {
12261
+ if (!agentStdout) {
12262
+ console.log("");
12263
+ p15.log.info(`Launching ${pc14.bold("Gemini CLI")} with relay-ai`);
12264
+ }
12265
+ const chosenProvider = await pickGeminiProvider(
12266
+ compatible,
12267
+ prefs,
12268
+ (prefs.favoriteModels ?? []).length > 0,
12269
+ launch.launchProvider
12270
+ );
12271
+ if (!chosenProvider) return 0;
12272
+ if (chosenProvider === "__favorites__") {
12273
+ const favPick = await pickGeminiFavoriteModel(compatible, prefs.favoriteModels ?? []);
12274
+ if (!favPick || favPick === "back") return 0;
12275
+ activeProvider = favPick.provider;
12276
+ selectedModel = favPick.model;
12277
+ } else {
12278
+ activeProvider = chosenProvider;
12279
+ const chosenModel = await pickGeminiModel(activeProvider, prefs);
12280
+ if (!chosenModel || chosenModel === "back") return 0;
12281
+ selectedModel = chosenModel;
12282
+ }
12283
+ if (!agentStdout) {
12284
+ const ok = await confirmGeminiLaunch(
12285
+ activeProvider.name,
12286
+ selectedModel.name || selectedModel.id,
12287
+ selectedModel.id
12288
+ );
12289
+ if (!ok) return 0;
12290
+ }
12291
+ }
12292
+ recordLaunchSelection("gemini", activeProvider.id, selectedModel.id, prefs);
12293
+ const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
12294
+ if (!launchApiKey?.trim()) {
12295
+ p15.log.error(
12296
+ `No API key found for ${activeProvider.name}. Set it with relay-ai providers add.`
12297
+ );
12298
+ return 1;
12299
+ }
12300
+ const providerRoutes = activeProvider.models.map((m) => ({
12301
+ aliasId: m.id,
12302
+ realModelId: m.upstreamModelId || m.id,
12303
+ displayName: m.name || m.id,
12304
+ upstreamUrl: m.baseUrl || m.apiBaseUrl || "",
12305
+ apiKey: launchApiKey,
12306
+ modelFormat: m.modelFormat,
12307
+ contextWindow: m.contextWindow,
12308
+ npm: m.npm,
12309
+ baseURL: m.apiBaseUrl,
12310
+ providerId: activeProvider.id,
12311
+ authType: activeProvider.authType,
12312
+ oauthAccountId: activeProvider.oauthAccountId,
12313
+ supportedParameters: m.supportedParameters,
12314
+ reasoning: m.reasoning,
12315
+ interleavedReasoningField: m.interleavedReasoningField
12316
+ }));
12317
+ const resolvedFavs = [];
12318
+ const favorites = prefs.favoriteModels ?? [];
12319
+ for (const fav of favorites) {
12320
+ const provider = compatible.find((lp) => lp.id === fav.providerId);
12321
+ const model = provider?.models.find((m) => m.id === fav.modelId);
12322
+ if (provider && model) {
12323
+ const apiKey = await resolveLocalProviderApiKey(provider);
12324
+ if (apiKey) {
12325
+ resolvedFavs.push({
12326
+ aliasId: model.id,
12327
+ realModelId: model.upstreamModelId || model.id,
12328
+ displayName: model.name || model.id,
12329
+ upstreamUrl: model.baseUrl || model.apiBaseUrl || "",
12330
+ apiKey,
12331
+ modelFormat: model.modelFormat,
12332
+ contextWindow: model.contextWindow,
12333
+ npm: model.npm,
12334
+ baseURL: model.apiBaseUrl,
12335
+ providerId: provider.id,
12336
+ authType: provider.authType,
12337
+ oauthAccountId: provider.oauthAccountId,
12338
+ supportedParameters: model.supportedParameters,
12339
+ reasoning: model.reasoning,
12340
+ interleavedReasoningField: model.interleavedReasoningField
12341
+ });
12342
+ }
12343
+ }
12344
+ }
12345
+ const routesMap = /* @__PURE__ */ new Map();
12346
+ for (const route of providerRoutes) {
12347
+ routesMap.set(route.aliasId, route);
12348
+ }
12349
+ for (const route of resolvedFavs) {
12350
+ if (!routesMap.has(route.aliasId)) {
12351
+ routesMap.set(route.aliasId, route);
12352
+ }
12353
+ }
12354
+ const startingRoute = routesMap.get(selectedModel.id);
12355
+ if (!startingRoute) {
12356
+ routesMap.set(selectedModel.id, {
12357
+ aliasId: selectedModel.id,
12358
+ realModelId: selectedModel.upstreamModelId || selectedModel.id,
12359
+ displayName: selectedModel.name || selectedModel.id,
12360
+ upstreamUrl: selectedModel.baseUrl || selectedModel.apiBaseUrl || "",
12361
+ apiKey: launchApiKey,
12362
+ modelFormat: selectedModel.modelFormat,
12363
+ contextWindow: selectedModel.contextWindow,
12364
+ npm: selectedModel.npm,
12365
+ baseURL: selectedModel.apiBaseUrl,
12366
+ providerId: activeProvider.id,
12367
+ authType: activeProvider.authType,
12368
+ oauthAccountId: activeProvider.oauthAccountId,
12369
+ supportedParameters: selectedModel.supportedParameters,
12370
+ reasoning: selectedModel.reasoning,
12371
+ interleavedReasoningField: selectedModel.interleavedReasoningField
12372
+ });
12373
+ }
12374
+ const finalRoutes = [...routesMap.values()];
12375
+ let proxyHandle = null;
12376
+ try {
12377
+ proxyHandle = await startGeminiProxy(finalRoutes, trace);
12378
+ } catch (err) {
12379
+ p15.log.error(`Failed to start Gemini proxy: ${err instanceof Error ? err.message : String(err)}`);
12380
+ return 1;
12381
+ }
12382
+ const childEnv = buildGeminiChildEnv(proxyHandle.port, proxyHandle.token);
12383
+ if (!agentStdout) {
12384
+ p15.log.info(`Gemini proxy started on port ${proxyHandle.port}`);
12385
+ p15.log.info(`\u{1F4A1} Type ${pc14.bold(".model <id>")} in the chat to switch models mid-session.`);
12386
+ }
12387
+ const exitCode = await launchGemini(geminiPath, selectedModel.id, childEnv, passthroughArgs);
12388
+ proxyHandle.close();
12389
+ if (!agentStdout) {
12390
+ p15.log.info("Gemini proxy stopped.");
12391
+ }
12392
+ if (trace) {
12393
+ printTraceLog(getGeminiProxyDebugLogPath());
12394
+ }
12395
+ return exitCode;
12396
+ }
12397
+
12398
+ // src/codex-app.ts
12399
+ import pc15 from "picocolors";
12400
+ import * as p17 from "@clack/prompts";
12401
+
12402
+ // src/codex/app-config.ts
12403
+ import { existsSync as existsSync14, readFileSync as readFileSync11, rmSync as rmSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7 } from "fs";
12404
+ import { dirname as dirname6, join as join15 } from "path";
12405
+ import { parse, stringify } from "smol-toml";
12406
+ function getCodexConfigPath() {
12407
+ return join15(getCodexHome(), "config.toml");
12408
+ }
12409
+ function getCodexAppSidecarProfilePath() {
12410
+ return join15(getCodexHome(), `${CODEX_APP_PROVIDER_ID}.config.toml`);
12411
+ }
12412
+ function asRecord(value) {
12413
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12414
+ }
12415
+ function rootString(config, key) {
12416
+ if (!(key in config)) return { had: false, value: "" };
12417
+ const v = config[key];
12418
+ return { had: true, value: typeof v === "string" ? v : String(v ?? "") };
12419
+ }
12420
+ function readCodexConfigText(path = getCodexConfigPath()) {
12421
+ if (!existsSync14(path)) return "";
12422
+ return readFileSync11(path, "utf8");
12423
+ }
12424
+ function parseCodexConfig(text5) {
12425
+ if (!text5.trim()) return {};
12426
+ return asRecord(parse(text5));
12427
+ }
12428
+ function captureRestoreState(text5) {
12429
+ const config = parseCodexConfig(text5);
12430
+ const profile = rootString(config, "profile");
12431
+ const model = rootString(config, "model");
12432
+ const modelProvider = rootString(config, "model_provider");
12433
+ const modelCatalog = rootString(config, "model_catalog_json");
12434
+ const openAIBaseUrl = rootString(config, "openai_base_url");
12435
+ const reasoning = rootString(config, "model_reasoning_effort");
12436
+ return {
12437
+ hadProfile: profile.had,
12438
+ profile: profile.value,
12439
+ hadModel: model.had,
12440
+ model: model.value,
12441
+ hadModelProvider: modelProvider.had,
12442
+ modelProvider: modelProvider.value,
12443
+ hadModelCatalogJson: modelCatalog.had,
12444
+ modelCatalogJson: modelCatalog.value,
12445
+ hadOpenAIBaseUrl: openAIBaseUrl.had,
12446
+ openAIBaseUrl: openAIBaseUrl.value,
12447
+ hadModelReasoningEffort: reasoning.had,
12448
+ modelReasoningEffort: reasoning.value
12449
+ };
12450
+ }
12451
+ function isAppManagedConfig(text5) {
12452
+ const config = parseCodexConfig(text5);
12453
+ const mp = rootString(config, "model_provider");
12454
+ if (mp.had && mp.value === CODEX_APP_PROVIDER_ID) return true;
12455
+ const baseUrl = rootString(config, "openai_base_url");
12456
+ const catalog = rootString(config, "model_catalog_json");
12457
+ return mp.value === "openai" && /^http:\/\/127\.0\.0\.1:\d+\/v1$/.test(baseUrl.value) && /(?:^|[\\/])app-models-[^\\/]+\.json$/.test(catalog.value);
12458
+ }
12459
+ function mergeAppConfig(existing, spec) {
12460
+ const patch = buildCodexAppRootConfig(spec);
12461
+ const out = { ...existing };
12462
+ delete out.profile;
12463
+ out.model = patch.model;
12464
+ out.model_provider = patch.model_provider;
12465
+ out.openai_base_url = patch.openai_base_url;
12466
+ out.model_catalog_json = patch.model_catalog_json;
12467
+ const providers = asRecord(out.model_providers);
12468
+ delete providers[CODEX_APP_PROVIDER_ID];
12469
+ const profiles = asRecord(out.profiles);
12470
+ delete profiles[CODEX_APP_PROVIDER_ID];
12471
+ if (Object.keys(profiles).length === 0) {
12472
+ delete out.profiles;
12473
+ } else {
12474
+ out.profiles = profiles;
12475
+ }
12476
+ if (Object.keys(providers).length === 0) {
12477
+ delete out.model_providers;
12478
+ } else {
12479
+ out.model_providers = providers;
12480
+ }
12481
+ const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
12482
+ if (existingEffort !== void 0) {
12483
+ const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
12484
+ providerId: spec.route.providerId,
12485
+ apiBaseUrl: spec.route.baseURL,
12486
+ supportedParameters: spec.route.supportedParameters,
12487
+ reasoning: spec.route.reasoning,
12488
+ interleavedReasoningField: spec.route.interleavedReasoningField
12489
+ });
12490
+ if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
12491
+ if (caps.levels.length > 0 && caps.defaultLevel) {
12492
+ out.model_reasoning_effort = caps.defaultLevel;
12493
+ } else {
12494
+ delete out.model_reasoning_effort;
12495
+ }
12496
+ }
12497
+ }
12498
+ return out;
12499
+ }
12500
+ function validateAppConfigText(text5, spec) {
12501
+ const config = parseCodexConfig(text5);
12502
+ if ("profile" in config) {
12503
+ throw new Error("Generated config still contains legacy root profile key");
12504
+ }
12505
+ const profiles = asRecord(config.profiles);
12506
+ if (profiles[CODEX_APP_PROVIDER_ID]) {
12507
+ throw new Error("Generated config still contains legacy profiles table");
12508
+ }
12509
+ const mp = rootString(config, "model_provider");
12510
+ if (mp.value !== "openai") {
12511
+ throw new Error("Generated config must keep the built-in OpenAI model_provider");
12512
+ }
12513
+ const baseUrl = rootString(config, "openai_base_url");
12514
+ if (baseUrl.value !== `http://127.0.0.1:${spec.proxyPort}/v1`) {
12515
+ throw new Error("Generated config openai_base_url mismatch");
12516
+ }
12517
+ const catalog = rootString(config, "model_catalog_json");
11362
12518
  if (catalog.value !== spec.catalogPath) {
11363
12519
  throw new Error("Generated config model_catalog_json mismatch");
11364
12520
  }
@@ -11409,13 +12565,13 @@ function restoreConfigFromState(state, configPath = getCodexConfigPath()) {
11409
12565
  }
11410
12566
  applyRestoreKey(config, "model_reasoning_effort", state.hadModelReasoningEffort, state.modelReasoningEffort);
11411
12567
  const sidecar = getCodexAppSidecarProfilePath();
11412
- if (existsSync13(sidecar)) {
12568
+ if (existsSync14(sidecar)) {
11413
12569
  try {
11414
12570
  rmSync2(sidecar, { force: true });
11415
12571
  } catch {
11416
12572
  }
11417
12573
  }
11418
- const hadFile = existsSync13(configPath);
12574
+ const hadFile = existsSync14(configPath);
11419
12575
  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
12576
  if (!hadFile && empty) return false;
11421
12577
  if (empty) {
@@ -11436,25 +12592,25 @@ function previewAppConfigToml(spec) {
11436
12592
  // src/codex/app-session.ts
11437
12593
  import {
11438
12594
  copyFileSync as copyFileSync4,
11439
- existsSync as existsSync14,
12595
+ existsSync as existsSync15,
11440
12596
  mkdirSync as mkdirSync8,
11441
12597
  readdirSync as readdirSync2,
11442
12598
  readFileSync as readFileSync12,
11443
12599
  rmSync as rmSync3
11444
12600
  } from "fs";
11445
- import { basename as basename2, join as join15 } from "path";
12601
+ import { basename as basename2, join as join16 } from "path";
11446
12602
  function getAppSessionLockPath(env = process.env) {
11447
- return join15(getRelayAiCodexDir(env), "session-app.json");
12603
+ return join16(getRelayAiCodexDir(env), "session-app.json");
11448
12604
  }
11449
12605
  function getAppRestoreStatePath(env = process.env) {
11450
- return join15(getRelayAiCodexDir(env), "app-restore-state.json");
12606
+ return join16(getRelayAiCodexDir(env), "app-restore-state.json");
11451
12607
  }
11452
12608
  function getAppCatalogPath(providerId, env = process.env) {
11453
- return join15(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
12609
+ return join16(getRelayAiCodexDir(env), `app-models-${providerId}.json`);
11454
12610
  }
11455
12611
  function readAppSessionLock(env = process.env) {
11456
12612
  const path = getAppSessionLockPath(env);
11457
- if (!existsSync14(path)) return null;
12613
+ if (!existsSync15(path)) return null;
11458
12614
  try {
11459
12615
  const parsed = JSON.parse(readFileSync12(path, "utf8"));
11460
12616
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
@@ -11468,11 +12624,11 @@ function writeAppSessionLock(lock, env = process.env) {
11468
12624
  }
11469
12625
  function clearAppSessionLock(env = process.env) {
11470
12626
  const path = getAppSessionLockPath(env);
11471
- if (existsSync14(path)) rmSync3(path, { force: true });
12627
+ if (existsSync15(path)) rmSync3(path, { force: true });
11472
12628
  }
11473
12629
  function readAppRestoreState(env = process.env) {
11474
12630
  const path = getAppRestoreStatePath(env);
11475
- if (!existsSync14(path)) return null;
12631
+ if (!existsSync15(path)) return null;
11476
12632
  try {
11477
12633
  return JSON.parse(readFileSync12(path, "utf8"));
11478
12634
  } catch {
@@ -11486,16 +12642,16 @@ function writeAppRestoreState(state, env = process.env) {
11486
12642
  }
11487
12643
  function clearAppRestoreState(env = process.env) {
11488
12644
  const path = getAppRestoreStatePath(env);
11489
- if (existsSync14(path)) rmSync3(path, { force: true });
12645
+ if (existsSync15(path)) rmSync3(path, { force: true });
11490
12646
  }
11491
12647
  function backupConfigToml(env = process.env) {
11492
12648
  const configPath = getCodexConfigPath();
11493
- if (!existsSync14(configPath)) return void 0;
12649
+ if (!existsSync15(configPath)) return void 0;
11494
12650
  rotateBackups(configPath, env);
11495
12651
  const backupsDir = getBackupsDir(env);
11496
12652
  mkdirSync8(backupsDir, { recursive: true });
11497
12653
  const base = basename2(configPath);
11498
- const backupPath = join15(backupsDir, `${base}.${Date.now()}.bak`);
12654
+ const backupPath = join16(backupsDir, `${base}.${Date.now()}.bak`);
11499
12655
  copyFileSync4(configPath, backupPath);
11500
12656
  return backupPath;
11501
12657
  }
@@ -11511,8 +12667,8 @@ function saveAppRestoreStateBeforePatch(env = process.env) {
11511
12667
  }
11512
12668
  function ownedAppCatalogPaths(env = process.env) {
11513
12669
  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));
12670
+ if (!existsSync15(codexDir)) return [];
12671
+ return readdirSync2(codexDir).filter((n) => n.startsWith("app-models-") && n.endsWith(".json")).map((n) => join16(codexDir, n));
11516
12672
  }
11517
12673
  function removeAppCatalogs(env = process.env) {
11518
12674
  const removed = [];
@@ -11544,7 +12700,7 @@ function restoreCodexAppOverlay(env = process.env) {
11544
12700
  }
11545
12701
  if (restoreState) {
11546
12702
  restoreConfigFromState(restoreState);
11547
- } else if (lock?.backupPath && existsSync14(lock.backupPath)) {
12703
+ } else if (lock?.backupPath && existsSync15(lock.backupPath)) {
11548
12704
  copyFileSync4(lock.backupPath, getCodexConfigPath());
11549
12705
  }
11550
12706
  removeAppCatalogs(env);
@@ -11590,11 +12746,11 @@ function waitForShutdown2() {
11590
12746
  }
11591
12747
 
11592
12748
  // 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";
12749
+ import { execSync as execSync5 } from "child_process";
12750
+ import { existsSync as existsSync16, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
12751
+ import { homedir as homedir11 } from "os";
12752
+ import { join as join17 } from "path";
12753
+ import * as p16 from "@clack/prompts";
11598
12754
  var CODEX_BUNDLE_ID = "com.openai.codex";
11599
12755
  function codexAppSupported() {
11600
12756
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -11602,7 +12758,7 @@ function codexAppSupported() {
11602
12758
  }
11603
12759
  }
11604
12760
  function run(cmd, encoding = "utf8") {
11605
- return execSync4(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
12761
+ return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
11606
12762
  }
11607
12763
  function runPowerShell(script) {
11608
12764
  return run(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
@@ -11610,30 +12766,30 @@ function runPowerShell(script) {
11610
12766
  function darwinAppCandidates() {
11611
12767
  return [
11612
12768
  "/Applications/Codex.app",
11613
- join16(homedir10(), "Applications", "Codex.app")
12769
+ join17(homedir11(), "Applications", "Codex.app")
11614
12770
  ];
11615
12771
  }
11616
12772
  function winLocalAppData() {
11617
- return process.env.LOCALAPPDATA ?? join16(homedir10(), "AppData", "Local");
12773
+ return process.env.LOCALAPPDATA ?? join17(homedir11(), "AppData", "Local");
11618
12774
  }
11619
12775
  function winCodexExeCandidates() {
11620
12776
  const local = winLocalAppData();
11621
12777
  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")
12778
+ join17(local, "Programs", "Codex"),
12779
+ join17(local, "Programs", "OpenAI Codex"),
12780
+ join17(local, "Codex"),
12781
+ join17(local, "OpenAI Codex"),
12782
+ join17(local, "OpenAI", "Codex"),
12783
+ join17(local, "openai-codex-electron")
11628
12784
  ];
11629
12785
  const out = [];
11630
12786
  for (const base of bases) {
11631
- out.push(join16(base, "Codex.exe"));
12787
+ out.push(join17(base, "Codex.exe"));
11632
12788
  try {
11633
- if (existsSync15(base)) {
12789
+ if (existsSync16(base)) {
11634
12790
  for (const name of readdirSync3(base)) {
11635
12791
  if (name.startsWith("app-")) {
11636
- out.push(join16(base, name, "Codex.exe"));
12792
+ out.push(join17(base, name, "Codex.exe"));
11637
12793
  }
11638
12794
  }
11639
12795
  }
@@ -11646,7 +12802,7 @@ function mdfindCodexApp() {
11646
12802
  try {
11647
12803
  const out = run(`mdfind "kMDItemCFBundleIdentifier == '${CODEX_BUNDLE_ID}'"`);
11648
12804
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
11649
- return first && existsSync15(first) ? first : null;
12805
+ return first && existsSync16(first) ? first : null;
11650
12806
  } catch {
11651
12807
  return null;
11652
12808
  }
@@ -11654,14 +12810,14 @@ function mdfindCodexApp() {
11654
12810
  function findCodexApp() {
11655
12811
  if (process.platform === "darwin") {
11656
12812
  for (const path of darwinAppCandidates()) {
11657
- if (existsSync15(path)) return path;
12813
+ if (existsSync16(path)) return path;
11658
12814
  }
11659
12815
  return mdfindCodexApp();
11660
12816
  }
11661
12817
  if (process.platform === "win32") {
11662
12818
  for (const path of winCodexExeCandidates()) {
11663
12819
  try {
11664
- if (existsSync15(path) && statSync4(path).isFile()) return path;
12820
+ if (existsSync16(path) && statSync4(path).isFile()) return path;
11665
12821
  } catch {
11666
12822
  }
11667
12823
  }
@@ -11725,9 +12881,9 @@ async function waitForQuit(timeoutMs) {
11725
12881
  function openCodexAppAt(path) {
11726
12882
  if (process.platform === "darwin") {
11727
12883
  if (path.endsWith(".app")) {
11728
- execSync4(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
12884
+ execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
11729
12885
  } else {
11730
- execSync4(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
12886
+ execSync5(`open -b ${CODEX_BUNDLE_ID}`, { stdio: "inherit" });
11731
12887
  }
11732
12888
  return;
11733
12889
  }
@@ -11750,9 +12906,9 @@ function openCodexApp() {
11750
12906
  }
11751
12907
  function darwinQuit() {
11752
12908
  try {
11753
- execSync4(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
12909
+ execSync5(`osascript -e 'tell application "Codex" to quit'`, { stdio: "pipe" });
11754
12910
  } catch {
11755
- execSync4(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
12911
+ execSync5(`osascript -e 'tell application id "${CODEX_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
11756
12912
  }
11757
12913
  }
11758
12914
  function winQuitGraceful() {
@@ -11780,9 +12936,9 @@ async function launchOrRestartCodexApp(prompt = "Restart Codex to apply relay-ai
11780
12936
  openCodexAppAt(appPath);
11781
12937
  return;
11782
12938
  }
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.");
12939
+ const restart = await p16.confirm({ message: prompt, initialValue: true });
12940
+ if (p16.isCancel(restart) || !restart) {
12941
+ p16.log.info("Quit and reopen Codex when you are ready for the new model to take effect.");
11786
12942
  return;
11787
12943
  }
11788
12944
  if (process.platform === "darwin") darwinQuit();
@@ -11800,9 +12956,9 @@ function codexAppInstallHint() {
11800
12956
 
11801
12957
  // src/codex-app.ts
11802
12958
  function codexAppHelpText() {
11803
- return `${pc14.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
12959
+ return `${pc15.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
11804
12960
 
11805
- ${pc14.bold("Usage:")}
12961
+ ${pc15.bold("Usage:")}
11806
12962
  relay-ai codex-app [options]
11807
12963
  relay-ai codex-app --vertex
11808
12964
  relay-ai codex-app --restore
@@ -11810,7 +12966,7 @@ ${pc14.bold("Usage:")}
11810
12966
  relay-ai codex-app --help
11811
12967
  relay-ai codex-app --version
11812
12968
 
11813
- ${pc14.bold("Options:")}
12969
+ ${pc15.bold("Options:")}
11814
12970
  --vertex Use Claude models through Google Vertex AI
11815
12971
  --restore Restore Codex config after an interrupted app session
11816
12972
  --config Preview the generated Codex app configuration without launching
@@ -11818,31 +12974,31 @@ ${pc14.bold("Options:")}
11818
12974
  --help Show this command help
11819
12975
  --version Show version
11820
12976
 
11821
- ${pc14.bold("Description:")}
12977
+ ${pc15.bold("Description:")}
11822
12978
  Picks a provider and model from ~/.relay-ai/providers.json, patches ~/.codex/config.toml
11823
12979
  (with backup + restore on Ctrl+C), starts a local Responses proxy, and opens the
11824
12980
  Codex desktop app. Keep this terminal open while using Codex.
11825
12981
 
11826
- ${pc14.bold("Platforms:")}
12982
+ ${pc15.bold("Platforms:")}
11827
12983
  macOS and Windows. Linux is not supported (no Codex desktop app).
11828
12984
 
11829
- ${pc14.bold("Cleanup:")}
12985
+ ${pc15.bold("Cleanup:")}
11830
12986
  Ctrl+C stops the proxy and restores your previous Codex config.
11831
12987
  After crash: relay-ai codex-app --restore
11832
12988
 
11833
- ${pc14.bold("Preview (no writes):")}
12989
+ ${pc15.bold("Preview (no writes):")}
11834
12990
  relay-ai codex-app --config
11835
12991
 
11836
12992
  See docs/CODEX.md for CLI vs app, files touched, and restore.
11837
12993
 
11838
- ${pc14.bold("Examples:")}
12994
+ ${pc15.bold("Examples:")}
11839
12995
  relay-ai codex-app
11840
12996
  relay-ai codex-app --vertex
11841
12997
  relay-ai codex-app --config
11842
12998
  relay-ai codex-app --restore
11843
12999
 
11844
- ${pc14.bold("Favorites:")}
11845
- When you have saved favorites via ${pc14.cyan("relay-ai models")}, the Codex App
13000
+ ${pc15.bold("Favorites:")}
13001
+ When you have saved favorites via ${pc15.cyan("relay-ai models")}, the Codex App
11846
13002
  picker will show your starting model + favorites for mid-session switching.
11847
13003
  Zen/Go favorites are included when an OpenCode API key is available.`;
11848
13004
  }
@@ -11864,26 +13020,26 @@ function vertexEntryToLocalModel2(entry) {
11864
13020
  }
11865
13021
  async function runCodexAppVertexLaunch(configOnly, trace = false) {
11866
13022
  if (!hasApplicationDefaultCredentials()) {
11867
- p15.log.error("Google Application Default Credentials not found.");
11868
- p15.log.info("Run: gcloud auth application-default login");
13023
+ p17.log.error("Google Application Default Credentials not found.");
13024
+ p17.log.info("Run: gcloud auth application-default login");
11869
13025
  return 1;
11870
13026
  }
11871
13027
  const config = buildVertexRuntimeConfig();
11872
13028
  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");
13029
+ p17.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
13030
+ p17.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
11875
13031
  return 1;
11876
13032
  }
11877
13033
  let selectedEntry;
11878
13034
  if (config.models.length === 1) {
11879
13035
  selectedEntry = config.models[0];
11880
13036
  } else {
11881
- const choice = await p15.select({
13037
+ const choice = await p17.select({
11882
13038
  message: "Select a starting Vertex AI model:",
11883
13039
  options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
11884
13040
  });
11885
- if (p15.isCancel(choice)) {
11886
- p15.cancel("Cancelled.");
13041
+ if (p17.isCancel(choice)) {
13042
+ p17.cancel("Cancelled.");
11887
13043
  return 0;
11888
13044
  }
11889
13045
  selectedEntry = choice;
@@ -11905,19 +13061,19 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11905
13061
  const home = process.env["HOME"] ?? "";
11906
13062
  const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
11907
13063
  console.log("");
11908
- console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
13064
+ console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
11909
13065
  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`);
13066
+ console.log(` ${pc15.bold("Mode:")} Vertex AI`);
13067
+ console.log(` ${pc15.bold("Project:")} ${config.project}`);
13068
+ console.log(` ${pc15.bold("Location:")} ${config.location}`);
13069
+ console.log(` ${pc15.bold("Model:")} ${selectedEntry.display_name}`);
13070
+ console.log(` ${pc15.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
11915
13071
  console.log("");
11916
- console.log(` ${pc14.bold("Catalog file:")}`);
11917
- console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
13072
+ console.log(` ${pc15.bold("Catalog file:")}`);
13073
+ console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
11918
13074
  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."));
13075
+ console.log(pc15.dim(" No app was launched."));
13076
+ console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app --vertex") + pc15.dim(" to launch."));
11921
13077
  console.log("");
11922
13078
  return 0;
11923
13079
  }
@@ -11956,14 +13112,14 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11956
13112
  proxyPort
11957
13113
  });
11958
13114
  sessionActive = true;
11959
- p15.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
13115
+ p17.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11960
13116
  logProxy(proxyPort);
11961
13117
  logActiveModel(selectedEntry.display_name, selectedEntry.id);
11962
13118
  try {
11963
13119
  await launchOrRestartCodexApp();
11964
13120
  } catch (err) {
11965
- p15.log.warn(String(err instanceof Error ? err.message : err));
11966
- p15.log.info(codexAppInstallHint());
13121
+ p17.log.warn(String(err instanceof Error ? err.message : err));
13122
+ p17.log.info(codexAppInstallHint());
11967
13123
  }
11968
13124
  printCodexAppSessionPanel({
11969
13125
  modelLabel: selectedEntry.display_name,
@@ -11979,8 +13135,8 @@ async function runCodexAppVertexLaunch(configOnly, trace = false) {
11979
13135
  sessionActive = false;
11980
13136
  }
11981
13137
  if (isCodexAppRunning()) {
11982
- const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
11983
- if (shouldClose && !p15.isCancel(shouldClose)) {
13138
+ const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13139
+ if (shouldClose && !p17.isCancel(shouldClose)) {
11984
13140
  quitCodexAppGracefully();
11985
13141
  }
11986
13142
  }
@@ -12003,7 +13159,7 @@ async function runCodexAppCommand(args, opts = {}) {
12003
13159
  try {
12004
13160
  codexAppSupported();
12005
13161
  } catch (err) {
12006
- console.error(pc14.red(String(err instanceof Error ? err.message : err)));
13162
+ console.error(pc15.red(String(err instanceof Error ? err.message : err)));
12007
13163
  return 1;
12008
13164
  }
12009
13165
  const interrupted = recoverInterruptedCodexAppSession();
@@ -12011,17 +13167,17 @@ async function runCodexAppCommand(args, opts = {}) {
12011
13167
  const trace = args.includes("--trace");
12012
13168
  const debugLogPath = getCodexProxyDebugLogPath();
12013
13169
  if (trace && !configOnly) {
12014
- p15.log.info(`Debug log: ${debugLogPath}`);
13170
+ p17.log.info(`Debug log: ${debugLogPath}`);
12015
13171
  }
12016
13172
  const isTty = Boolean(process.stdin.isTTY);
12017
13173
  if (!configOnly) {
12018
13174
  const sessionCheck = checkAppSessionLock(isTty);
12019
13175
  if (!sessionCheck.ok) {
12020
13176
  if (sessionCheck.reason === "non_tty") {
12021
- console.error(pc14.red("relay-ai codex-app requires an interactive terminal."));
13177
+ console.error(pc15.red("relay-ai codex-app requires an interactive terminal."));
12022
13178
  return 1;
12023
13179
  }
12024
- console.error(pc14.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
13180
+ console.error(pc15.yellow(`Another relay-ai codex-app session may be running (pid ${sessionCheck.lock.pid}).`));
12025
13181
  console.error("Stop it with Ctrl+C in that terminal, or run relay-ai codex-app --restore after it exits.");
12026
13182
  return 1;
12027
13183
  }
@@ -12029,28 +13185,28 @@ async function runCodexAppCommand(args, opts = {}) {
12029
13185
  if (!configOnly) {
12030
13186
  codexAppIntro();
12031
13187
  if (interrupted.recovered) {
12032
- p15.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
13188
+ p17.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
12033
13189
  }
12034
13190
  }
12035
13191
  if (opts.vertex) {
12036
13192
  return runCodexAppVertexLaunch(configOnly, trace);
12037
13193
  }
12038
- const catalogSpinner = p15.spinner();
13194
+ const catalogSpinner = p17.spinner();
12039
13195
  catalogSpinner.start("Loading your providers...");
12040
13196
  let catalog;
12041
13197
  try {
12042
13198
  catalog = await fetchProviderCatalog({ agent: "codex-app" });
12043
13199
  } catch (err) {
12044
13200
  catalogSpinner.stop("");
12045
- console.error(pc14.red(String(err instanceof Error ? err.message : err)));
13201
+ console.error(pc15.red(String(err instanceof Error ? err.message : err)));
12046
13202
  return 1;
12047
13203
  }
12048
13204
  catalogSpinner.stop("");
12049
13205
  const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
12050
13206
  if (compatible.length === 0) {
12051
13207
  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.");
13208
+ p17.log.warn("No Codex-compatible providers in your registry.");
13209
+ p17.log.info("Add a provider with relay-ai providers add.");
12054
13210
  }
12055
13211
  return 0;
12056
13212
  }
@@ -12058,10 +13214,10 @@ async function runCodexAppCommand(args, opts = {}) {
12058
13214
  const favorites = prefs.favoriteModels ?? [];
12059
13215
  const favoritesActive = favorites.length > 0;
12060
13216
  if (favoritesActive && !configOnly) {
12061
- p15.log.info(
13217
+ p17.log.info(
12062
13218
  `Favorites mode active \u2014 Codex App picker will show ${favorites.length + 1} models (1 starting + ${favorites.length} favorites).`
12063
13219
  );
12064
- p15.log.info("Edit with `relay-ai models`.");
13220
+ p17.log.info("Edit with `relay-ai models`.");
12065
13221
  }
12066
13222
  let activeProvider = providerForCodexPicker(
12067
13223
  compatible.find((lp) => lp.id === prefs.lastCodexProvider) ?? compatible[0]
@@ -12076,12 +13232,12 @@ async function runCodexAppCommand(args, opts = {}) {
12076
13232
  const favoriteProviders = compatible.map(providerForCodexPicker);
12077
13233
  const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
12078
13234
  if (!favoriteStart) {
12079
- p15.log.warn("No saved Codex App favorites are currently available.");
13235
+ p17.log.warn("No saved Codex App favorites are currently available.");
12080
13236
  return 0;
12081
13237
  }
12082
13238
  activeProvider = favoriteStart.provider;
12083
13239
  selectedModel = favoriteStart.model;
12084
- p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13240
+ p17.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12085
13241
  break;
12086
13242
  } else {
12087
13243
  activeProvider = providerForCodexPicker(pickedProvider);
@@ -12101,7 +13257,7 @@ async function runCodexAppCommand(args, opts = {}) {
12101
13257
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
12102
13258
  if (!apiKey) {
12103
13259
  if (!configOnly) {
12104
- p15.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
13260
+ p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
12105
13261
  }
12106
13262
  return 1;
12107
13263
  }
@@ -12161,36 +13317,36 @@ async function runCodexAppCommand(args, opts = {}) {
12161
13317
  const home = process.env["HOME"] ?? "";
12162
13318
  const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
12163
13319
  console.log("");
12164
- console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
13320
+ console.log(pc15.bold(pc15.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app")));
12165
13321
  console.log("");
12166
13322
  if (favoritesActive) {
12167
- console.log(` ${pc14.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
13323
+ console.log(` ${pc15.bold("Mode:")} Favorites Catalog (${resolvedFavorites.length} model${resolvedFavorites.length !== 1 ? "s" : ""})`);
12168
13324
  console.log("");
12169
- console.log(` ${pc14.bold("Models:")}`);
13325
+ console.log(` ${pc15.bold("Models:")}`);
12170
13326
  for (const r of resolvedFavorites) {
12171
- console.log(` ${pc14.cyan(r.model.id)} ${pc14.dim(`(${r.providerName})`)}`);
13327
+ console.log(` ${pc15.cyan(r.model.id)} ${pc15.dim(`(${r.providerName})`)}`);
12172
13328
  }
12173
13329
  } 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`);
13330
+ console.log(` ${pc15.bold("Mode:")} Single model`);
13331
+ console.log(` ${pc15.bold("Provider:")} ${activeProvider.name}`);
13332
+ console.log(` ${pc15.bold("Model:")} ${formatCodexModelLabel(selectedModel)}`);
13333
+ console.log(` ${pc15.bold("Catalog:")} ${routable.length} model${routable.length !== 1 ? "s" : ""} available`);
12178
13334
  }
12179
13335
  console.log("");
12180
- console.log(` ${pc14.bold("config.toml patch preview:")}`);
13336
+ console.log(` ${pc15.bold("config.toml patch preview:")}`);
12181
13337
  const tomlPreview = previewAppConfigToml({
12182
13338
  ...specBase,
12183
13339
  proxyPort: PREVIEW_PROXY_PORT
12184
13340
  });
12185
13341
  for (const line of tomlPreview.split("\n")) {
12186
- console.log(` ${pc14.dim(line)}`);
13342
+ console.log(` ${pc15.dim(line)}`);
12187
13343
  }
12188
13344
  console.log("");
12189
- console.log(` ${pc14.bold("Catalog file:")}`);
12190
- console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
13345
+ console.log(` ${pc15.bold("Catalog file:")}`);
13346
+ console.log(` ${pc15.dim(shortenPath(catalogPath))}`);
12191
13347
  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."));
13348
+ console.log(pc15.dim(" No app was launched."));
13349
+ console.log(pc15.dim(" Run ") + pc15.cyan("relay-ai codex-app") + pc15.dim(" to launch."));
12194
13350
  console.log("");
12195
13351
  return 0;
12196
13352
  }
@@ -12234,8 +13390,8 @@ async function runCodexAppCommand(args, opts = {}) {
12234
13390
  try {
12235
13391
  await launchOrRestartCodexApp();
12236
13392
  } catch (err) {
12237
- p15.log.warn(String(err instanceof Error ? err.message : err));
12238
- p15.log.info(codexAppInstallHint());
13393
+ p17.log.warn(String(err instanceof Error ? err.message : err));
13394
+ p17.log.info(codexAppInstallHint());
12239
13395
  }
12240
13396
  printCodexAppSessionPanel({
12241
13397
  modelLabel,
@@ -12252,8 +13408,8 @@ async function runCodexAppCommand(args, opts = {}) {
12252
13408
  sessionActive = false;
12253
13409
  }
12254
13410
  if (isCodexAppRunning()) {
12255
- const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
12256
- if (shouldClose && !p15.isCancel(shouldClose)) {
13411
+ const shouldClose = await p17.confirm({ message: "Codex Desktop is still running. Close it?" });
13412
+ if (shouldClose && !p17.isCancel(shouldClose)) {
12257
13413
  quitCodexAppGracefully();
12258
13414
  }
12259
13415
  }
@@ -12265,29 +13421,29 @@ async function runCodexAppCommand(args, opts = {}) {
12265
13421
  }
12266
13422
 
12267
13423
  // src/claude-app.ts
12268
- import pc15 from "picocolors";
12269
- import * as p17 from "@clack/prompts";
13424
+ import pc16 from "picocolors";
13425
+ import * as p19 from "@clack/prompts";
12270
13426
 
12271
13427
  // 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";
13428
+ import { existsSync as existsSync17, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
13429
+ import { homedir as homedir12 } from "os";
13430
+ import { join as join18, dirname as dirname7 } from "path";
13431
+ import { randomUUID as randomUUID3 } from "crypto";
12276
13432
  function getClaudeDesktopHome() {
12277
13433
  if (process.platform === "win32") {
12278
- return join17(process.env.APPDATA || join17(homedir11(), "AppData", "Roaming"), "Claude-3p");
13434
+ return join18(process.env.APPDATA || join18(homedir12(), "AppData", "Roaming"), "Claude-3p");
12279
13435
  }
12280
- return join17(homedir11(), "Library", "Application Support", "Claude-3p");
13436
+ return join18(homedir12(), "Library", "Application Support", "Claude-3p");
12281
13437
  }
12282
13438
  function getConfigLibraryPath() {
12283
- return join17(getClaudeDesktopHome(), "configLibrary");
13439
+ return join18(getClaudeDesktopHome(), "configLibrary");
12284
13440
  }
12285
13441
  function getMetaJsonPath() {
12286
- return join17(getConfigLibraryPath(), "_meta.json");
13442
+ return join18(getConfigLibraryPath(), "_meta.json");
12287
13443
  }
12288
13444
  function readMetaJson() {
12289
13445
  const metaPath = getMetaJsonPath();
12290
- if (!existsSync16(metaPath)) return null;
13446
+ if (!existsSync17(metaPath)) return null;
12291
13447
  try {
12292
13448
  return JSON.parse(readFileSync13(metaPath, "utf8"));
12293
13449
  } catch {
@@ -12310,8 +13466,8 @@ function buildRelayAiConfig(proxyPort) {
12310
13466
  };
12311
13467
  }
12312
13468
  function writeRelayAiConfig(proxyPort) {
12313
- const uuid = randomUUID2();
12314
- const configPath = join17(getConfigLibraryPath(), `${uuid}.json`);
13469
+ const uuid = randomUUID3();
13470
+ const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
12315
13471
  const config = buildRelayAiConfig(proxyPort);
12316
13472
  mkdirSync9(dirname7(configPath), { recursive: true });
12317
13473
  writeFileSync7(configPath, `${JSON.stringify(config, null, 2)}
@@ -12326,14 +13482,14 @@ function writeRelayAiConfig(proxyPort) {
12326
13482
  }
12327
13483
 
12328
13484
  // 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";
13485
+ import { existsSync as existsSync18, readFileSync as readFileSync14, rmSync as rmSync4, writeFileSync as writeFileSync8, copyFileSync as copyFileSync5, unlinkSync as unlinkSync3 } from "fs";
13486
+ import { join as join19 } from "path";
12331
13487
  function getSessionLockPath2() {
12332
- return join18(getClaudeDesktopHome(), ".relay-ai.lock");
13488
+ return join19(getClaudeDesktopHome(), ".relay-ai.lock");
12333
13489
  }
12334
13490
  function readSessionLock2() {
12335
13491
  const path = getSessionLockPath2();
12336
- if (!existsSync17(path)) return null;
13492
+ if (!existsSync18(path)) return null;
12337
13493
  try {
12338
13494
  const parsed = JSON.parse(readFileSync14(path, "utf8"));
12339
13495
  if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
@@ -12358,21 +13514,21 @@ function isProcessAlive3(pid) {
12358
13514
  function backupMetaJson() {
12359
13515
  const metaPath = getMetaJsonPath();
12360
13516
  const backupPath = `${metaPath}.bak`;
12361
- if (existsSync17(metaPath)) {
13517
+ if (existsSync18(metaPath)) {
12362
13518
  copyFileSync5(metaPath, backupPath);
12363
13519
  }
12364
13520
  }
12365
13521
  function restoreMetaJson() {
12366
13522
  const metaPath = getMetaJsonPath();
12367
13523
  const backupPath = `${metaPath}.bak`;
12368
- if (existsSync17(backupPath)) {
13524
+ if (existsSync18(backupPath)) {
12369
13525
  copyFileSync5(backupPath, metaPath);
12370
13526
  unlinkSync3(backupPath);
12371
13527
  }
12372
13528
  }
12373
13529
  function removeRelayAiConfig(uuid) {
12374
- const configPath = join18(getConfigLibraryPath(), `${uuid}.json`);
12375
- if (existsSync17(configPath)) {
13530
+ const configPath = join19(getConfigLibraryPath(), `${uuid}.json`);
13531
+ if (existsSync18(configPath)) {
12376
13532
  try {
12377
13533
  rmSync4(configPath, { force: true });
12378
13534
  } catch {
@@ -12436,11 +13592,11 @@ function setupExitCleanup(uuid) {
12436
13592
  }
12437
13593
 
12438
13594
  // 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";
13595
+ import { execSync as execSync6 } from "child_process";
13596
+ import { existsSync as existsSync19, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
13597
+ import { homedir as homedir13 } from "os";
13598
+ import { join as join20 } from "path";
13599
+ import * as p18 from "@clack/prompts";
12444
13600
  var CLAUDE_BUNDLE_ID = "com.anthropic.claudefordesktop";
12445
13601
  function claudeAppSupported() {
12446
13602
  if (process.platform !== "darwin" && process.platform !== "win32") {
@@ -12448,7 +13604,7 @@ function claudeAppSupported() {
12448
13604
  }
12449
13605
  }
12450
13606
  function run2(cmd, encoding = "utf8") {
12451
- return execSync5(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
13607
+ return execSync6(cmd, { encoding, stdio: ["pipe", "pipe", "pipe"] }).trim();
12452
13608
  }
12453
13609
  function runPowerShell2(script) {
12454
13610
  return run2(`powershell.exe -NoProfile -Command ${JSON.stringify(script)}`);
@@ -12456,26 +13612,26 @@ function runPowerShell2(script) {
12456
13612
  function darwinAppCandidates2() {
12457
13613
  return [
12458
13614
  "/Applications/Claude.app",
12459
- join19(homedir12(), "Applications", "Claude.app")
13615
+ join20(homedir13(), "Applications", "Claude.app")
12460
13616
  ];
12461
13617
  }
12462
13618
  function winLocalAppData2() {
12463
- return process.env.LOCALAPPDATA ?? join19(homedir12(), "AppData", "Local");
13619
+ return process.env.LOCALAPPDATA ?? join20(homedir13(), "AppData", "Local");
12464
13620
  }
12465
13621
  function winClaudeExeCandidates() {
12466
13622
  const local = winLocalAppData2();
12467
13623
  const bases = [
12468
- join19(local, "Programs", "Claude"),
12469
- join19(local, "Claude")
13624
+ join20(local, "Programs", "Claude"),
13625
+ join20(local, "Claude")
12470
13626
  ];
12471
13627
  const out = [];
12472
13628
  for (const base of bases) {
12473
- out.push(join19(base, "Claude.exe"));
13629
+ out.push(join20(base, "Claude.exe"));
12474
13630
  try {
12475
- if (existsSync18(base)) {
13631
+ if (existsSync19(base)) {
12476
13632
  for (const name of readdirSync4(base)) {
12477
13633
  if (name.startsWith("app-")) {
12478
- out.push(join19(base, name, "Claude.exe"));
13634
+ out.push(join20(base, name, "Claude.exe"));
12479
13635
  }
12480
13636
  }
12481
13637
  }
@@ -12488,7 +13644,7 @@ function mdfindClaudeApp() {
12488
13644
  try {
12489
13645
  const out = run2(`mdfind "kMDItemCFBundleIdentifier == '${CLAUDE_BUNDLE_ID}'"`);
12490
13646
  const first = out.split("\n").map((l) => l.trim()).find(Boolean);
12491
- return first && existsSync18(first) ? first : null;
13647
+ return first && existsSync19(first) ? first : null;
12492
13648
  } catch {
12493
13649
  return null;
12494
13650
  }
@@ -12496,14 +13652,14 @@ function mdfindClaudeApp() {
12496
13652
  function findClaudeApp() {
12497
13653
  if (process.platform === "darwin") {
12498
13654
  for (const path of darwinAppCandidates2()) {
12499
- if (existsSync18(path)) return path;
13655
+ if (existsSync19(path)) return path;
12500
13656
  }
12501
13657
  return mdfindClaudeApp();
12502
13658
  }
12503
13659
  if (process.platform === "win32") {
12504
13660
  for (const path of winClaudeExeCandidates()) {
12505
13661
  try {
12506
- if (existsSync18(path) && statSync5(path).isFile()) return path;
13662
+ if (existsSync19(path) && statSync5(path).isFile()) return path;
12507
13663
  } catch {
12508
13664
  }
12509
13665
  }
@@ -12567,9 +13723,9 @@ async function waitForQuit2(timeoutMs) {
12567
13723
  function openClaudeAppAt(path) {
12568
13724
  if (process.platform === "darwin") {
12569
13725
  if (path.endsWith(".app")) {
12570
- execSync5(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
13726
+ execSync6(`open ${JSON.stringify(path)}`, { stdio: "inherit" });
12571
13727
  } else {
12572
- execSync5(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
13728
+ execSync6(`open -b ${CLAUDE_BUNDLE_ID}`, { stdio: "inherit" });
12573
13729
  }
12574
13730
  return;
12575
13731
  }
@@ -12592,9 +13748,9 @@ function openClaudeApp() {
12592
13748
  }
12593
13749
  function darwinQuit2() {
12594
13750
  try {
12595
- execSync5(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
13751
+ execSync6(`osascript -e 'tell application "Claude" to quit'`, { stdio: "pipe" });
12596
13752
  } catch {
12597
- execSync5(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
13753
+ execSync6(`osascript -e 'tell application id "${CLAUDE_BUNDLE_ID}" to quit'`, { stdio: "pipe" });
12598
13754
  }
12599
13755
  }
12600
13756
  function winQuitGraceful2() {
@@ -12620,9 +13776,9 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
12620
13776
  openClaudeAppAt(appPath);
12621
13777
  return;
12622
13778
  }
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.");
13779
+ const restart = await p18.confirm({ message: prompt, initialValue: true });
13780
+ if (p18.isCancel(restart) || !restart) {
13781
+ p18.log.info("Quit and reopen Claude Desktop when you are ready for the new model to take effect.");
12626
13782
  return;
12627
13783
  }
12628
13784
  if (process.platform === "darwin") darwinQuit2();
@@ -12637,30 +13793,30 @@ async function launchOrRestartClaudeApp(prompt = "Restart Claude Desktop to appl
12637
13793
 
12638
13794
  // src/claude-app.ts
12639
13795
  function claudeAppHelpText() {
12640
- return `${pc15.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
13796
+ return `${pc16.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
12641
13797
 
12642
- ${pc15.bold("Usage:")}
13798
+ ${pc16.bold("Usage:")}
12643
13799
  relay-ai claude-app [options]
12644
13800
  relay-ai claude-app --trace
12645
13801
  relay-ai claude-app --restore
12646
13802
  relay-ai claude-app --help
12647
13803
  relay-ai claude-app --version
12648
13804
 
12649
- ${pc15.bold("Options:")}
13805
+ ${pc16.bold("Options:")}
12650
13806
  --trace Write proxy debug logs to ~/.relay-ai/logs/
12651
13807
  --restore Restore Claude Desktop config after an interrupted app session
12652
13808
  --help Show this command help
12653
13809
  --version Show version
12654
13810
 
12655
- ${pc15.bold("Description:")}
13811
+ ${pc16.bold("Description:")}
12656
13812
  Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
12657
13813
  (with backup + restore on Ctrl+C), starts a local Responses proxy, and opens
12658
13814
  the Claude Desktop app. Keep this terminal open while using Claude.
12659
13815
 
12660
- ${pc15.bold("Platforms:")}
13816
+ ${pc16.bold("Platforms:")}
12661
13817
  macOS and Windows. Linux is not supported.
12662
13818
 
12663
- ${pc15.bold("Cleanup:")}
13819
+ ${pc16.bold("Cleanup:")}
12664
13820
  Ctrl+C stops the proxy and restores your previous Claude config.
12665
13821
  After a crash: relay-ai claude-app --restore
12666
13822
  `;
@@ -12684,37 +13840,37 @@ async function runClaudeAppCommand(args) {
12684
13840
  try {
12685
13841
  claudeAppSupported();
12686
13842
  } catch (err) {
12687
- console.error(pc15.red(String(err instanceof Error ? err.message : err)));
13843
+ console.error(pc16.red(String(err instanceof Error ? err.message : err)));
12688
13844
  return 1;
12689
13845
  }
12690
13846
  const isTty = Boolean(process.stdin.isTTY);
12691
13847
  if (!isTty) {
12692
- console.error(pc15.red("relay-ai claude-app requires an interactive terminal."));
13848
+ console.error(pc16.red("relay-ai claude-app requires an interactive terminal."));
12693
13849
  return 1;
12694
13850
  }
12695
13851
  if (isConcurrentLiveSession()) {
12696
- console.error(pc15.yellow(`Another relay-ai claude-app session may be running.`));
13852
+ console.error(pc16.yellow(`Another relay-ai claude-app session may be running.`));
12697
13853
  console.error("Stop it with Ctrl+C in that terminal.");
12698
13854
  return 1;
12699
13855
  }
12700
13856
  if (hasStaleSession()) {
12701
- p17.log.warn("Recovered from an interrupted claude-app session.");
13857
+ p19.log.warn("Recovered from an interrupted claude-app session.");
12702
13858
  recoverSession();
12703
13859
  }
12704
- const catalogSpinner = p17.spinner();
13860
+ const catalogSpinner = p19.spinner();
12705
13861
  catalogSpinner.start("Loading your providers...");
12706
13862
  let catalog;
12707
13863
  try {
12708
13864
  catalog = await fetchProviderCatalog({ agent: "codex-app" });
12709
13865
  } catch (err) {
12710
13866
  catalogSpinner.stop("");
12711
- console.error(pc15.red(String(err instanceof Error ? err.message : err)));
13867
+ console.error(pc16.red(String(err instanceof Error ? err.message : err)));
12712
13868
  return 1;
12713
13869
  }
12714
13870
  catalogSpinner.stop("");
12715
13871
  const compatible = codexCompatibleProviders(providersForPicker(catalog), "codex-app");
12716
13872
  if (compatible.length === 0) {
12717
- p17.log.warn("No compatible providers in your registry.");
13873
+ p19.log.warn("No compatible providers in your registry.");
12718
13874
  return 0;
12719
13875
  }
12720
13876
  const prefs = loadPreferences();
@@ -12736,7 +13892,7 @@ async function runClaudeAppCommand(args) {
12736
13892
  const authRef = regEntry?.authRef ?? (activeProvider.apiKey ? `keyring:provider:${activeProvider.id}` : oauthAuthRef(activeProvider.id));
12737
13893
  const apiKey = activeProvider.apiKey?.trim() || await resolveProviderCredential(activeProvider.id, authRef);
12738
13894
  if (!apiKey) {
12739
- p17.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
13895
+ p19.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
12740
13896
  return 1;
12741
13897
  }
12742
13898
  activeProvider.apiKey = apiKey;
@@ -12799,28 +13955,28 @@ async function runClaudeAppCommand(args) {
12799
13955
  });
12800
13956
  }
12801
13957
  console.log(`
12802
- ${pc15.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
13958
+ ${pc16.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
12803
13959
  try {
12804
13960
  await launchOrRestartClaudeApp();
12805
13961
  } catch (err) {
12806
- p17.log.warn(String(err instanceof Error ? err.message : err));
13962
+ p19.log.warn(String(err instanceof Error ? err.message : err));
12807
13963
  }
12808
13964
  console.log(`
12809
- ${pc15.bold("Claude Desktop 3P Mode Active")}`);
13965
+ ${pc16.bold("Claude Desktop 3P Mode Active")}`);
12810
13966
  if (useFavorites) {
12811
- console.log(`${pc15.dim("Catalog:")} Favorite models only`);
13967
+ console.log(`${pc16.dim("Catalog:")} Favorite models only`);
12812
13968
  } else {
12813
- console.log(`${pc15.dim("Model:")} ${selectedModel.id}`);
12814
- console.log(`${pc15.dim("Provider:")} ${activeProvider.name}`);
13969
+ console.log(`${pc16.dim("Model:")} ${selectedModel.id}`);
13970
+ console.log(`${pc16.dim("Provider:")} ${activeProvider.name}`);
12815
13971
  }
12816
- console.log(`${pc15.cyan("Press Ctrl+C to stop and restore config.")}`);
13972
+ console.log(`${pc16.cyan("Press Ctrl+C to stop and restore config.")}`);
12817
13973
  await waitForShutdown3();
12818
13974
  console.log("");
12819
13975
  cleanupSession(uuid);
12820
13976
  sessionActive = false;
12821
13977
  if (isClaudeAppRunning()) {
12822
- const shouldClose = await p17.confirm({ message: "Claude Desktop is still running. Close it?" });
12823
- if (shouldClose && !p17.isCancel(shouldClose)) {
13978
+ const shouldClose = await p19.confirm({ message: "Claude Desktop is still running. Close it?" });
13979
+ if (shouldClose && !p19.isCancel(shouldClose)) {
12824
13980
  quitClaudeAppGracefully();
12825
13981
  }
12826
13982
  }
@@ -12835,17 +13991,17 @@ ${pc15.bold("Claude Desktop 3P Mode Active")}`);
12835
13991
  }
12836
13992
 
12837
13993
  // 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";
13994
+ import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
13995
+ import { homedir as homedir14 } from "os";
13996
+ import { join as join21 } from "path";
12841
13997
  var SKILL_DIR_NAME = "relay-ai-cli";
12842
13998
  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")
13999
+ join21(getAppHome(), "skills"),
14000
+ join21(homedir14(), ".claude", "skills"),
14001
+ join21(homedir14(), ".agents", "skills"),
14002
+ join21(homedir14(), ".codex", "skills"),
14003
+ join21(homedir14(), ".cursor", "skills"),
14004
+ join21(homedir14(), ".cursor", "skills-cursor")
12849
14005
  ];
12850
14006
  function parseSkillVersion(content) {
12851
14007
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -12859,8 +14015,8 @@ function parseSkillVersion(content) {
12859
14015
  return null;
12860
14016
  }
12861
14017
  function readInstalledSkillVersion(skillDir) {
12862
- const skillPath = join20(skillDir, "SKILL.md");
12863
- if (!existsSync19(skillPath)) return null;
14018
+ const skillPath = join21(skillDir, "SKILL.md");
14019
+ if (!existsSync20(skillPath)) return null;
12864
14020
  try {
12865
14021
  const head = readFileSync15(skillPath, "utf-8").slice(0, 1024);
12866
14022
  return parseSkillVersion(head.includes("---", 4) ? head : `${head}
@@ -12872,8 +14028,8 @@ function readInstalledSkillVersion(skillDir) {
12872
14028
  }
12873
14029
  function skillInstallTargets() {
12874
14030
  return SKILL_INSTALL_DIRS.map((dir) => {
12875
- const skillDir = join20(dir, SKILL_DIR_NAME);
12876
- return { skillDir, skillPath: join20(skillDir, "SKILL.md") };
14031
+ const skillDir = join21(dir, SKILL_DIR_NAME);
14032
+ return { skillDir, skillPath: join21(skillDir, "SKILL.md") };
12877
14033
  });
12878
14034
  }
12879
14035
  function formatProviderModels(provider) {
@@ -12886,7 +14042,7 @@ function formatProviderModels(provider) {
12886
14042
  function buildLiveStateSection() {
12887
14043
  const prefs = loadPreferences();
12888
14044
  const registry = loadRegistry();
12889
- const enabled = registry.providers.filter((p19) => p19.enabled);
14045
+ const enabled = registry.providers.filter((p21) => p21.enabled);
12890
14046
  const prefLines = [];
12891
14047
  if (prefs.lastProvider || prefs.lastModel) {
12892
14048
  prefLines.push(` Claude last launch: provider=${prefs.lastProvider ?? "(none)"} model=${prefs.lastModel ?? "(none)"}`);
@@ -12894,15 +14050,18 @@ function buildLiveStateSection() {
12894
14050
  if (prefs.lastCodexProvider || prefs.lastCodexModel) {
12895
14051
  prefLines.push(` Codex last launch: provider=${prefs.lastCodexProvider ?? "(none)"} model=${prefs.lastCodexModel ?? "(none)"}`);
12896
14052
  }
14053
+ if (prefs.lastGeminiProvider || prefs.lastGeminiModel) {
14054
+ prefLines.push(` Gemini last launch: provider=${prefs.lastGeminiProvider ?? "(none)"} model=${prefs.lastGeminiModel ?? "(none)"}`);
14055
+ }
12897
14056
  if (prefs.favoriteModels?.length) {
12898
14057
  prefLines.push(` Favorites (${prefs.favoriteModels.length}/${MAX_MODEL_CATALOG}):`);
12899
14058
  for (const f of prefs.favoriteModels) {
12900
14059
  prefLines.push(` ${f.providerId} / ${f.modelId}`);
12901
14060
  }
12902
14061
  }
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)
14062
+ const providerBlocks = enabled.length === 0 ? [" No registry providers configured. Built-in cloud: zen, go (OpenCode Zen/Go)."] : enabled.map((p21) => [
14063
+ ` ${p21.name} (${p21.id}) \u2014 ${p21.modelsCache?.models.length ?? 0} cached model(s)`,
14064
+ formatProviderModels(p21)
12906
14065
  ].join("\n"));
12907
14066
  return `
12908
14067
  ================================================================================
@@ -12938,8 +14097,8 @@ function staticAiDocBody() {
12938
14097
  RELAY-AI \u2014 AI AGENT REFERENCE (v${VERSION})
12939
14098
  ================================================================================
12940
14099
 
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).
14100
+ relay-ai launches Claude Code, OpenAI Codex, Google Gemini CLI, and desktop apps
14101
+ against YOUR provider registry (Groq, Mistral, OpenAI, Zen/Go, Ollama, custom endpoints, \u2026).
12943
14102
  It handles API translation, local proxies, env isolation, and model routing.
12944
14103
 
12945
14104
  SKILL VERSIONING
@@ -12991,13 +14150,14 @@ PREVIEW LAUNCH WITHOUT STARTING A SESSION:
12991
14150
  INTERACTIVE BROWSE (requires TTY \u2014 avoid in agent scripts):
12992
14151
  relay-ai claude provider + model wizard
12993
14152
  relay-ai codex provider + model wizard
14153
+ relay-ai gemini provider + model wizard
12994
14154
  relay-ai providers provider management hub
12995
14155
 
12996
14156
  ================================================================================
12997
14157
  AGENT PLATFORM PATTERNS \u2014 MULTI-MODEL / ONE-SHOT QUERIES
12998
14158
  ================================================================================
12999
14159
 
13000
- relay-ai is designed so agents can use Claude Code or Codex as a PLATFORM:
14160
+ relay-ai is designed so agents can use Claude Code, Codex, or Gemini CLI as a PLATFORM:
13001
14161
  run many models sequentially or in parallel shell jobs, each with a focused
13002
14162
  prompt, without interactive wizards.
13003
14163
 
@@ -13045,6 +14205,24 @@ OPENAI CODEX \u2014 NON-INTERACTIVE (exec / positional prompt)
13045
14205
  --provider <id>
13046
14206
  --model <id> or provider__model-id slug
13047
14207
 
14208
+ GOOGLE GEMINI CLI \u2014 NON-INTERACTIVE (-p / --prompt)
14209
+ Skips the provider/model wizard when:
14210
+ \u2022 Both --provider and --model are set, OR
14211
+ \u2022 Non-interactive args (-p / --prompt, -i / --prompt-interactive, or positional query)
14212
+ and saved preferences exist
14213
+
14214
+ Examples:
14215
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "Review this file"
14216
+ relay-ai gemini -p "What is the capital of France?"
14217
+
14218
+ Machine-readable stdout:
14219
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o json
14220
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "task" -o stream-json
14221
+
14222
+ Boot flags (relay-ai \u2014 NOT passed to Gemini):
14223
+ --provider <id>
14224
+ --model <id> or provider__model-id slug
14225
+
13048
14226
  MULTI-MODEL LOOP (shell pattern):
13049
14227
  for model in llama-3.3-70b-versatile mixtral-8x7b-32768; do
13050
14228
  relay-ai claude --provider groq --model "$model" -p "Same prompt for all models"
@@ -13054,10 +14232,14 @@ MULTI-MODEL LOOP (shell pattern):
13054
14232
  relay-ai codex --provider zen --model "$model" exec "Same task"
13055
14233
  done
13056
14234
 
14235
+ for model in gemini-2.5-flash gemini-2.5-pro; do
14236
+ relay-ai gemini --provider google --model "$model" -p "Same task"
14237
+ done
14238
+
13057
14239
  FAVORITES / MID-SESSION SWITCHING:
13058
14240
  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
14241
+ When favorites exist, interactive claude/codex/gemini launches expose /model switching.
14242
+ Boot flags (--provider + --model) or print/exec/-p mode use SINGLE-MODEL launch
13061
14243
  (favorites catalog is skipped \u2014 better for agent one-shots).
13062
14244
 
13063
14245
  ================================================================================
@@ -13092,6 +14274,18 @@ CLAUDE CODE
13092
14274
  relay-ai claude --provider anthropic --model claude-sonnet-4-6 -p "review file.ts"
13093
14275
  relay-ai claude --dry-run --provider groq --model llama-3.3-70b-versatile
13094
14276
 
14277
+ GOOGLE GEMINI CLI
14278
+ relay-ai gemini [relay-options] [gemini-flags]
14279
+
14280
+ Relay options:
14281
+ --provider <id> Boot provider (skip wizard with --model)
14282
+ --model <id> Boot model id or provider__model slug
14283
+ --trace Debug logs in ~/.relay-ai/logs/
14284
+
14285
+ Examples:
14286
+ relay-ai gemini
14287
+ relay-ai gemini --provider google --model gemini-2.5-flash -p "What is the capital of France?"
14288
+
13095
14289
  OPENAI CODEX CLI
13096
14290
  relay-ai codex [relay-options] [codex-flags]
13097
14291
 
@@ -13125,7 +14319,7 @@ PROVIDERS REGISTRY
13125
14319
 
13126
14320
  MODELS / FAVORITES
13127
14321
  relay-ai models manage favoriteModels in config (alias: favorites)
13128
- Used for mid-session /model switching in interactive Claude/Codex sessions.
14322
+ Used for mid-session /model switching in interactive Claude/Codex/Gemini sessions.
13129
14323
 
13130
14324
  API GATEWAY (for tools that speak Anthropic/OpenAI HTTP)
13131
14325
  relay-ai server foreground gateway on port 17645
@@ -13161,10 +14355,10 @@ DO:
13161
14355
 
13162
14356
  DO NOT:
13163
14357
  \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
14358
+ \u2022 Pass --provider / --model to Claude, Codex, or Gemini directly \u2014 relay-ai consumes them
13165
14359
  \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 +
14360
+ \u2022 Assume favorites catalog in print/exec/-p mode \u2014 use explicit boot flags
14361
+ \u2022 Mutate settings files (e.g. ~/.claude/settings.json, ~/.codex/config.toml, ~/.gemini/config/config.json) \u2014 relay-ai uses env +
13168
14362
  temporary overlay profiles only
13169
14363
 
13170
14364
  NON-TTY:
@@ -13178,6 +14372,7 @@ TROUBLESHOOTING
13178
14372
  relay-ai codex --restore clean stale overlay after crash
13179
14373
  relay-ai claude --trace proxy + Claude debug logs
13180
14374
  relay-ai codex --trace proxy debug log on exit
14375
+ relay-ai gemini --trace proxy debug log on exit
13181
14376
  relay-ai providers list verify provider ids
13182
14377
  relay-ai providers refresh-models repopulate model cache
13183
14378
 
@@ -13196,7 +14391,7 @@ Human-readable guide: docs/AI-AGENTS.md in the relay-ai repo.
13196
14391
  ALEF AGENT INTEGRATION
13197
14392
  ================================================================================
13198
14393
 
13199
- alef-agent shells out to relay-ai to run Claude Code or Codex against any
14394
+ alef-agent shells out to relay-ai to run Claude Code, Codex, or Gemini CLI against any
13200
14395
  provider in ~/.relay-ai/providers.json. relay-ai is a launcher + proxy; the
13201
14396
  child CLI owns NDJSON/JSONL on stdout.
13202
14397
 
@@ -13253,7 +14448,8 @@ ALEF CHECKLIST
13253
14448
  \u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
13254
14449
  \u25A1 Claude: --output-format stream-json (or json) with -p
13255
14450
  \u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
13256
- \u25A1 Parse stdout only; ignore stderr for JSONL stream
14451
+ \u25A1 Gemini: -o json (or stream-json) with -p
14452
+ \u25A1 Parse stdout only; ignore stderr for JSONL/NDJSON stream
13257
14453
  \u25A1 Zen/Go: --provider zen explicitly + OPENCODE_API_KEY available
13258
14454
  \u25A1 Codex network: default danger-full-access \u2014 no extra -s needed for nlm/curl/npm
13259
14455
  \u25A1 MCP (Claude): --allowed-tools mcp__server__tool on claude args after relay-ai flags
@@ -13442,10 +14638,12 @@ function parseArgs(args) {
13442
14638
  }
13443
14639
  if (first === "providers") {
13444
14640
  const parsed2 = emptyParsed("providers");
13445
- parsed2.claudeArgs = rest;
14641
+ parsed2.claudeArgs = [];
13446
14642
  for (const arg of rest) {
13447
- if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
14643
+ if (arg === "--trace") parsed2.trace = true;
14644
+ else if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
13448
14645
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
14646
+ else parsed2.claudeArgs.push(arg);
13449
14647
  }
13450
14648
  return parsed2;
13451
14649
  }
@@ -13498,6 +14696,32 @@ function parseArgs(args) {
13498
14696
  }
13499
14697
  return parsed2;
13500
14698
  }
14699
+ if (first === "gemini") {
14700
+ const parsed2 = emptyParsed("gemini");
14701
+ for (let i = 0; i < rest.length; i += 1) {
14702
+ const arg = rest[i];
14703
+ if (arg === "--trace") {
14704
+ parsed2.trace = true;
14705
+ continue;
14706
+ }
14707
+ if (arg === "--help" || arg === "-h") {
14708
+ parsed2.showHelp = true;
14709
+ continue;
14710
+ }
14711
+ if (arg === "--version" || arg === "-v") {
14712
+ parsed2.showVersion = true;
14713
+ continue;
14714
+ }
14715
+ const consumed = tryConsumeRelayLaunchFlag(arg, rest, i, parsed2);
14716
+ if (consumed !== null) {
14717
+ if ("error" in consumed) return parsed2;
14718
+ i = consumed.next;
14719
+ continue;
14720
+ }
14721
+ parsed2.claudeArgs.push(arg);
14722
+ }
14723
+ return parsed2;
14724
+ }
13501
14725
  if (first !== "claude") {
13502
14726
  return {
13503
14727
  ...emptyParsed("root"),
@@ -13530,15 +14754,16 @@ function parseArgs(args) {
13530
14754
  return parsed;
13531
14755
  }
13532
14756
  function rootHelpText() {
13533
- return `${pc16.bold("relay-ai")} v${VERSION}
14757
+ return `${pc17.bold("relay-ai")} v${VERSION}
13534
14758
  Launch AI coding tools with OpenCode Zen / Go or local providers (Groq, Mistral,
13535
14759
  OpenAI, Gemini, Ollama, and more).
13536
14760
 
13537
- ${pc16.bold("Usage:")}
14761
+ ${pc17.bold("Usage:")}
13538
14762
  relay-ai claude [options] [claude-flags]
13539
14763
  relay-ai claude-app [options]
13540
14764
  relay-ai codex [options] [codex-flags]
13541
14765
  relay-ai codex-app [options]
14766
+ relay-ai gemini [options] [gemini-flags]
13542
14767
  relay-ai server [options]
13543
14768
  relay-ai models
13544
14769
  relay-ai favorites
@@ -13549,32 +14774,34 @@ ${pc16.bold("Usage:")}
13549
14774
  relay-ai --ai --install Install or upgrade agent skill when version changed
13550
14775
  relay-ai --ai --install --force Reinstall skill even if already current
13551
14776
 
13552
- ${pc16.bold("Root options:")}
14777
+ ${pc17.bold("Root options:")}
13553
14778
  -h, --help Show this help
13554
14779
  -v, --version Show version
13555
14780
  --ai Print the full reference for AI agents
13556
14781
  --ai --install Install or upgrade the relay-ai agent skill
13557
14782
  --force Reinstall the agent skill when used with --ai --install
13558
14783
 
13559
- ${pc16.bold("Commands:")}
14784
+ ${pc17.bold("Commands:")}
13560
14785
  claude Launch Claude Code \u2014 pick a provider from your registry
13561
14786
  models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
13562
14787
  favorites Alias for models
13563
14788
  providers Add, import, and manage your AI providers
13564
14789
  server Run a foreground API gateway (OpenCode Zen / Go and local providers)
13565
14790
  codex Launch OpenAI Codex CLI with registry providers
14791
+ gemini Launch Google Gemini CLI with registry providers
13566
14792
  codex-app Launch Codex desktop app with registry providers (macOS + Windows)
13567
14793
  claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
13568
14794
 
13569
- ${pc16.bold("Migration:")}
14795
+ ${pc17.bold("Migration:")}
13570
14796
  Bare relay-ai prints this help instead of launching Claude Code.
13571
14797
  Use relay-ai claude for the wizard and launcher.
13572
14798
 
13573
- ${pc16.bold("Examples:")}
14799
+ ${pc17.bold("Examples:")}
13574
14800
  relay-ai claude
13575
14801
  relay-ai models
13576
14802
  relay-ai providers
13577
14803
  relay-ai codex
14804
+ relay-ai gemini
13578
14805
  relay-ai codex-app
13579
14806
  relay-ai claude-app
13580
14807
  relay-ai server
@@ -13583,15 +14810,15 @@ ${pc16.bold("Examples:")}
13583
14810
  relay-ai claude -- --print "hello"`;
13584
14811
  }
13585
14812
  function claudeHelpText() {
13586
- return `${pc16.bold("relay-ai claude")} v${VERSION}
14813
+ return `${pc17.bold("relay-ai claude")} v${VERSION}
13587
14814
  Launch Claude Code with OpenCode Zen, Go, or local providers as the API backend.
13588
14815
 
13589
- ${pc16.bold("Usage:")}
14816
+ ${pc17.bold("Usage:")}
13590
14817
  relay-ai claude [options] [claude-flags]
13591
14818
  relay-ai claude --help
13592
14819
  relay-ai claude --version
13593
14820
 
13594
- ${pc16.bold("Options:")}
14821
+ ${pc17.bold("Options:")}
13595
14822
  --dry-run Run the wizard but show a preview instead of launching Claude Code
13596
14823
  --setup Hint: use relay-ai providers to add or manage providers
13597
14824
  --trace Write debug logs to ~/.relay-ai/logs/ and show errors on exit
@@ -13600,22 +14827,22 @@ ${pc16.bold("Options:")}
13600
14827
  --help Show this command help
13601
14828
  --version Show version
13602
14829
 
13603
- ${pc16.bold("Providers:")}
14830
+ ${pc17.bold("Providers:")}
13604
14831
  Cloud (Zen/Go) Requires OPENCODE_API_KEY \u2014 get one at https://opencode.ai/auth
13605
14832
  Registry Configure with relay-ai providers add or import (Groq, Mistral,
13606
14833
  Nvidia, DeepSeek, OpenAI, custom endpoints, etc.).
13607
14834
 
13608
- ${pc16.bold("Model switching:")}
14835
+ ${pc17.bold("Model switching:")}
13609
14836
  Run relay-ai models to save favorites (max ${MAX_MODEL_CATALOG}).
13610
14837
  When favorites exist, launch starts a multi-route proxy and Claude Code /model
13611
14838
  lists your starting model plus favorites for live switching.
13612
14839
  With no favorites, launch uses a single model as before.
13613
14840
 
13614
- ${pc16.bold("Note:")}
14841
+ ${pc17.bold("Note:")}
13615
14842
  Claude Code may save the launched model to ~/.claude/settings.json.
13616
14843
  Bare claude later can still show that model \u2014 reset with claude --model sonnet.
13617
14844
 
13618
- ${pc16.bold("Examples:")}
14845
+ ${pc17.bold("Examples:")}
13619
14846
  relay-ai claude
13620
14847
  relay-ai claude -c
13621
14848
  relay-ai claude --resume abc-123
@@ -13629,54 +14856,54 @@ ${pc16.bold("Examples:")}
13629
14856
  relay-ai claude -- --dangerously-skip-permissions`;
13630
14857
  }
13631
14858
  function serverHelpText() {
13632
- return `${pc16.bold("relay-ai server")} v${VERSION}
14859
+ return `${pc17.bold("relay-ai server")} v${VERSION}
13633
14860
  Run a foreground API gateway for registry providers, Zen/Go, or Vertex AI.
13634
14861
 
13635
- ${pc16.bold("Usage:")}
14862
+ ${pc17.bold("Usage:")}
13636
14863
  relay-ai server
13637
14864
  relay-ai server --vertex
13638
14865
  relay-ai server --help
13639
14866
  relay-ai server --version
13640
14867
 
13641
- ${pc16.bold("Behavior:")}
14868
+ ${pc17.bold("Behavior:")}
13642
14869
  Default: interactive wizard for exposed providers, discovery id masking (for
13643
14870
  Claude Desktop / Cowork), optional favorites-only catalog, then listen mode.
13644
14871
  --vertex: Anthropic-compatible gateway to Claude on Google Vertex AI using
13645
14872
  local gcloud Application Default Credentials (no OpenCode API key).
13646
14873
  Binds to port 17645. Network mode asks for a server password.
13647
14874
 
13648
- ${pc16.bold("Vertex env:")}
14875
+ ${pc17.bold("Vertex env:")}
13649
14876
  ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
13650
14877
  GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
13651
14878
  Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
13652
14879
 
13653
- ${pc16.bold("Endpoints:")}
14880
+ ${pc17.bold("Endpoints:")}
13654
14881
  Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
13655
14882
  OpenAI-compatible: OPENAI_BASE_URL=http://127.0.0.1:17645/openai/v1
13656
14883
  API key: use anything locally; use the server password in network mode.`;
13657
14884
  }
13658
14885
  function modelsHelpText() {
13659
- return `${pc16.bold("relay-ai models")} v${VERSION}
14886
+ return `${pc17.bold("relay-ai models")} v${VERSION}
13660
14887
  Manage favorite models for mid-session switching in Claude Code.
13661
14888
 
13662
- ${pc16.bold("Usage:")}
14889
+ ${pc17.bold("Usage:")}
13663
14890
  relay-ai models
13664
14891
  relay-ai models --help
13665
14892
  relay-ai models --version
13666
14893
 
13667
- ${pc16.bold("Behavior:")}
14894
+ ${pc17.bold("Behavior:")}
13668
14895
  Opens an interactive manager to add or remove favorites.
13669
14896
  Search all providers at once (paginated results) or browse one provider at a time.
13670
14897
  Pick from Zen, Go, or any provider in your registry.
13671
14898
  Favorites are saved to ~/.relay-ai/config.json (max ${MAX_MODEL_CATALOG}).
13672
14899
 
13673
- ${pc16.bold("How it works:")}
14900
+ ${pc17.bold("How it works:")}
13674
14901
  When favorites exist, relay-ai claude starts a multi-route catalog proxy.
13675
14902
  Claude Code /model lists your starting model plus favorites \u2014 switch live
13676
14903
  without restarting. Mix cloud and local favorites in one session.
13677
14904
  With no favorites, launch uses a single model as before.
13678
14905
 
13679
- ${pc16.bold("Examples:")}
14906
+ ${pc17.bold("Examples:")}
13680
14907
  relay-ai models
13681
14908
  relay-ai claude # switch menu active when favorites are set`;
13682
14909
  }
@@ -13689,11 +14916,11 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13689
14916
  let proxyHandle;
13690
14917
  try {
13691
14918
  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)`)
14919
+ p20.log.info(
14920
+ `Switch menu active \u2014 proxy on port ${proxyHandle.port} ` + pc17.dim(`(${catalogRoutes.length} model${catalogRoutes.length !== 1 ? "s" : ""} in /model)`)
13694
14921
  );
13695
14922
  } catch (err) {
13696
- p18.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
14923
+ p20.log.error(`Failed to start proxy: ${err instanceof Error ? err.message : String(err)}`);
13697
14924
  return 1;
13698
14925
  }
13699
14926
  const childEnv = buildChildEnv(
@@ -13706,7 +14933,7 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13706
14933
  );
13707
14934
  const debugLogPath = prepareClaudeTraceLog();
13708
14935
  const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
13709
- if (trace) p18.log.info(`Debug log: ${debugLogPath}`);
14936
+ if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
13710
14937
  const exitCode = await launchClaude(
13711
14938
  childEnv,
13712
14939
  claudeCodeClientModelId(startingRoute.aliasId, contextWindow),
@@ -13718,14 +14945,14 @@ async function launchClaudeViaCatalog(catalogRoutes, startingRoute, contextWindo
13718
14945
  }
13719
14946
  async function runModelsCommand() {
13720
14947
  relayIntro("Favorite Models");
13721
- const spinner9 = p18.spinner();
13722
- spinner9.start("Loading providers...");
14948
+ const spinner10 = p20.spinner();
14949
+ spinner10.start("Loading providers...");
13723
14950
  const catalog = await fetchProviderCatalog();
13724
- spinner9.stop("");
14951
+ spinner10.stop("");
13725
14952
  const allProviders = providersForPicker(catalog);
13726
14953
  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(".")}`);
14954
+ p20.log.warn("No providers found.");
14955
+ p20.log.info(`${pc17.dim("OpenCode Zen/Go is always available. Add providers with ")}${pc17.cyan("relay-ai providers")}${pc17.dim(".")}`);
13729
14956
  relayOutro("Done");
13730
14957
  return 0;
13731
14958
  }
@@ -13743,45 +14970,45 @@ async function runModelsCommand() {
13743
14970
  for (let i = 0; i < favorites.length; i++) {
13744
14971
  const fav = favorites[i];
13745
14972
  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`);
14973
+ const label = entry ? `${fmtEnabledStar(true)} ${fmtModel(entry.modelName)} ${pc17.dim(`(${entry.providerName})`)}` : pc17.dim(`\u2605 ${fav.modelId} \u2014 provider gone`);
13747
14974
  options.push({ value: `fav-${i}`, label, hint: "select to remove" });
13748
14975
  }
13749
14976
  const atCap = favorites.length >= MAX_MODEL_CATALOG;
13750
14977
  options.push({
13751
14978
  value: "__add__",
13752
- label: atCap ? pc16.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc16.cyan("+ Add a model \u2192"),
14979
+ label: atCap ? pc17.dim(`+ Add a model \u2192 (limit of ${MAX_MODEL_CATALOG} reached)`) : pc17.cyan("+ Add a model \u2192"),
13753
14980
  hint: atCap ? "Remove a favorite first to make room" : `${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""} available`
13754
14981
  });
13755
14982
  options.push({ value: "__done__", label: "Done", hint: "" });
13756
14983
  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({
14984
+ const choice = await p20.select({
13758
14985
  message: header,
13759
14986
  options,
13760
14987
  initialValue: "__done__"
13761
14988
  });
13762
- if (p18.isCancel(choice) || choice === "__done__") break;
14989
+ if (p20.isCancel(choice) || choice === "__done__") break;
13763
14990
  if (choice === "__add__") {
13764
14991
  if (atCap) {
13765
- p18.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
14992
+ p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 remove one first.`);
13766
14993
  continue;
13767
14994
  }
13768
14995
  const globalCount = buildGlobalFavoriteIndex(allProviders).length;
13769
- const addPath = await p18.select({
14996
+ const addPath = await p20.select({
13770
14997
  message: "Add a favorite",
13771
14998
  options: [
13772
14999
  {
13773
15000
  value: "global",
13774
- label: pc16.cyan("Search all providers"),
15001
+ label: pc17.cyan("Search all providers"),
13775
15002
  hint: `${globalCount} models \xB7 ${allProviders.length} provider${allProviders.length !== 1 ? "s" : ""}`
13776
15003
  },
13777
15004
  {
13778
15005
  value: "provider",
13779
- label: pc16.cyan("Browse by provider \u2192"),
15006
+ label: pc17.cyan("Browse by provider \u2192"),
13780
15007
  hint: "Pick one provider first"
13781
15008
  }
13782
15009
  ]
13783
15010
  });
13784
- if (p18.isCancel(addPath)) continue;
15011
+ if (p20.isCancel(addPath)) continue;
13785
15012
  let provider;
13786
15013
  let browsedMultiple = [];
13787
15014
  if (addPath === "global") {
@@ -13796,12 +15023,12 @@ async function runModelsCommand() {
13796
15023
  let currentInitialProvider = void 0;
13797
15024
  while (true) {
13798
15025
  const providerOptions = allProviders.map((ap) => providerSelectOption(ap));
13799
- const pickedProviderId = await p18.select({
15026
+ const pickedProviderId = await p20.select({
13800
15027
  message: "Which provider?",
13801
15028
  options: providerOptions,
13802
15029
  initialValue: currentInitialProvider
13803
15030
  });
13804
- if (p18.isCancel(pickedProviderId)) break;
15031
+ if (p20.isCancel(pickedProviderId)) break;
13805
15032
  provider = allProviders.find((ap) => ap.id === pickedProviderId);
13806
15033
  const options2 = provider.models.map((m) => {
13807
15034
  const favorited = isFavorite(favorites, { providerId: provider.id, modelId: m.id });
@@ -13809,15 +15036,15 @@ async function runModelsCommand() {
13809
15036
  return {
13810
15037
  value: m.id,
13811
15038
  label: fmtModel(label, m.id),
13812
- hint: favorited ? pc16.yellow("\u2605 already favorite") : ""
15039
+ hint: favorited ? pc17.yellow("\u2605 already favorite") : ""
13813
15040
  };
13814
15041
  });
13815
- const pickedModelIds = await p18.multiselect({
13816
- message: `Select models to add from ${provider.name} ${pc16.dim("(Space to select, Enter to confirm)")}`,
15042
+ const pickedModelIds = await p20.multiselect({
15043
+ message: `Select models to add from ${provider.name} ${pc17.dim("(Space to select, Enter to confirm)")}`,
13817
15044
  options: options2,
13818
15045
  required: false
13819
15046
  });
13820
- if (p18.isCancel(pickedModelIds)) {
15047
+ if (p20.isCancel(pickedModelIds)) {
13821
15048
  currentInitialProvider = provider.id;
13822
15049
  continue;
13823
15050
  }
@@ -13852,16 +15079,16 @@ async function runModelsCommand() {
13852
15079
  if (addedModels.length > 0) {
13853
15080
  if (addedModels.length === 1) {
13854
15081
  const modelName = addedModels[0].name || addedModels[0].id;
13855
- p18.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
15082
+ p20.log.success(`Added ${modelName} (${provider.name}) to favorites.`);
13856
15083
  } else {
13857
- p18.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
15084
+ p20.log.success(`Added ${addedModels.length} models from ${provider.name} to favorites.`);
13858
15085
  }
13859
15086
  }
13860
15087
  if (duplicateCount > 0) {
13861
- p18.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
15088
+ p20.log.warn(`${duplicateCount} selected model(s) were already in your favorites.`);
13862
15089
  }
13863
15090
  if (limitReached) {
13864
- p18.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
15091
+ p20.log.warn(`Limit of ${MAX_MODEL_CATALOG} favorites reached \u2014 some selected models could not be added.`);
13865
15092
  }
13866
15093
  } else if (choice.startsWith("fav-")) {
13867
15094
  const idx = parseInt(choice.slice(4), 10);
@@ -13870,7 +15097,7 @@ async function runModelsCommand() {
13870
15097
  const label = entry ? `${entry.modelName} (${entry.providerName})` : fav.modelId;
13871
15098
  favorites = removeFavorite(favorites, fav);
13872
15099
  favoritesDirty = true;
13873
- p18.log.success(`Removed ${label} from favorites.`);
15100
+ p20.log.success(`Removed ${label} from favorites.`);
13874
15101
  }
13875
15102
  }
13876
15103
  if (favoritesDirty) {
@@ -13878,7 +15105,7 @@ async function runModelsCommand() {
13878
15105
  }
13879
15106
  relayOutro(
13880
15107
  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")
15108
+ favorites.length === 0 ? pc17.dim("Launch uses single-model mode") : pc17.cyan("/model menu ready on next launch")
13882
15109
  );
13883
15110
  return 0;
13884
15111
  }
@@ -13889,7 +15116,7 @@ async function runClaudeCommand(parsed) {
13889
15116
  setAgentStdoutMode(agentStdout);
13890
15117
  const claudePath = findClaudeBinary();
13891
15118
  if (!claudePath) {
13892
- console.error(pc16.red("\nError: claude binary not found on PATH.\n"));
15119
+ console.error(pc17.red("\nError: claude binary not found on PATH.\n"));
13893
15120
  console.error("Install Claude Code:");
13894
15121
  console.error(" npm install -g @anthropic-ai/claude-code\n");
13895
15122
  return 1;
@@ -13904,7 +15131,7 @@ async function runClaudeCommand(parsed) {
13904
15131
  prefs
13905
15132
  });
13906
15133
  if (launchPlan.error) {
13907
- console.error(pc16.red(`
15134
+ console.error(pc17.red(`
13908
15135
  Error: ${launchPlan.error}
13909
15136
  `));
13910
15137
  return 1;
@@ -13912,7 +15139,7 @@ Error: ${launchPlan.error}
13912
15139
  const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
13913
15140
  if (!agentStdout) relayIntro("Claude Code");
13914
15141
  if (setup && !dryRun && !agentStdout) {
13915
- p18.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
15142
+ p20.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
13916
15143
  }
13917
15144
  if (!dryRun && await needsFirstRunSetup()) {
13918
15145
  const firstRun = await runFirstRunWizard(trace);
@@ -13923,25 +15150,25 @@ Error: ${launchPlan.error}
13923
15150
  try {
13924
15151
  catalog = await fetchProviderCatalog();
13925
15152
  } catch (err) {
13926
- console.error(pc16.red(String(err instanceof Error ? err.message : err)));
15153
+ console.error(pc17.red(String(err instanceof Error ? err.message : err)));
13927
15154
  return 1;
13928
15155
  }
13929
15156
  } else {
13930
- const catalogSpinner = p18.spinner();
15157
+ const catalogSpinner = p20.spinner();
13931
15158
  catalogSpinner.start("Loading your providers...");
13932
15159
  try {
13933
15160
  catalog = await fetchProviderCatalog();
13934
15161
  } catch (err) {
13935
15162
  catalogSpinner.stop("");
13936
- console.error(pc16.red(String(err instanceof Error ? err.message : err)));
15163
+ console.error(pc17.red(String(err instanceof Error ? err.message : err)));
13937
15164
  return 1;
13938
15165
  }
13939
15166
  catalogSpinner.stop("");
13940
15167
  }
13941
15168
  const allProviders = providersForPicker(catalog);
13942
15169
  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."));
15170
+ p20.log.warn("No providers available.");
15171
+ p20.log.info(pc17.dim("Run relay-ai providers add or import to get started."));
13945
15172
  return 0;
13946
15173
  }
13947
15174
  const providerOptions = allProviders.map((lp) => providerSelectOption(lp));
@@ -13958,7 +15185,7 @@ Error: ${launchPlan.error}
13958
15185
  if (launchPlan.skip && launchPlan.target) {
13959
15186
  const resolved = findProviderAndModel(allProviders, launchPlan.target);
13960
15187
  if (!resolved) {
13961
- p18.log.error(
15188
+ p20.log.error(
13962
15189
  `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
13963
15190
  );
13964
15191
  return 1;
@@ -13966,31 +15193,31 @@ Error: ${launchPlan.error}
13966
15193
  activeProvider = resolved.provider;
13967
15194
  selectedModel = resolved.model;
13968
15195
  if (!agentStdout) {
13969
- p18.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
15196
+ p20.log.step(`Using ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13970
15197
  }
13971
15198
  if (!dryRun) recordLaunchSelection("claude", activeProvider.id, selectedModel.id, prefs);
13972
15199
  } else {
13973
15200
  let currentInitialProvider = initialProvider;
13974
15201
  while (true) {
13975
- const chosen = await p18.select({
15202
+ const chosen = await p20.select({
13976
15203
  message: "Which provider?",
13977
15204
  options: providerOptions,
13978
15205
  initialValue: currentInitialProvider
13979
15206
  });
13980
- if (p18.isCancel(chosen)) {
13981
- p18.cancel("Cancelled.");
15207
+ if (p20.isCancel(chosen)) {
15208
+ p20.cancel("Cancelled.");
13982
15209
  return 0;
13983
15210
  }
13984
15211
  const providerChoice = chosen;
13985
15212
  if (providerChoice === "__favorites__") {
13986
15213
  const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
13987
15214
  if (!favoriteStart) {
13988
- p18.log.warn("No saved favorites are currently available.");
15215
+ p20.log.warn("No saved favorites are currently available.");
13989
15216
  return 0;
13990
15217
  }
13991
15218
  activeProvider = favoriteStart.provider;
13992
15219
  selectedModel = favoriteStart.model;
13993
- p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
15220
+ p20.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
13994
15221
  break;
13995
15222
  } else {
13996
15223
  activeProvider = allProviders.find((lp) => lp.id === providerChoice);
@@ -14017,27 +15244,27 @@ Error: ${launchPlan.error}
14017
15244
  );
14018
15245
  const startingRoute = resolveRoute(activeProvider.id, selectedModel.id) ?? null;
14019
15246
  if (!startingRoute) {
14020
- p18.log.error("Could not resolve a proxy route for the selected model.");
15247
+ p20.log.error("Could not resolve a proxy route for the selected model.");
14021
15248
  return 1;
14022
15249
  }
14023
15250
  const { routes: catalogRoutes, droppedFavorites } = buildCatalogRoutes(startingRoute, favorites, resolveRoute);
14024
15251
  if (droppedFavorites.length > 0) {
14025
- p18.log.warn(
15252
+ p20.log.warn(
14026
15253
  `Skipping ${droppedFavorites.length} favorite${droppedFavorites.length === 1 ? "" : "s"} that are no longer available in /model`
14027
15254
  );
14028
15255
  }
14029
15256
  if (dryRun) {
14030
15257
  const endpoint = selectedModel.baseUrl ?? selectedModel.completionsUrl ?? "(unknown)";
14031
15258
  console.log("");
14032
- console.log(pc16.bold(pc16.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
15259
+ console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute (switch-menu mode):")));
14033
15260
  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)}`));
15261
+ console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
15262
+ console.log(` ${pc17.bold("Starting model:")} ${selectedModel.id}`);
15263
+ console.log(` ${pc17.bold("Endpoint:")} ${endpoint}`);
15264
+ console.log(` ${pc17.bold("/model catalog:")} ${catalogRoutes.length} model(s)`);
15265
+ catalogRoutes.forEach((r) => console.log(` ${pc17.dim(r.displayName)}`));
14039
15266
  console.log("");
14040
- console.log(pc16.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
15267
+ console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
14041
15268
  console.log("");
14042
15269
  return 0;
14043
15270
  }
@@ -14053,21 +15280,21 @@ Error: ${launchPlan.error}
14053
15280
  const formatDesc = selectedModel.modelFormat === "anthropic" ? "direct passthrough" : "via SDK adapter proxy";
14054
15281
  const endpoint = selectedModel.modelFormat === "anthropic" ? selectedModel.baseUrl ?? "(unknown)" : selectedModel.npm ?? "SDK";
14055
15282
  console.log("");
14056
- console.log(pc16.bold(pc16.cyan(" DRY RUN \u2014 would execute:")));
15283
+ console.log(pc17.bold(pc17.cyan(" DRY RUN \u2014 would execute:")));
14057
15284
  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`);
15285
+ console.log(` ${pc17.bold("Provider:")} ${activeProvider.name}`);
15286
+ console.log(` ${pc17.bold("Model:")} ${selectedModel.id}`);
15287
+ console.log(` ${pc17.bold("Format:")} ${selectedModel.modelFormat} (${formatDesc})`);
15288
+ console.log(` ${pc17.bold(selectedModel.modelFormat === "anthropic" ? "Endpoint:" : "SDK npm:")} ${endpoint}`);
15289
+ console.log(` ${pc17.bold("Key:")} ${activeProvider.name} provider key`);
14063
15290
  console.log("");
14064
- console.log(pc16.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
15291
+ console.log(pc17.dim(" (dry run complete \u2014 Claude Code was NOT launched)"));
14065
15292
  console.log("");
14066
15293
  return 0;
14067
15294
  }
14068
15295
  const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
14069
15296
  if (!launchApiKey?.trim()) {
14070
- p18.log.error(
15297
+ p20.log.error(
14071
15298
  `No credential found for ${activeProvider.name}. Add a key with relay-ai providers or set OPENCODE_API_KEY.`
14072
15299
  );
14073
15300
  return 1;
@@ -14103,12 +15330,12 @@ Error: ${launchPlan.error}
14103
15330
  launchApiKey
14104
15331
  );
14105
15332
  if (!isAgentStdoutMode()) {
14106
- p18.log.info(
14107
- `SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc16.dim(` (${selectedModel.npm})`) : "")
15333
+ p20.log.info(
15334
+ `SDK adapter proxy started on port ${proxyHandle.port}` + (selectedModel.npm ? pc17.dim(` (${selectedModel.npm})`) : "")
14108
15335
  );
14109
15336
  }
14110
15337
  } catch (err) {
14111
- p18.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
15338
+ p20.log.error(`Failed to start SDK adapter proxy: ${err instanceof Error ? err.message : String(err)}`);
14112
15339
  return 1;
14113
15340
  }
14114
15341
  childEnv = buildChildEnv(
@@ -14124,7 +15351,7 @@ Error: ${launchPlan.error}
14124
15351
  }
14125
15352
  const debugLogPath = prepareClaudeTraceLog();
14126
15353
  const traceArgs = trace ? ["--debug-file", debugLogPath] : [];
14127
- if (trace) p18.log.info(`Debug log: ${debugLogPath}`);
15354
+ if (trace) p20.log.info(`Debug log: ${debugLogPath}`);
14128
15355
  const exitCode = await launchClaude(
14129
15356
  childEnv,
14130
15357
  claudeCodeClientModelId(selectedModel.id, selectedModel.contextWindow),
@@ -14137,7 +15364,7 @@ Error: ${launchPlan.error}
14137
15364
  async function main(args = process.argv.slice(2)) {
14138
15365
  const parsed = parseArgs(args);
14139
15366
  if (parsed.error) {
14140
- console.error(pc16.red(`
15367
+ console.error(pc17.red(`
14141
15368
  Error: ${parsed.error}
14142
15369
  `));
14143
15370
  printHelp(rootHelpText());
@@ -14192,6 +15419,9 @@ Error: ${parsed.error}
14192
15419
  printHelp(providersHelpText());
14193
15420
  return 0;
14194
15421
  }
15422
+ if (parsed.trace) {
15423
+ process.env.RELAY_AI_TRACE = "1";
15424
+ }
14195
15425
  return runProvidersCommand(parsed.claudeArgs);
14196
15426
  }
14197
15427
  if (parsed.command === "codex-app") {
@@ -14223,6 +15453,20 @@ Error: ${parsed.error}
14223
15453
  vertex: parsed.vertex
14224
15454
  });
14225
15455
  }
15456
+ if (parsed.command === "gemini") {
15457
+ if (parsed.showVersion) {
15458
+ console.log(VERSION);
15459
+ return 0;
15460
+ }
15461
+ if (parsed.showHelp) {
15462
+ console.log(geminiHelpText());
15463
+ return 0;
15464
+ }
15465
+ return runGeminiCommand(parsed.claudeArgs, parsed.trace, {
15466
+ launchProvider: parsed.launchProvider,
15467
+ launchModel: parsed.launchModel
15468
+ });
15469
+ }
14226
15470
  if (parsed.showVersion) {
14227
15471
  console.log(VERSION);
14228
15472
  return 0;
@@ -14248,7 +15492,7 @@ if (isCliEntryPoint()) {
14248
15492
  if (err === /* @__PURE__ */ Symbol.for("clack:cancel")) {
14249
15493
  process.exit(0);
14250
15494
  }
14251
- console.error(pc16.red("\nUnexpected error:"), err);
15495
+ console.error(pc17.red("\nUnexpected error:"), err);
14252
15496
  process.exit(1);
14253
15497
  });
14254
15498
  }