rlm-cli 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/interactive.js +219 -31
  2. package/package.json +1 -1
@@ -91,16 +91,97 @@ function resolveModel(modelId) {
91
91
  }
92
92
  return undefined;
93
93
  }
94
+ // Provider → env var mapping for well-known providers
95
+ const PROVIDER_KEYS = {
96
+ anthropic: "ANTHROPIC_API_KEY",
97
+ openai: "OPENAI_API_KEY",
98
+ google: "GOOGLE_API_KEY",
99
+ "google-gemini-cli": "GOOGLE_API_KEY",
100
+ "google-vertex": "GOOGLE_VERTEX_API_KEY",
101
+ groq: "GROQ_API_KEY",
102
+ xai: "XAI_API_KEY",
103
+ mistral: "MISTRAL_API_KEY",
104
+ openrouter: "OPENROUTER_API_KEY",
105
+ huggingface: "HUGGINGFACE_API_KEY",
106
+ cerebras: "CEREBRAS_API_KEY",
107
+ };
108
+ // User-facing provider list for setup & /provider command
109
+ const SETUP_PROVIDERS = [
110
+ { name: "Anthropic", label: "Claude", env: "ANTHROPIC_API_KEY", piProvider: "anthropic" },
111
+ { name: "OpenAI", label: "GPT", env: "OPENAI_API_KEY", piProvider: "openai" },
112
+ { name: "Google", label: "Gemini", env: "GOOGLE_API_KEY", piProvider: "google" },
113
+ { name: "Groq", label: "Groq", env: "GROQ_API_KEY", piProvider: "groq" },
114
+ { name: "xAI", label: "Grok", env: "XAI_API_KEY", piProvider: "xai" },
115
+ { name: "Mistral", label: "Mistral", env: "MISTRAL_API_KEY", piProvider: "mistral" },
116
+ { name: "OpenRouter", label: "OpenRouter", env: "OPENROUTER_API_KEY", piProvider: "openrouter" },
117
+ ];
118
+ function providerEnvKey(provider) {
119
+ return PROVIDER_KEYS[provider] || `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
120
+ }
94
121
  function detectProvider() {
95
- if (process.env.ANTHROPIC_API_KEY)
96
- return "anthropic";
97
- if (process.env.OPENAI_API_KEY) {
98
- if (process.env.OPENAI_BASE_URL?.includes("openrouter"))
99
- return "openrouter";
100
- return "openai";
122
+ for (const provider of Object.keys(PROVIDER_KEYS)) {
123
+ if (process.env[PROVIDER_KEYS[provider]])
124
+ return provider;
101
125
  }
102
126
  return "unknown";
103
127
  }
128
+ function hasAnyApiKey() {
129
+ return detectProvider() !== "unknown";
130
+ }
131
+ /** Returns the pi-ai provider name + model for a given model ID, searching all providers.
132
+ * Prioritises SETUP_PROVIDERS so e.g. "gpt-4o" resolves to "openai" not "azure-openai-responses". */
133
+ function resolveModelWithProvider(modelId) {
134
+ // First pass: check well-known (setup) providers
135
+ const knownNames = new Set(SETUP_PROVIDERS.map((p) => p.piProvider));
136
+ for (const provider of getProviders()) {
137
+ if (!knownNames.has(provider))
138
+ continue;
139
+ for (const m of getModels(provider)) {
140
+ if (m.id === modelId)
141
+ return { model: m, provider };
142
+ }
143
+ }
144
+ // Second pass: all remaining providers
145
+ for (const provider of getProviders()) {
146
+ if (knownNames.has(provider))
147
+ continue;
148
+ for (const m of getModels(provider)) {
149
+ if (m.id === modelId)
150
+ return { model: m, provider };
151
+ }
152
+ }
153
+ return undefined;
154
+ }
155
+ /** Returns the first model ID from a given pi-ai provider. */
156
+ function getDefaultModelForProvider(provider) {
157
+ const models = getModels(provider);
158
+ return models.length > 0 ? models[0].id : undefined;
159
+ }
160
+ /** Prompt user for a provider's API key if not already set. Returns true if key is available. */
161
+ async function promptForProviderKey(rlInstance, providerInfo) {
162
+ if (process.env[providerInfo.env])
163
+ return true;
164
+ const key = await new Promise((resolve) => rlInstance.question(` ${c.cyan}${providerInfo.env}:${c.reset} `, (a) => resolve(a.trim())));
165
+ if (!key)
166
+ return false;
167
+ process.env[providerInfo.env] = key;
168
+ // Save to shell profile
169
+ const shellRc = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
170
+ const rcPath = shellRc.replace("~", process.env.HOME || "~");
171
+ try {
172
+ fs.appendFileSync(rcPath, `\nexport ${providerInfo.env}=${key}\n`);
173
+ console.log(`\n ${c.green}✓${c.reset} ${providerInfo.name} key saved to ${c.dim}${shellRc}${c.reset}`);
174
+ }
175
+ catch {
176
+ console.log(`\n ${c.yellow}!${c.reset} Could not write to ${shellRc}. Add manually:`);
177
+ console.log(` ${c.yellow}export ${providerInfo.env}=${key}${c.reset}`);
178
+ }
179
+ return true;
180
+ }
181
+ /** Find the SETUP_PROVIDERS entry that owns a given pi-ai provider name. */
182
+ function findSetupProvider(piProvider) {
183
+ return SETUP_PROVIDERS.find((p) => p.piProvider === piProvider);
184
+ }
104
185
  // ── Paste detection ─────────────────────────────────────────────────────────
105
186
  function isMultiLineInput(input) {
106
187
  return input.includes("\n");
@@ -160,8 +241,9 @@ ${c.bold}Context${c.reset}
160
241
  ${c.cyan}/clear-context${c.reset} Unload context
161
242
 
162
243
  ${c.bold}Model${c.reset}
163
- ${c.cyan}/model${c.reset} List available models
244
+ ${c.cyan}/model${c.reset} List models for current provider
164
245
  ${c.cyan}/model${c.reset} <#|id> Switch model by number or ID
246
+ ${c.cyan}/provider${c.reset} Switch provider
165
247
 
166
248
  ${c.bold}Tools${c.reset}
167
249
  ${c.cyan}/trajectories${c.reset} List saved runs
@@ -362,8 +444,20 @@ function displaySubQueryResult(info) {
362
444
  function getAvailableModels() {
363
445
  const items = [];
364
446
  for (const provider of getProviders()) {
365
- const providerKey = `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
366
- if (!process.env[providerKey] && provider !== detectProvider())
447
+ const key = providerEnvKey(provider);
448
+ if (!process.env[key] && provider !== detectProvider())
449
+ continue;
450
+ for (const m of getModels(provider)) {
451
+ items.push({ id: m.id, provider });
452
+ }
453
+ }
454
+ return items;
455
+ }
456
+ /** Get models for a specific provider (matching by pi-ai provider name or SETUP_PROVIDERS piProvider). */
457
+ function getModelsForProvider(providerName) {
458
+ const items = [];
459
+ for (const provider of getProviders()) {
460
+ if (provider !== providerName)
367
461
  continue;
368
462
  for (const m of getModels(provider)) {
369
463
  items.push({ id: m.id, provider });
@@ -611,15 +705,37 @@ async function detectAndLoadUrl(input) {
611
705
  // ── Main interactive loop ───────────────────────────────────────────────────
612
706
  async function interactive() {
613
707
  // Validate env
614
- const hasApiKey = process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
615
- if (!hasApiKey) {
708
+ if (!hasAnyApiKey()) {
616
709
  printBanner();
617
710
  console.log(` ${c.red}No API key found.${c.reset}\n`);
618
- console.log(` Set one of these environment variables:\n`);
619
- console.log(` ${c.yellow}export ANTHROPIC_API_KEY=sk-ant-...${c.reset} ${c.dim}# Anthropic (Claude)${c.reset}`);
620
- console.log(` ${c.yellow}export OPENAI_API_KEY=sk-...${c.reset} ${c.dim}# OpenAI (GPT)${c.reset}\n`);
621
- console.log(` ${c.dim}Add to your shell profile (~/.zshrc or ~/.bashrc) to persist across sessions.${c.reset}\n`);
622
- process.exit(0);
711
+ console.log(` ${c.bold}Select your provider:${c.reset}\n`);
712
+ for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
713
+ console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
714
+ }
715
+ console.log();
716
+ const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
717
+ const ask = (prompt) => new Promise((resolve) => setupRl.question(prompt, (a) => resolve(a.trim())));
718
+ const choice = await ask(` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
719
+ const idx = parseInt(choice, 10) - 1;
720
+ if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
721
+ console.log(`\n ${c.dim}Invalid choice. Exiting.${c.reset}\n`);
722
+ setupRl.close();
723
+ process.exit(0);
724
+ }
725
+ const provider = SETUP_PROVIDERS[idx];
726
+ const gotKey = await promptForProviderKey(setupRl, provider);
727
+ setupRl.close();
728
+ if (!gotKey) {
729
+ console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
730
+ process.exit(0);
731
+ }
732
+ // Auto-select default model for chosen provider
733
+ const defaultModel = getDefaultModelForProvider(provider.piProvider);
734
+ if (defaultModel) {
735
+ currentModelId = defaultModel;
736
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
737
+ }
738
+ console.log();
623
739
  }
624
740
  // Resolve model
625
741
  currentModel = resolveModel(currentModelId);
@@ -714,27 +830,55 @@ async function interactive() {
714
830
  break;
715
831
  case "model":
716
832
  case "m": {
717
- const available = getAvailableModels();
833
+ const curProvider = detectProvider();
718
834
  if (arg) {
719
- // Accept a number or a model ID
720
- const pick = /^\d+$/.test(arg) ? available[parseInt(arg, 10) - 1]?.id : arg;
721
- const newModel = pick ? resolveModel(pick) : undefined;
722
- if (newModel && pick) {
723
- currentModelId = pick;
724
- currentModel = newModel;
725
- console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
726
- console.log();
727
- printStatusLine();
835
+ // Accept a number (from current provider list) or a model ID
836
+ const curModels = getModelsForProvider(curProvider);
837
+ let pick;
838
+ if (/^\d+$/.test(arg)) {
839
+ pick = curModels[parseInt(arg, 10) - 1]?.id;
728
840
  }
729
841
  else {
842
+ pick = arg;
843
+ }
844
+ if (!pick) {
845
+ console.log(` ${c.red}Invalid selection.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
846
+ break;
847
+ }
848
+ // Check if this model belongs to a different provider
849
+ const resolved = resolveModelWithProvider(pick);
850
+ if (!resolved) {
730
851
  console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
852
+ break;
853
+ }
854
+ if (resolved.provider !== curProvider) {
855
+ // Cross-provider switch
856
+ const setupInfo = findSetupProvider(resolved.provider);
857
+ const envVar = setupInfo?.env || providerEnvKey(resolved.provider);
858
+ const provName = setupInfo?.name || resolved.provider;
859
+ if (!process.env[envVar]) {
860
+ console.log(` ${c.yellow}That model requires ${provName}.${c.reset}`);
861
+ const gotKey = await promptForProviderKey(rl, { name: provName, env: envVar });
862
+ if (!gotKey) {
863
+ console.log(` ${c.dim}Cancelled.${c.reset}`);
864
+ break;
865
+ }
866
+ }
731
867
  }
868
+ currentModelId = pick;
869
+ currentModel = resolved.model;
870
+ console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
871
+ console.log();
872
+ printStatusLine();
732
873
  }
733
874
  else {
734
- console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset}\n`);
735
- const pad = String(available.length).length;
736
- for (let i = 0; i < available.length; i++) {
737
- const m = available[i];
875
+ // List models for current provider
876
+ const models = getModelsForProvider(curProvider);
877
+ const provLabel = findSetupProvider(curProvider)?.name || curProvider;
878
+ console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
879
+ const pad = String(models.length).length;
880
+ for (let i = 0; i < models.length; i++) {
881
+ const m = models[i];
738
882
  const num = String(i + 1).padStart(pad);
739
883
  const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
740
884
  const label = m.id === currentModelId
@@ -742,8 +886,52 @@ async function interactive() {
742
886
  : `${c.dim}${m.id}${c.reset}`;
743
887
  console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
744
888
  }
745
- console.log(`\n ${c.dim}${available.length} models · scroll up to see full list.${c.reset}`);
889
+ console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
746
890
  console.log(` ${c.dim}Type${c.reset} ${c.cyan}/model <number>${c.reset} ${c.dim}or${c.reset} ${c.cyan}/model <id>${c.reset} ${c.dim}to switch.${c.reset}`);
891
+ console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
892
+ }
893
+ break;
894
+ }
895
+ case "provider":
896
+ case "prov": {
897
+ const curProvider = detectProvider();
898
+ const curLabel = findSetupProvider(curProvider)?.name || curProvider;
899
+ console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
900
+ for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
901
+ const p = SETUP_PROVIDERS[i];
902
+ const isCurrent = p.piProvider === curProvider;
903
+ const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
904
+ const hasKey = process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
905
+ const label = isCurrent
906
+ ? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
907
+ : `${p.name} ${c.dim}(${p.label})${c.reset}`;
908
+ console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
909
+ }
910
+ console.log();
911
+ const provChoice = await new Promise((resolve) => rl.question(` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `, (a) => resolve(a.trim())));
912
+ const idx = parseInt(provChoice, 10) - 1;
913
+ if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
914
+ console.log(` ${c.dim}Cancelled.${c.reset}`);
915
+ break;
916
+ }
917
+ const chosen = SETUP_PROVIDERS[idx];
918
+ const gotKey = await promptForProviderKey(rl, chosen);
919
+ if (!gotKey) {
920
+ console.log(` ${c.dim}Cancelled.${c.reset}`);
921
+ break;
922
+ }
923
+ // Auto-select first model from new provider
924
+ const defaultModel = getDefaultModelForProvider(chosen.piProvider);
925
+ if (defaultModel) {
926
+ currentModelId = defaultModel;
927
+ currentModel = resolveModel(currentModelId);
928
+ console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
929
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
930
+ console.log();
931
+ printStatusLine();
932
+ }
933
+ else {
934
+ console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
747
935
  }
748
936
  break;
749
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlm-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {