@jacobbd/relay-ai 0.6.2 → 0.6.3

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
@@ -47,7 +47,6 @@ import {
47
47
  fetchAnthropicModels,
48
48
  fetchProviderCatalog,
49
49
  fetchTemplateModels,
50
- filterServerModelsByFavorites,
51
50
  findBinaryOnPath,
52
51
  findClaudeBinary,
53
52
  fmtCommand,
@@ -87,7 +86,6 @@ import {
87
86
  listCredentialSkippedProviders,
88
87
  loadPreferences,
89
88
  loadRegistry,
90
- loadServerModels,
91
89
  logActiveModel,
92
90
  logConnected,
93
91
  logProxy,
@@ -167,7 +165,7 @@ import {
167
165
  validateCustomEndpointUrl,
168
166
  writeSecureLogLine,
169
167
  zenRegistryStub
170
- } from "./chunk-I3I5LKZI.js";
168
+ } from "./chunk-WHEMJ7C3.js";
171
169
  import {
172
170
  filterTemplates,
173
171
  init_provider_templates,
@@ -4305,6 +4303,14 @@ async function buildFavoritesList(starting, favorites, ctx, max = 20, options =
4305
4303
  }
4306
4304
  return { resolved: out, droppedFavorites, capacitySkippedFavorites };
4307
4305
  }
4306
+ function resolveFirstAvailableFavorite(favorites, providers) {
4307
+ for (const fav of favorites) {
4308
+ const provider = providers.find((lp) => lp.id === fav.providerId);
4309
+ const model = provider?.models.find((m) => m.id === fav.modelId);
4310
+ if (provider && model) return { provider, model };
4311
+ }
4312
+ return void 0;
4313
+ }
4308
4314
 
4309
4315
  // src/codex/favorites-launch.ts
4310
4316
  var identityProvider = (provider) => provider;
@@ -4627,6 +4633,9 @@ function planLaunchWizard(opts) {
4627
4633
  }
4628
4634
  return { skip: false, target: null };
4629
4635
  }
4636
+ function launchAllowsNonTty(plan, bypassWizard = false) {
4637
+ return bypassWizard || !!(plan.skip && plan.target);
4638
+ }
4630
4639
  function nonInteractiveLaunchError(agent) {
4631
4640
  if (agent === "claude") return "Print mode requires --provider and --model, or saved preferences from a prior launch.";
4632
4641
  if (agent === "codex") return "Non-interactive Codex launch requires --provider and --model, or saved preferences from a prior launch.";
@@ -10569,26 +10578,187 @@ function writeRelayAiConfig(proxyPort) {
10569
10578
  return uuid;
10570
10579
  }
10571
10580
 
10581
+ // src/claude-desktop/model-catalog.ts
10582
+ async function resolveClaudeAppCatalog(selectedProvider, selectedModel, compatibleProviders, favorites, max = MAX_MODEL_CATALOG) {
10583
+ const providersById = new Map(
10584
+ compatibleProviders.map((provider) => [provider.id, provider])
10585
+ );
10586
+ const context = {
10587
+ agent: "codex-app",
10588
+ localProviders: compatibleProviders,
10589
+ findLocalModel: (providerId, modelId) => {
10590
+ const provider = providersById.get(providerId);
10591
+ const model = provider?.models.find((candidate) => candidate.id === modelId);
10592
+ return provider && model ? { provider, model } : void 0;
10593
+ }
10594
+ };
10595
+ const starting = await resolveFavorite(
10596
+ { providerId: selectedProvider.id, modelId: selectedModel.id },
10597
+ context
10598
+ );
10599
+ if (!starting) {
10600
+ return {
10601
+ ok: false,
10602
+ error: `Model ${selectedModel.id} is no longer available on ${selectedProvider.name}.`
10603
+ };
10604
+ }
10605
+ if (!starting.apiKey.trim()) {
10606
+ return {
10607
+ ok: false,
10608
+ error: `No credential for ${selectedProvider.name}. Run relay-ai providers auth ${selectedProvider.id}.`
10609
+ };
10610
+ }
10611
+ const {
10612
+ resolved,
10613
+ droppedFavorites,
10614
+ capacitySkippedFavorites
10615
+ } = await buildFavoritesList(starting, favorites, context, max, {
10616
+ dropEmptyApiKey: true,
10617
+ trackCapacitySkipped: true
10618
+ });
10619
+ return {
10620
+ ok: true,
10621
+ entries: resolved,
10622
+ providersById,
10623
+ droppedFavorites,
10624
+ capacitySkippedFavorites
10625
+ };
10626
+ }
10627
+ function modelToServerModelInfo(model, provider, overrides = {}) {
10628
+ return {
10629
+ id: model.id,
10630
+ name: model.name,
10631
+ isFree: model.isFree ?? false,
10632
+ freeStatus: model.freeStatus,
10633
+ brand: model.brand ?? "",
10634
+ providerLabel: provider.name,
10635
+ providerId: provider.id,
10636
+ sourceBackend: provider.id,
10637
+ modelFormat: model.modelFormat,
10638
+ upstreamModelId: model.upstreamModelId,
10639
+ cost: model.cost,
10640
+ baseUrl: model.baseUrl,
10641
+ completionsUrl: model.completionsUrl,
10642
+ npm: model.npm,
10643
+ apiBaseUrl: model.apiBaseUrl,
10644
+ apiKey: provider.apiKey,
10645
+ authType: provider.authType,
10646
+ oauthAccountId: provider.oauthAccountId,
10647
+ contextWindow: model.contextWindow,
10648
+ supportedParameters: model.supportedParameters,
10649
+ reasoning: model.reasoning,
10650
+ interleavedReasoningField: model.interleavedReasoningField,
10651
+ useResponsesLite: model.useResponsesLite,
10652
+ preferWebSockets: model.preferWebSockets,
10653
+ headers: provider.headers,
10654
+ providerData: provider.providerData,
10655
+ ...overrides
10656
+ };
10657
+ }
10658
+ function entryKey(entry) {
10659
+ return `${entry.providerId}::${entry.model.id}`;
10660
+ }
10661
+ async function buildClaudeAppServerCatalog(entries, providersById, trace) {
10662
+ const convertedByKey = /* @__PURE__ */ new Map();
10663
+ const cloudCodeEntries = entries.filter((entry) => entry.model.modelFormat === "cloud-code");
10664
+ const regularEntries = entries.filter((entry) => entry.model.modelFormat !== "cloud-code");
10665
+ for (const entry of regularEntries) {
10666
+ const provider = providersById.get(entry.providerId);
10667
+ if (!provider) {
10668
+ throw new Error(`Internal error: provider ${entry.providerId} is missing from the Claude App catalog.`);
10669
+ }
10670
+ const resolvedProvider = { ...provider, apiKey: entry.apiKey };
10671
+ convertedByKey.set(
10672
+ entryKey(entry),
10673
+ modelToServerModelInfo(entry.model, resolvedProvider)
10674
+ );
10675
+ }
10676
+ const { backendItems, backend } = await partitionAndStartCloudCodeBackend(
10677
+ cloudCodeEntries.map((entry) => ({
10678
+ providerId: entry.providerId,
10679
+ model: entry.model,
10680
+ apiKey: entry.apiKey,
10681
+ providerData: entry.providerData,
10682
+ entry
10683
+ })),
10684
+ (proxyRoute, cloudCodeBackend, original) => {
10685
+ const provider = providersById.get(original.providerId);
10686
+ if (!provider) {
10687
+ throw new Error(`Internal error: provider ${original.providerId} is missing from the Claude App catalog.`);
10688
+ }
10689
+ const converted = modelToServerModelInfo(original.model, {
10690
+ ...provider,
10691
+ apiKey: original.apiKey
10692
+ }, {
10693
+ modelFormat: "anthropic",
10694
+ upstreamModelId: proxyRoute.aliasId,
10695
+ baseUrl: `http://127.0.0.1:${cloudCodeBackend.port}`,
10696
+ completionsUrl: void 0,
10697
+ npm: void 0,
10698
+ apiBaseUrl: void 0,
10699
+ apiKey: cloudCodeBackend.token,
10700
+ authType: void 0,
10701
+ oauthAccountId: void 0,
10702
+ headers: void 0
10703
+ });
10704
+ return { key: entryKey(original.entry), converted };
10705
+ },
10706
+ trace
10707
+ );
10708
+ for (const item of backendItems) {
10709
+ convertedByKey.set(item.key, item.converted);
10710
+ }
10711
+ const serverModels = entries.map((entry) => {
10712
+ const converted = convertedByKey.get(entryKey(entry));
10713
+ if (!converted) {
10714
+ throw new Error(`Internal error: model ${entry.providerId}/${entry.model.id} was not converted for Claude App.`);
10715
+ }
10716
+ return converted;
10717
+ });
10718
+ return { serverModels, backend };
10719
+ }
10720
+
10572
10721
  // src/claude-desktop/app-session.ts
10573
- import { existsSync as existsSync11, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync5, copyFileSync as copyFileSync3, unlinkSync as unlinkSync2 } from "fs";
10574
- import { join as join12 } from "path";
10722
+ import {
10723
+ copyFileSync as copyFileSync3,
10724
+ existsSync as existsSync11,
10725
+ mkdirSync as mkdirSync6,
10726
+ readFileSync as readFileSync6,
10727
+ renameSync as renameSync2,
10728
+ rmSync as rmSync5,
10729
+ unlinkSync as unlinkSync2,
10730
+ writeFileSync as writeFileSync5
10731
+ } from "fs";
10732
+ import { dirname as dirname4, join as join12 } from "path";
10575
10733
  function getSessionLockPath2() {
10576
10734
  return join12(getClaudeDesktopHome(), ".relay-ai.lock");
10577
10735
  }
10578
- function readSessionLock2() {
10736
+ function inspectSessionLock() {
10579
10737
  const path2 = getSessionLockPath2();
10580
- if (!existsSync11(path2)) return null;
10738
+ if (!existsSync11(path2)) return { status: "missing" };
10581
10739
  try {
10582
10740
  const parsed = JSON.parse(readFileSync6(path2, "utf8"));
10583
- if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string") return parsed;
10741
+ if (typeof parsed.pid === "number" && typeof parsed.startedAt === "string" && typeof parsed.uuid === "string" && typeof parsed.proxyPort === "number") {
10742
+ return { status: "valid", lock: parsed };
10743
+ }
10584
10744
  } catch {
10585
10745
  }
10586
- return null;
10746
+ return { status: "unreadable" };
10587
10747
  }
10588
10748
  function writeSessionLock2(lock) {
10589
10749
  const path2 = getSessionLockPath2();
10590
- writeFileSync5(path2, `${JSON.stringify(lock, null, 2)}
10750
+ const tempPath = `${path2}.tmp.${process.pid}`;
10751
+ mkdirSync6(dirname4(path2), { recursive: true });
10752
+ try {
10753
+ writeFileSync5(tempPath, `${JSON.stringify(lock, null, 2)}
10591
10754
  `, "utf8");
10755
+ renameSync2(tempPath, path2);
10756
+ } finally {
10757
+ try {
10758
+ rmSync5(tempPath, { force: true });
10759
+ } catch {
10760
+ }
10761
+ }
10592
10762
  }
10593
10763
  function isProcessAlive3(pid) {
10594
10764
  if (pid <= 0) return false;
@@ -10602,7 +10772,7 @@ function isProcessAlive3(pid) {
10602
10772
  function backupMetaJson() {
10603
10773
  const metaPath = getMetaJsonPath();
10604
10774
  const backupPath = `${metaPath}.bak`;
10605
- if (existsSync11(metaPath)) {
10775
+ if (existsSync11(metaPath) && !existsSync11(backupPath)) {
10606
10776
  copyFileSync3(metaPath, backupPath);
10607
10777
  }
10608
10778
  }
@@ -10624,20 +10794,36 @@ function removeRelayAiConfig(uuid) {
10624
10794
  }
10625
10795
  }
10626
10796
  function hasStaleSession() {
10627
- const lock = readSessionLock2();
10628
- if (!lock) return false;
10629
- if (!isProcessAlive3(lock.pid)) {
10630
- return true;
10631
- }
10632
- return false;
10797
+ const state = inspectSessionLock();
10798
+ if (state.status === "unreadable") return true;
10799
+ return state.status === "valid" && !isProcessAlive3(state.lock.pid);
10633
10800
  }
10634
10801
  function isConcurrentLiveSession() {
10635
- const lock = readSessionLock2();
10636
- if (!lock) return false;
10637
- return isProcessAlive3(lock.pid);
10802
+ const state = inspectSessionLock();
10803
+ return state.status === "valid" && isProcessAlive3(state.lock.pid);
10804
+ }
10805
+ function lockHeldByAnotherLiveProcess(lock) {
10806
+ return lock !== null && lock.pid !== process.pid && isProcessAlive3(lock.pid);
10638
10807
  }
10639
10808
  function recoverSession() {
10640
- const lock = readSessionLock2();
10809
+ const state = inspectSessionLock();
10810
+ if (state.status === "unreadable") {
10811
+ restoreMetaJson();
10812
+ try {
10813
+ rmSync5(getSessionLockPath2(), { force: true });
10814
+ } catch {
10815
+ }
10816
+ return { recovered: true, message: "Cleared a corrupt claude-app session lock and restored shared config." };
10817
+ }
10818
+ const lock = state.status === "valid" ? state.lock : null;
10819
+ if (lockHeldByAnotherLiveProcess(lock)) {
10820
+ return {
10821
+ recovered: false,
10822
+ blocked: true,
10823
+ liveSession: true,
10824
+ message: `Another relay-ai claude-app session is running (pid ${lock.pid}). Ctrl+C it first, then run --restore.`
10825
+ };
10826
+ }
10641
10827
  if (lock) {
10642
10828
  restoreMetaJson();
10643
10829
  removeRelayAiConfig(lock.uuid);
@@ -10648,6 +10834,7 @@ function recoverSession() {
10648
10834
  } else {
10649
10835
  restoreMetaJson();
10650
10836
  }
10837
+ return { recovered: true, message: "Restored Claude Desktop relay-ai config." };
10651
10838
  }
10652
10839
  function waitForShutdown3() {
10653
10840
  return new Promise((resolve2) => {
@@ -10668,11 +10855,20 @@ function waitForShutdown3() {
10668
10855
  });
10669
10856
  }
10670
10857
  function cleanupSession(uuid) {
10671
- restoreMetaJson();
10672
- removeRelayAiConfig(uuid);
10673
- try {
10674
- rmSync5(getSessionLockPath2(), { force: true });
10675
- } catch {
10858
+ const state = inspectSessionLock();
10859
+ const lock = state.status === "valid" ? state.lock : null;
10860
+ const sharedStateIsOwnedElsewhere = lockHeldByAnotherLiveProcess(lock);
10861
+ if (!sharedStateIsOwnedElsewhere) {
10862
+ restoreMetaJson();
10863
+ try {
10864
+ rmSync5(getSessionLockPath2(), { force: true });
10865
+ } catch {
10866
+ }
10867
+ }
10868
+ const meta = readMetaJson();
10869
+ const configIsReferenced = meta === null ? existsSync11(getMetaJsonPath()) : meta.appliedId === uuid || meta.entries.some((entry) => entry.id === uuid);
10870
+ if (!sharedStateIsOwnedElsewhere || !configIsReferenced) {
10871
+ removeRelayAiConfig(uuid);
10676
10872
  }
10677
10873
  }
10678
10874
  function setupExitCleanup(uuid) {
@@ -10680,6 +10876,10 @@ function setupExitCleanup(uuid) {
10680
10876
  }
10681
10877
 
10682
10878
  // src/claude-app.ts
10879
+ var CLAUDE_APP_GATEWAY_OPTIONS = {
10880
+ maskGatewayIds: true,
10881
+ longContextDisplay: "single-1m"
10882
+ };
10683
10883
  function claudeAppHelpText() {
10684
10884
  return `${pc11.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
10685
10885
 
@@ -10697,9 +10897,10 @@ ${pc11.bold("Options:")}
10697
10897
  --version Show version
10698
10898
 
10699
10899
  ${pc11.bold("Description:")}
10700
- Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
10701
- (with backup + restore on Ctrl+C), starts a local Responses proxy, and opens
10702
- the Claude Desktop app. Keep this terminal open while using Claude.
10900
+ Picks a provider and model from ~/.relay-ai/providers.json, combines the selected model
10901
+ with your available saved favorites, patches Claude Desktop config (with backup + restore
10902
+ on Ctrl+C), starts a local Responses proxy, and opens the Claude Desktop app.
10903
+ Keep this terminal open while using Claude.
10703
10904
 
10704
10905
  ${pc11.bold("Platforms:")}
10705
10906
  macOS and Windows. Linux is not supported.
@@ -10712,42 +10913,15 @@ ${pc11.bold("Cleanup:")}
10712
10913
  function providerForClaudePicker(provider) {
10713
10914
  return { ...provider, models: routableModelsForProvider(provider, "claude-app") };
10714
10915
  }
10715
- function modelToServerModelInfo(model, provider, overrides = {}) {
10716
- return {
10717
- id: model.id,
10718
- name: model.name,
10719
- isFree: model.isFree ?? false,
10720
- brand: model.brand ?? "",
10721
- providerLabel: provider.name,
10722
- providerId: provider.id,
10723
- sourceBackend: provider.id,
10724
- modelFormat: model.modelFormat,
10725
- upstreamModelId: model.upstreamModelId,
10726
- cost: model.cost,
10727
- baseUrl: model.baseUrl,
10728
- completionsUrl: model.completionsUrl,
10729
- npm: model.npm,
10730
- apiBaseUrl: model.apiBaseUrl,
10731
- apiKey: provider.apiKey,
10732
- authType: provider.authType,
10733
- oauthAccountId: provider.oauthAccountId,
10734
- contextWindow: model.contextWindow,
10735
- supportedParameters: model.supportedParameters,
10736
- reasoning: model.reasoning,
10737
- interleavedReasoningField: model.interleavedReasoningField,
10738
- headers: provider.headers,
10739
- ...overrides
10740
- };
10741
- }
10742
10916
  async function runClaudeAppCommand(args, boot) {
10743
10917
  if (args.includes("--help") || args.includes("-h")) {
10744
10918
  console.log(claudeAppHelpText());
10745
10919
  return 0;
10746
10920
  }
10747
10921
  if (args.includes("--restore")) {
10748
- recoverSession();
10749
- console.log("Restored Claude Desktop relay-ai config.");
10750
- return 0;
10922
+ const result = recoverSession();
10923
+ console.log(result.message);
10924
+ return result.blocked || result.liveSession ? 1 : 0;
10751
10925
  }
10752
10926
  const trace = args.includes("--trace");
10753
10927
  const debugLogPath = trace ? getProxyDebugLogPath() : void 0;
@@ -10812,87 +10986,54 @@ async function runClaudeAppCommand(args, boot) {
10812
10986
  if (!pickedProvider) return 0;
10813
10987
  if (pickedProvider === "__favorites__") {
10814
10988
  useFavorites = true;
10989
+ const firstFavorite = resolveFirstAvailableFavorite(favorites, compatible);
10990
+ if (!firstFavorite) {
10991
+ p13.log.warn("No saved Claude App favorites are currently available.");
10992
+ return 0;
10993
+ }
10994
+ activeProvider = firstFavorite.provider;
10995
+ selectedModel = firstFavorite.model;
10815
10996
  } else {
10816
10997
  activeProvider = providerForClaudePicker(pickedProvider);
10817
10998
  const pickedModel = await pickCodexModel(activeProvider, prefs);
10818
- if (!pickedModel) return 0;
10999
+ if (!pickedModel || pickedModel === "back") return 0;
10819
11000
  selectedModel = pickedModel;
10820
11001
  }
10821
11002
  }
10822
- if (activeProvider) {
10823
- const apiKey = await resolveLocalProviderApiKey(activeProvider);
10824
- if (!apiKey) {
10825
- p13.log.error(`No credential for ${activeProvider.name}. Run relay-ai providers auth ${activeProvider.id}.`);
10826
- return 1;
10827
- }
10828
- activeProvider.apiKey = apiKey;
11003
+ if (!activeProvider || !selectedModel) {
11004
+ p13.log.error("No Claude App launch model was selected.");
11005
+ return 1;
10829
11006
  }
10830
- let serverModels = [];
10831
- let cloudCodeBackend = null;
10832
- let cloudCodeFavBackend = null;
10833
- if (useFavorites) {
10834
- const antigravityProvider = catalog.find((lp) => lp.id === "antigravity");
10835
- const cloudCodeFavoriteModels = favorites.map((fav) => {
10836
- if (fav.providerId !== "antigravity") return null;
10837
- const model = antigravityProvider?.models.find((m) => m.id === fav.modelId);
10838
- return model?.modelFormat === "cloud-code" ? model : null;
10839
- }).filter((m) => m !== null);
10840
- const regularFavorites = favorites.filter(
10841
- (fav) => !cloudCodeFavoriteModels.some((m) => m.id === fav.modelId && fav.providerId === "antigravity")
10842
- );
10843
- let cloudCodeServerModels = [];
10844
- if (cloudCodeFavoriteModels.length > 0 && antigravityProvider?.apiKey) {
10845
- const cloudRoutes = cloudCodeFavoriteModels.map(
10846
- (model) => buildCloudCodeProxyRoute(
10847
- model,
10848
- antigravityProvider.apiKey,
10849
- antigravityProvider.providerData ?? {}
10850
- )
10851
- );
10852
- const startingAlias = cloudRoutes[0].aliasId;
10853
- cloudCodeFavBackend = await startCloudCodeCatalogBackend(cloudRoutes, startingAlias, trace);
10854
- const favBackend = cloudCodeFavBackend;
10855
- cloudCodeServerModels = cloudCodeFavoriteModels.map((model) => modelToServerModelInfo(model, antigravityProvider, {
10856
- isFree: false,
10857
- providerId: "antigravity",
10858
- sourceBackend: "antigravity",
10859
- modelFormat: "anthropic",
10860
- cost: void 0,
10861
- baseUrl: `http://127.0.0.1:${favBackend.port}`,
10862
- completionsUrl: void 0,
10863
- npm: void 0,
10864
- apiBaseUrl: void 0,
10865
- apiKey: favBackend.token,
10866
- authType: void 0,
10867
- oauthAccountId: void 0,
10868
- headers: void 0
10869
- }));
10870
- }
10871
- const allModels = await loadServerModels();
10872
- const regularServerModels = filterServerModelsByFavorites(allModels, regularFavorites);
10873
- serverModels = [...cloudCodeServerModels, ...regularServerModels];
10874
- } else if (selectedModel.modelFormat === "cloud-code") {
10875
- const providerData = activeProvider.providerData ?? {};
10876
- const cloudRoute = buildCloudCodeProxyRoute(selectedModel, activeProvider.apiKey, providerData);
10877
- cloudCodeBackend = await startCloudCodeCatalogBackend([cloudRoute], cloudRoute.aliasId, trace);
10878
- serverModels = [modelToServerModelInfo(selectedModel, activeProvider, {
10879
- modelFormat: "anthropic",
10880
- baseUrl: `http://127.0.0.1:${cloudCodeBackend.port}`,
10881
- completionsUrl: void 0,
10882
- npm: void 0,
10883
- apiBaseUrl: void 0,
10884
- apiKey: cloudCodeBackend.token,
10885
- authType: void 0,
10886
- oauthAccountId: void 0,
10887
- headers: void 0
10888
- })];
10889
- } else {
10890
- serverModels = [modelToServerModelInfo(selectedModel, activeProvider)];
11007
+ const catalogResolution = await resolveClaudeAppCatalog(
11008
+ activeProvider,
11009
+ selectedModel,
11010
+ compatible,
11011
+ favorites
11012
+ );
11013
+ if (!catalogResolution.ok) {
11014
+ p13.log.error(catalogResolution.error);
11015
+ return 1;
11016
+ }
11017
+ if (catalogResolution.droppedFavorites.length > 0) {
11018
+ const skipped = catalogResolution.droppedFavorites.map((favorite) => `${favorite.providerId}/${favorite.modelId}`).join(", ");
11019
+ p13.log.warn(`Skipped unavailable or unauthorized favorite(s): ${skipped}`);
11020
+ }
11021
+ if (catalogResolution.capacitySkippedFavorites.length > 0) {
11022
+ const skipped = catalogResolution.capacitySkippedFavorites.map((favorite) => `${favorite.providerId}/${favorite.modelId}`).join(", ");
11023
+ p13.log.warn(`Skipped favorite(s) beyond the 20-model catalog limit: ${skipped}`);
10891
11024
  }
11025
+ let cloudCodeBackend = null;
10892
11026
  let proxyHandle = null;
10893
11027
  let sessionActive = false;
10894
11028
  let uuid = "";
10895
11029
  try {
11030
+ const builtCatalog = await buildClaudeAppServerCatalog(
11031
+ catalogResolution.entries,
11032
+ catalogResolution.providersById,
11033
+ trace
11034
+ );
11035
+ const serverModels = builtCatalog.serverModels;
11036
+ cloudCodeBackend = builtCatalog.backend;
10896
11037
  backupMetaJson();
10897
11038
  proxyHandle = await startServer({
10898
11039
  host: "127.0.0.1",
@@ -10900,9 +11041,9 @@ async function runClaudeAppCommand(args, boot) {
10900
11041
  // random port
10901
11042
  apiKey: "dummy",
10902
11043
  serverPassword: null,
10903
- catalog: createGatewayModelCatalog(serverModels, { maskGatewayIds: true }),
11044
+ catalog: createGatewayModelCatalog(serverModels, CLAUDE_APP_GATEWAY_OPTIONS),
10904
11045
  backends: BACKENDS,
10905
- gateway: { maskGatewayIds: true },
11046
+ gateway: CLAUDE_APP_GATEWAY_OPTIONS,
10906
11047
  debugLogPath
10907
11048
  });
10908
11049
  uuid = writeRelayAiConfig(proxyHandle.port);
@@ -10932,11 +11073,10 @@ ${pc11.green("\u2714")} Proxy started on port ${proxyHandle.port}`);
10932
11073
  }
10933
11074
  console.log(`
10934
11075
  ${pc11.bold("Claude Desktop 3P Mode Active")}`);
10935
- if (useFavorites) {
10936
- console.log(`${pc11.dim("Catalog:")} Favorite models only`);
10937
- } else {
10938
- console.log(`${pc11.dim("Model:")} ${selectedModel.id}`);
10939
- console.log(`${pc11.dim("Provider:")} ${activeProvider.name}`);
11076
+ console.log(`${pc11.dim("Model:")} ${selectedModel.id}`);
11077
+ console.log(`${pc11.dim("Provider:")} ${activeProvider.name}`);
11078
+ if (serverModels.length > 1) {
11079
+ console.log(`${pc11.dim("Catalog:")} ${serverModels.length} models (selected + favorites)`);
10940
11080
  }
10941
11081
  console.log(`${pc11.cyan("Press Ctrl+C to stop and restore config.")}`);
10942
11082
  await waitForShutdown3();
@@ -10944,7 +11084,6 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
10944
11084
  cleanupSession(uuid);
10945
11085
  sessionActive = false;
10946
11086
  if (cloudCodeBackend) cloudCodeBackend.handle.close();
10947
- if (cloudCodeFavBackend) cloudCodeFavBackend.handle.close();
10948
11087
  if (isClaudeAppRunning()) {
10949
11088
  const shouldClose = await p13.confirm({ message: "Claude Desktop is still running. Close it?" });
10950
11089
  if (shouldClose && !p13.isCancel(shouldClose)) {
@@ -10958,13 +11097,13 @@ ${pc11.bold("Claude Desktop 3P Mode Active")}`);
10958
11097
  cleanupSession(uuid);
10959
11098
  }
10960
11099
  if (cloudCodeBackend) cloudCodeBackend.handle.close();
10961
- if (cloudCodeFavBackend) cloudCodeFavBackend.handle.close();
11100
+ p13.log.error(String(err instanceof Error ? err.message : err));
10962
11101
  return 1;
10963
11102
  }
10964
11103
  }
10965
11104
 
10966
11105
  // src/ai-doc.ts
10967
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
11106
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
10968
11107
  import { homedir as homedir10 } from "os";
10969
11108
  import { join as join13 } from "path";
10970
11109
  var SKILL_DIR_NAME = "relay-ai-cli";
@@ -11496,7 +11635,7 @@ function installAiDoc(opts = {}) {
11496
11635
  result.skipped.push(skillPath);
11497
11636
  continue;
11498
11637
  }
11499
- mkdirSync6(skillDir, { recursive: true });
11638
+ mkdirSync7(skillDir, { recursive: true });
11500
11639
  writeFileSync6(skillPath, doc, "utf-8");
11501
11640
  if (previous) {
11502
11641
  result.updated.push({ path: skillPath, fromVersion: previous });
@@ -11622,17 +11761,18 @@ import { randomBytes, randomUUID as randomUUID3 } from "crypto";
11622
11761
  import {
11623
11762
  chmodSync,
11624
11763
  existsSync as existsSync13,
11625
- mkdirSync as mkdirSync7,
11764
+ mkdirSync as mkdirSync8,
11626
11765
  readFileSync as readFileSync8,
11627
11766
  readdirSync as readdirSync3,
11628
11767
  rmSync as rmSync6,
11629
11768
  statSync as statSync2,
11630
11769
  writeFileSync as writeFileSync7
11631
11770
  } from "fs";
11632
- import { dirname as dirname4, join as join14, resolve } from "path";
11771
+ import { dirname as dirname5, join as join14, resolve } from "path";
11633
11772
  import forge from "node-forge";
11634
11773
  var SESSION_ROOT = "http-proxy-sessions";
11635
11774
  var OWNER_FILE = "owner.pid";
11775
+ var MID_CREATION_GRACE_MS = 3e4;
11636
11776
  function serialNumber() {
11637
11777
  const bytes = randomBytes(16);
11638
11778
  bytes[0] &= 127;
@@ -11650,24 +11790,40 @@ function processIsRunning(pid) {
11650
11790
  function cleanupStaleHttpProxySessions(appHome = getAppHome()) {
11651
11791
  const root = join14(appHome, SESSION_ROOT);
11652
11792
  if (!existsSync13(root)) return;
11793
+ const now = Date.now();
11653
11794
  for (const name of readdirSync3(root)) {
11654
11795
  const sessionDir = join14(root, name);
11655
11796
  try {
11656
- if (!statSync2(sessionDir).isDirectory()) continue;
11657
- const pid = Number(readFileSync8(join14(sessionDir, OWNER_FILE), "utf8").trim());
11797
+ const stat = statSync2(sessionDir);
11798
+ if (!stat.isDirectory()) continue;
11799
+ const ownerPath = join14(sessionDir, OWNER_FILE);
11800
+ if (!existsSync13(ownerPath)) {
11801
+ if (now - stat.mtimeMs > MID_CREATION_GRACE_MS) {
11802
+ rmSync6(sessionDir, { recursive: true, force: true });
11803
+ }
11804
+ continue;
11805
+ }
11806
+ const pid = Number(readFileSync8(ownerPath, "utf8").trim());
11807
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
11808
+ const ownerStat = statSync2(ownerPath);
11809
+ const newestMtimeMs = Math.max(stat.mtimeMs, ownerStat.mtimeMs);
11810
+ if (now - newestMtimeMs > MID_CREATION_GRACE_MS) {
11811
+ rmSync6(sessionDir, { recursive: true, force: true });
11812
+ }
11813
+ continue;
11814
+ }
11658
11815
  if (!processIsRunning(pid)) rmSync6(sessionDir, { recursive: true, force: true });
11659
11816
  } catch {
11660
- rmSync6(sessionDir, { recursive: true, force: true });
11661
11817
  }
11662
11818
  }
11663
11819
  }
11664
11820
  function createHttpProxyCertificates(appHome = getAppHome()) {
11665
11821
  cleanupStaleHttpProxySessions(appHome);
11666
11822
  const root = join14(appHome, SESSION_ROOT);
11667
- mkdirSync7(root, { recursive: true, mode: 448 });
11823
+ mkdirSync8(root, { recursive: true, mode: 448 });
11668
11824
  chmodSync(root, 448);
11669
11825
  const sessionDir = join14(root, randomUUID3());
11670
- mkdirSync7(sessionDir, { mode: 448 });
11826
+ mkdirSync8(sessionDir, { mode: 448 });
11671
11827
  chmodSync(sessionDir, 448);
11672
11828
  writeFileSync7(join14(sessionDir, OWNER_FILE), `${process.pid}
11673
11829
  `, { mode: 384 });
@@ -11750,7 +11906,7 @@ function createHttpProxyCaBundle(relayCaCertPath, additionalCaCertPath) {
11750
11906
  const relayCa = readFileSync8(relayCaCertPath, "utf8").trimEnd();
11751
11907
  const additionalCa = readFileSync8(additionalCaCertPath, "utf8").trim();
11752
11908
  if (!additionalCa) return relayCaCertPath;
11753
- const combinedPath = join14(dirname4(relayCaCertPath), "combined-ca.pem");
11909
+ const combinedPath = join14(dirname5(relayCaCertPath), "combined-ca.pem");
11754
11910
  writeFileSync7(
11755
11911
  combinedPath,
11756
11912
  `${relayCa}
@@ -13155,6 +13311,14 @@ Error: ${launchPlan.error}
13155
13311
  return 1;
13156
13312
  }
13157
13313
  const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
13314
+ if (!launchAllowsNonTty(launchPlan, httpProxyOnly) && !process.stdin.isTTY) {
13315
+ console.error(
13316
+ pc12.red(
13317
+ "relay-ai claude requires an interactive terminal (or use --provider and --model for non-interactive launch)."
13318
+ )
13319
+ );
13320
+ return 1;
13321
+ }
13158
13322
  if (!agentStdout) relayIntro("Claude Code");
13159
13323
  if (setup && !dryRun && !agentStdout) {
13160
13324
  p14.log.info("Provider setup now lives in relay-ai providers \u2014 opening that next is recommended.");
@@ -13599,7 +13763,7 @@ Options:
13599
13763
  --trace Write debug logs under ~/.relay-ai/logs/`);
13600
13764
  return 0;
13601
13765
  }
13602
- const { runUiCommand } = await import("./ui-command-GNCQS7F7.js");
13766
+ const { runUiCommand } = await import("./ui-command-AMF7XGFD.js");
13603
13767
  return runUiCommand({ trace: parsed.trace, serverMode: parsed.uiServerMode });
13604
13768
  }
13605
13769
  if (parsed.command === "models") {
@@ -13632,6 +13796,10 @@ Options:
13632
13796
  console.log(VERSION);
13633
13797
  return 0;
13634
13798
  }
13799
+ if (parsed.showHelp) {
13800
+ console.log(codexAppHelpText());
13801
+ return 0;
13802
+ }
13635
13803
  return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex, launchProvider: parsed.launchProvider, launchModel: parsed.launchModel });
13636
13804
  }
13637
13805
  if (parsed.command === "claude-app") {
@@ -13639,6 +13807,10 @@ Options:
13639
13807
  console.log(VERSION);
13640
13808
  return 0;
13641
13809
  }
13810
+ if (parsed.showHelp) {
13811
+ console.log(claudeAppHelpText());
13812
+ return 0;
13813
+ }
13642
13814
  return runClaudeAppCommand(parsed.claudeArgs, { launchProvider: parsed.launchProvider, launchModel: parsed.launchModel });
13643
13815
  }
13644
13816
  if (parsed.command === "codex") {