rlm-cli 0.2.4 → 0.2.6
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 +257 -57
- package/package.json +1 -1
package/dist/interactive.js
CHANGED
|
@@ -91,16 +91,121 @@ 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
|
+
/** Wrap rl.question with ESC-to-cancel. Returns user input or null on ESC/empty. */
|
|
161
|
+
function questionWithEsc(rlInstance, promptText) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let escaped = false;
|
|
164
|
+
const onKeypress = (_str, key) => {
|
|
165
|
+
if (key?.name === "escape" && !escaped) {
|
|
166
|
+
escaped = true;
|
|
167
|
+
stdin.removeListener("keypress", onKeypress);
|
|
168
|
+
// Clear the prompt line visually
|
|
169
|
+
process.stdout.write("\r\x1b[2K");
|
|
170
|
+
// Programmatically submit empty to close the pending question
|
|
171
|
+
rlInstance.write("\n");
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
stdin.on("keypress", onKeypress);
|
|
175
|
+
rlInstance.question(promptText, (answer) => {
|
|
176
|
+
stdin.removeListener("keypress", onKeypress);
|
|
177
|
+
resolve(escaped ? null : answer.trim() || null);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Prompt user for a provider's API key if not already set.
|
|
182
|
+
* Returns true (got key), false (empty input), or null (ESC pressed). */
|
|
183
|
+
async function promptForProviderKey(rlInstance, providerInfo) {
|
|
184
|
+
if (process.env[providerInfo.env])
|
|
185
|
+
return true;
|
|
186
|
+
const key = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
|
|
187
|
+
if (key === null)
|
|
188
|
+
return null; // ESC
|
|
189
|
+
if (!key)
|
|
190
|
+
return false; // empty
|
|
191
|
+
process.env[providerInfo.env] = key;
|
|
192
|
+
// Save to shell profile
|
|
193
|
+
const shellRc = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
|
|
194
|
+
const rcPath = shellRc.replace("~", process.env.HOME || "~");
|
|
195
|
+
try {
|
|
196
|
+
fs.appendFileSync(rcPath, `\nexport ${providerInfo.env}=${key}\n`);
|
|
197
|
+
console.log(`\n ${c.green}✓${c.reset} ${providerInfo.name} key saved to ${c.dim}${shellRc}${c.reset}`);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
console.log(`\n ${c.yellow}!${c.reset} Could not write to ${shellRc}. Add manually:`);
|
|
201
|
+
console.log(` ${c.yellow}export ${providerInfo.env}=${key}${c.reset}`);
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
/** Find the SETUP_PROVIDERS entry that owns a given pi-ai provider name. */
|
|
206
|
+
function findSetupProvider(piProvider) {
|
|
207
|
+
return SETUP_PROVIDERS.find((p) => p.piProvider === piProvider);
|
|
208
|
+
}
|
|
104
209
|
// ── Paste detection ─────────────────────────────────────────────────────────
|
|
105
210
|
function isMultiLineInput(input) {
|
|
106
211
|
return input.includes("\n");
|
|
@@ -160,8 +265,9 @@ ${c.bold}Context${c.reset}
|
|
|
160
265
|
${c.cyan}/clear-context${c.reset} Unload context
|
|
161
266
|
|
|
162
267
|
${c.bold}Model${c.reset}
|
|
163
|
-
${c.cyan}/model${c.reset} List
|
|
268
|
+
${c.cyan}/model${c.reset} List models for current provider
|
|
164
269
|
${c.cyan}/model${c.reset} <#|id> Switch model by number or ID
|
|
270
|
+
${c.cyan}/provider${c.reset} Switch provider
|
|
165
271
|
|
|
166
272
|
${c.bold}Tools${c.reset}
|
|
167
273
|
${c.cyan}/trajectories${c.reset} List saved runs
|
|
@@ -362,8 +468,20 @@ function displaySubQueryResult(info) {
|
|
|
362
468
|
function getAvailableModels() {
|
|
363
469
|
const items = [];
|
|
364
470
|
for (const provider of getProviders()) {
|
|
365
|
-
const
|
|
366
|
-
if (!process.env[
|
|
471
|
+
const key = providerEnvKey(provider);
|
|
472
|
+
if (!process.env[key] && provider !== detectProvider())
|
|
473
|
+
continue;
|
|
474
|
+
for (const m of getModels(provider)) {
|
|
475
|
+
items.push({ id: m.id, provider });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return items;
|
|
479
|
+
}
|
|
480
|
+
/** Get models for a specific provider (matching by pi-ai provider name or SETUP_PROVIDERS piProvider). */
|
|
481
|
+
function getModelsForProvider(providerName) {
|
|
482
|
+
const items = [];
|
|
483
|
+
for (const provider of getProviders()) {
|
|
484
|
+
if (provider !== providerName)
|
|
367
485
|
continue;
|
|
368
486
|
for (const m of getModels(provider)) {
|
|
369
487
|
items.push({ id: m.id, provider });
|
|
@@ -611,44 +729,51 @@ async function detectAndLoadUrl(input) {
|
|
|
611
729
|
// ── Main interactive loop ───────────────────────────────────────────────────
|
|
612
730
|
async function interactive() {
|
|
613
731
|
// Validate env
|
|
614
|
-
|
|
615
|
-
if (!hasApiKey) {
|
|
732
|
+
if (!hasAnyApiKey()) {
|
|
616
733
|
printBanner();
|
|
617
734
|
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`);
|
|
619
735
|
const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
620
|
-
|
|
621
|
-
|
|
736
|
+
let setupDone = false;
|
|
737
|
+
while (!setupDone) {
|
|
738
|
+
console.log(` ${c.bold}Select your provider:${c.reset}\n`);
|
|
739
|
+
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
740
|
+
console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
|
|
741
|
+
}
|
|
742
|
+
console.log();
|
|
743
|
+
const choice = await questionWithEsc(setupRl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
|
|
744
|
+
if (choice === null) {
|
|
745
|
+
// ESC at provider selection → exit
|
|
746
|
+
console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
|
|
622
747
|
setupRl.close();
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
console.log(
|
|
649
|
-
|
|
748
|
+
process.exit(0);
|
|
749
|
+
}
|
|
750
|
+
const idx = parseInt(choice, 10) - 1;
|
|
751
|
+
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
752
|
+
console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
const provider = SETUP_PROVIDERS[idx];
|
|
756
|
+
const gotKey = await promptForProviderKey(setupRl, provider);
|
|
757
|
+
if (gotKey === null) {
|
|
758
|
+
// ESC at key entry → back to provider selection
|
|
759
|
+
console.log();
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (!gotKey) {
|
|
763
|
+
console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
|
|
764
|
+
setupRl.close();
|
|
765
|
+
process.exit(0);
|
|
766
|
+
}
|
|
767
|
+
// Auto-select default model for chosen provider
|
|
768
|
+
const defaultModel = getDefaultModelForProvider(provider.piProvider);
|
|
769
|
+
if (defaultModel) {
|
|
770
|
+
currentModelId = defaultModel;
|
|
771
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
772
|
+
}
|
|
773
|
+
console.log();
|
|
774
|
+
setupDone = true;
|
|
650
775
|
}
|
|
651
|
-
|
|
776
|
+
setupRl.close();
|
|
652
777
|
}
|
|
653
778
|
// Resolve model
|
|
654
779
|
currentModel = resolveModel(currentModelId);
|
|
@@ -743,27 +868,55 @@ async function interactive() {
|
|
|
743
868
|
break;
|
|
744
869
|
case "model":
|
|
745
870
|
case "m": {
|
|
746
|
-
const
|
|
871
|
+
const curProvider = detectProvider();
|
|
747
872
|
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();
|
|
873
|
+
// Accept a number (from current provider list) or a model ID
|
|
874
|
+
const curModels = getModelsForProvider(curProvider);
|
|
875
|
+
let pick;
|
|
876
|
+
if (/^\d+$/.test(arg)) {
|
|
877
|
+
pick = curModels[parseInt(arg, 10) - 1]?.id;
|
|
757
878
|
}
|
|
758
879
|
else {
|
|
880
|
+
pick = arg;
|
|
881
|
+
}
|
|
882
|
+
if (!pick) {
|
|
883
|
+
console.log(` ${c.red}Invalid selection.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
// Check if this model belongs to a different provider
|
|
887
|
+
const resolved = resolveModelWithProvider(pick);
|
|
888
|
+
if (!resolved) {
|
|
759
889
|
console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
890
|
+
break;
|
|
760
891
|
}
|
|
892
|
+
if (resolved.provider !== curProvider) {
|
|
893
|
+
// Cross-provider switch
|
|
894
|
+
const setupInfo = findSetupProvider(resolved.provider);
|
|
895
|
+
const envVar = setupInfo?.env || providerEnvKey(resolved.provider);
|
|
896
|
+
const provName = setupInfo?.name || resolved.provider;
|
|
897
|
+
if (!process.env[envVar]) {
|
|
898
|
+
console.log(` ${c.yellow}That model requires ${provName}.${c.reset}`);
|
|
899
|
+
const gotKey = await promptForProviderKey(rl, { name: provName, env: envVar });
|
|
900
|
+
if (!gotKey) {
|
|
901
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
currentModelId = pick;
|
|
907
|
+
currentModel = resolved.model;
|
|
908
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
909
|
+
console.log();
|
|
910
|
+
printStatusLine();
|
|
761
911
|
}
|
|
762
912
|
else {
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
913
|
+
// List models for current provider
|
|
914
|
+
const models = getModelsForProvider(curProvider);
|
|
915
|
+
const provLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
916
|
+
console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
|
|
917
|
+
const pad = String(models.length).length;
|
|
918
|
+
for (let i = 0; i < models.length; i++) {
|
|
919
|
+
const m = models[i];
|
|
767
920
|
const num = String(i + 1).padStart(pad);
|
|
768
921
|
const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
|
|
769
922
|
const label = m.id === currentModelId
|
|
@@ -771,8 +924,55 @@ async function interactive() {
|
|
|
771
924
|
: `${c.dim}${m.id}${c.reset}`;
|
|
772
925
|
console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
|
|
773
926
|
}
|
|
774
|
-
console.log(`\n ${c.dim}${
|
|
927
|
+
console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
|
|
775
928
|
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}`);
|
|
929
|
+
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case "provider":
|
|
934
|
+
case "prov": {
|
|
935
|
+
const curProvider = detectProvider();
|
|
936
|
+
const curLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
937
|
+
console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
|
|
938
|
+
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
939
|
+
const p = SETUP_PROVIDERS[i];
|
|
940
|
+
const isCurrent = p.piProvider === curProvider;
|
|
941
|
+
const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
|
|
942
|
+
// Only show ✓ for non-current providers that have a key
|
|
943
|
+
const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
|
|
944
|
+
const label = isCurrent
|
|
945
|
+
? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
|
|
946
|
+
: `${p.name} ${c.dim}(${p.label})${c.reset}`;
|
|
947
|
+
console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
|
|
948
|
+
}
|
|
949
|
+
console.log();
|
|
950
|
+
const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
|
|
951
|
+
if (provChoice === null)
|
|
952
|
+
break; // ESC
|
|
953
|
+
const idx = parseInt(provChoice, 10) - 1;
|
|
954
|
+
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
955
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
const chosen = SETUP_PROVIDERS[idx];
|
|
959
|
+
const gotKey = await promptForProviderKey(rl, chosen);
|
|
960
|
+
if (!gotKey) {
|
|
961
|
+
// null (ESC) or false (empty) → cancel
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
// Auto-select first model from new provider
|
|
965
|
+
const defaultModel = getDefaultModelForProvider(chosen.piProvider);
|
|
966
|
+
if (defaultModel) {
|
|
967
|
+
currentModelId = defaultModel;
|
|
968
|
+
currentModel = resolveModel(currentModelId);
|
|
969
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
|
|
970
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
971
|
+
console.log();
|
|
972
|
+
printStatusLine();
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
|
|
776
976
|
}
|
|
777
977
|
break;
|
|
778
978
|
}
|