rlm-cli 0.2.4 → 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 +213 -54
  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,42 +705,35 @@ 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(` ${c.bold}Paste your API key to get started:${c.reset}\n`);
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();
619
716
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
620
- const key = await new Promise((resolve) => {
621
- setupRl.question(` ${c.cyan}API key:${c.reset} `, (answer) => {
622
- setupRl.close();
623
- resolve(answer.trim());
624
- });
625
- });
626
- if (!key) {
627
- console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
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();
628
723
  process.exit(0);
629
724
  }
630
- // Detect provider from key format
631
- let envVar;
632
- if (key.startsWith("sk-ant-")) {
633
- envVar = "ANTHROPIC_API_KEY";
634
- process.env.ANTHROPIC_API_KEY = key;
635
- }
636
- else {
637
- envVar = "OPENAI_API_KEY";
638
- process.env.OPENAI_API_KEY = key;
639
- }
640
- // Save to shell profile
641
- const shellRc = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
642
- const rcPath = shellRc.replace("~", process.env.HOME || "~");
643
- try {
644
- fs.appendFileSync(rcPath, `\nexport ${envVar}=${key}\n`);
645
- console.log(`\n ${c.green}✓${c.reset} Key saved to ${c.dim}${shellRc}${c.reset}`);
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);
646
731
  }
647
- catch {
648
- console.log(`\n ${c.yellow}!${c.reset} Could not write to ${shellRc}. Add manually:`);
649
- console.log(` ${c.yellow}export ${envVar}=${key}${c.reset}`);
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}`);
650
737
  }
651
738
  console.log();
652
739
  }
@@ -743,27 +830,55 @@ async function interactive() {
743
830
  break;
744
831
  case "model":
745
832
  case "m": {
746
- const available = getAvailableModels();
833
+ const curProvider = detectProvider();
747
834
  if (arg) {
748
- // Accept a number or a model ID
749
- const pick = /^\d+$/.test(arg) ? available[parseInt(arg, 10) - 1]?.id : arg;
750
- const newModel = pick ? resolveModel(pick) : undefined;
751
- if (newModel && pick) {
752
- currentModelId = pick;
753
- currentModel = newModel;
754
- console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
755
- console.log();
756
- 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;
757
840
  }
758
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) {
759
851
  console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
852
+ break;
760
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
+ }
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();
761
873
  }
762
874
  else {
763
- console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset}\n`);
764
- const pad = String(available.length).length;
765
- for (let i = 0; i < available.length; i++) {
766
- 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];
767
882
  const num = String(i + 1).padStart(pad);
768
883
  const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
769
884
  const label = m.id === currentModelId
@@ -771,8 +886,52 @@ async function interactive() {
771
886
  : `${c.dim}${m.id}${c.reset}`;
772
887
  console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
773
888
  }
774
- 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}`);
775
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}`);
776
935
  }
777
936
  break;
778
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlm-cli",
3
- "version": "0.2.4",
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": {