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.
- package/dist/interactive.js +213 -54
- package/package.json +1 -1
package/dist/interactive.js
CHANGED
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
366
|
-
if (!process.env[
|
|
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
|
-
|
|
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}
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
833
|
+
const curProvider = detectProvider();
|
|
747
834
|
if (arg) {
|
|
748
|
-
// Accept a number or a model ID
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
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
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
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}${
|
|
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
|
}
|