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.
- package/dist/interactive.js +219 -31
- 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,15 +705,37 @@ 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(`
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
833
|
+
const curProvider = detectProvider();
|
|
718
834
|
if (arg) {
|
|
719
|
-
// Accept a number or a model ID
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
if (
|
|
723
|
-
|
|
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
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
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}${
|
|
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
|
}
|