rlm-cli 0.2.10 → 0.2.12
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/LICENSE +21 -0
- package/dist/interactive.js +284 -243
- package/package.json +8 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vipul Maheshwari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/interactive.js
CHANGED
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
import "./env.js";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
13
14
|
import * as readline from "node:readline";
|
|
14
15
|
import { stdin, stdout } from "node:process";
|
|
16
|
+
// Global error handlers — prevent raw stack traces from leaking to terminal
|
|
17
|
+
process.on("uncaughtException", (err) => {
|
|
18
|
+
console.error(`\n \x1b[31mUnexpected error: ${err.message}\x1b[0m\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
process.on("unhandledRejection", (err) => {
|
|
22
|
+
console.error(`\n \x1b[31mUnexpected error: ${err?.message || err}\x1b[0m\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
15
25
|
const { getModels, getProviders } = await import("@mariozechner/pi-ai");
|
|
16
26
|
const { PythonRepl } = await import("./repl.js");
|
|
17
27
|
const { runRlmLoop } = await import("./rlm.js");
|
|
@@ -68,7 +78,8 @@ class Spinner {
|
|
|
68
78
|
}
|
|
69
79
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
70
80
|
const DEFAULT_MODEL = process.env.RLM_MODEL || "claude-sonnet-4-6";
|
|
71
|
-
const
|
|
81
|
+
const RLM_HOME = path.join(os.homedir(), ".rlm");
|
|
82
|
+
const TRAJ_DIR = path.join(RLM_HOME, "trajectories");
|
|
72
83
|
const W = Math.min(process.stdout.columns || 80, 100);
|
|
73
84
|
// ── Session state ───────────────────────────────────────────────────────────
|
|
74
85
|
let currentModelId = DEFAULT_MODEL;
|
|
@@ -200,19 +211,27 @@ function questionWithEsc(rlInstance, promptText) {
|
|
|
200
211
|
async function promptForProviderKey(rlInstance, providerInfo) {
|
|
201
212
|
if (process.env[providerInfo.env])
|
|
202
213
|
return true;
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
214
|
+
const rawKey = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
|
|
215
|
+
if (rawKey === null)
|
|
205
216
|
return null; // ESC
|
|
206
|
-
if (!
|
|
217
|
+
if (!rawKey)
|
|
207
218
|
return false; // empty
|
|
219
|
+
// Sanitize: strip newlines, control chars, whitespace
|
|
220
|
+
const key = rawKey.replace(/[\r\n\x00-\x1f]/g, "").trim();
|
|
221
|
+
if (!key)
|
|
222
|
+
return false;
|
|
208
223
|
process.env[providerInfo.env] = key;
|
|
209
224
|
// Save to ~/.rlm/credentials (persistent across sessions)
|
|
210
|
-
const
|
|
211
|
-
const credPath = path.join(credDir, "credentials");
|
|
225
|
+
const credPath = path.join(RLM_HOME, "credentials");
|
|
212
226
|
try {
|
|
213
|
-
if (!fs.existsSync(
|
|
214
|
-
fs.mkdirSync(
|
|
227
|
+
if (!fs.existsSync(RLM_HOME))
|
|
228
|
+
fs.mkdirSync(RLM_HOME, { recursive: true });
|
|
215
229
|
fs.appendFileSync(credPath, `${providerInfo.env}=${key}\n`);
|
|
230
|
+
// Restrict permissions (owner-only read/write)
|
|
231
|
+
try {
|
|
232
|
+
fs.chmodSync(credPath, 0o600);
|
|
233
|
+
}
|
|
234
|
+
catch { /* Windows etc. */ }
|
|
216
235
|
console.log(`\n ${c.green}✓${c.reset} ${providerInfo.name} key saved to ${c.dim}~/.rlm/credentials${c.reset}`);
|
|
217
236
|
}
|
|
218
237
|
catch {
|
|
@@ -310,10 +329,15 @@ async function handleFile(arg) {
|
|
|
310
329
|
console.log(` ${c.red}File not found: ${filePath}${c.reset}`);
|
|
311
330
|
return;
|
|
312
331
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
332
|
+
try {
|
|
333
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
334
|
+
contextSource = arg;
|
|
335
|
+
const lines = contextText.split("\n").length;
|
|
336
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from ${c.underline}${arg}${c.reset}`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
340
|
+
}
|
|
317
341
|
}
|
|
318
342
|
async function handleUrl(arg) {
|
|
319
343
|
if (!arg) {
|
|
@@ -689,12 +713,17 @@ async function runQuery(query) {
|
|
|
689
713
|
console.log(boxBottom(c.green));
|
|
690
714
|
console.log();
|
|
691
715
|
// Save trajectory
|
|
692
|
-
|
|
693
|
-
fs.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
716
|
+
try {
|
|
717
|
+
if (!fs.existsSync(TRAJ_DIR))
|
|
718
|
+
fs.mkdirSync(TRAJ_DIR, { recursive: true });
|
|
719
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
720
|
+
const trajFile = `trajectory-${ts}.json`;
|
|
721
|
+
fs.writeFileSync(path.join(TRAJ_DIR, trajFile), JSON.stringify(trajectory, null, 2), "utf-8");
|
|
722
|
+
console.log(` ${c.dim}Saved: ~/.rlm/trajectories/${trajFile}${c.reset}\n`);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
console.log(` ${c.yellow}Could not save trajectory.${c.reset}\n`);
|
|
726
|
+
}
|
|
698
727
|
}
|
|
699
728
|
catch (err) {
|
|
700
729
|
spinner.stop();
|
|
@@ -753,11 +782,17 @@ function expandAtFiles(input) {
|
|
|
753
782
|
if (atMatch) {
|
|
754
783
|
const filePath = path.resolve(atMatch[1]);
|
|
755
784
|
if (fs.existsSync(filePath)) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
785
|
+
try {
|
|
786
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
787
|
+
contextSource = atMatch[1];
|
|
788
|
+
const lines = contextText.split("\n").length;
|
|
789
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${atMatch[1]}${c.reset}`);
|
|
790
|
+
return atMatch[2] || "";
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
794
|
+
return "";
|
|
795
|
+
}
|
|
761
796
|
}
|
|
762
797
|
else {
|
|
763
798
|
console.log(` ${c.red}File not found: ${atMatch[1]}${c.reset}`);
|
|
@@ -869,251 +904,257 @@ async function interactive() {
|
|
|
869
904
|
};
|
|
870
905
|
rl.prompt();
|
|
871
906
|
rl.on("line", async (rawLine) => {
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
907
|
+
try {
|
|
908
|
+
if (isRunning)
|
|
909
|
+
return; // ignore input while a query is active
|
|
910
|
+
const line = rawLine.trim();
|
|
911
|
+
// URL auto-detect
|
|
912
|
+
if (line.startsWith("http://") || line.startsWith("https://")) {
|
|
913
|
+
const loaded = await detectAndLoadUrl(line);
|
|
914
|
+
if (loaded) {
|
|
915
|
+
printStatusLine();
|
|
916
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
917
|
+
rl.prompt();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
883
920
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
921
|
+
// Multi-line paste detect
|
|
922
|
+
if (isMultiLineInput(rawLine)) {
|
|
923
|
+
const result = handleMultiLineAsContext(rawLine);
|
|
924
|
+
if (result) {
|
|
925
|
+
contextText = result.context;
|
|
926
|
+
contextSource = "(pasted)";
|
|
927
|
+
printStatusLine();
|
|
928
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
929
|
+
rl.prompt();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (!line) {
|
|
893
934
|
rl.prompt();
|
|
894
935
|
return;
|
|
895
936
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
937
|
+
// Slash commands
|
|
938
|
+
if (line.startsWith("/")) {
|
|
939
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
940
|
+
const arg = rest.join(" ");
|
|
941
|
+
switch (cmd) {
|
|
942
|
+
case "help":
|
|
943
|
+
case "h":
|
|
944
|
+
printCommandHelp();
|
|
945
|
+
break;
|
|
946
|
+
case "file":
|
|
947
|
+
case "f":
|
|
948
|
+
await handleFile(arg);
|
|
949
|
+
break;
|
|
950
|
+
case "url":
|
|
951
|
+
case "u":
|
|
952
|
+
await handleUrl(arg);
|
|
953
|
+
break;
|
|
954
|
+
case "paste":
|
|
955
|
+
case "p":
|
|
956
|
+
await handlePaste(rl);
|
|
957
|
+
break;
|
|
958
|
+
case "context":
|
|
959
|
+
case "ctx":
|
|
960
|
+
handleContext();
|
|
961
|
+
break;
|
|
962
|
+
case "clear-context":
|
|
963
|
+
case "cc":
|
|
964
|
+
contextText = "";
|
|
965
|
+
contextSource = "";
|
|
966
|
+
console.log(` ${c.green}✓${c.reset} Context cleared.`);
|
|
967
|
+
break;
|
|
968
|
+
case "model":
|
|
969
|
+
case "m": {
|
|
970
|
+
const curProvider = detectProvider();
|
|
971
|
+
if (arg) {
|
|
972
|
+
// Accept a number (from current provider list) or a model ID
|
|
973
|
+
const curModels = getModelsForProvider(curProvider);
|
|
974
|
+
let pick;
|
|
975
|
+
if (/^\d+$/.test(arg)) {
|
|
976
|
+
pick = curModels[parseInt(arg, 10) - 1]?.id;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
pick = arg;
|
|
980
|
+
}
|
|
981
|
+
if (!pick) {
|
|
982
|
+
console.log(` ${c.red}Invalid selection.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
// Check if this model belongs to a different provider
|
|
986
|
+
const resolved = resolveModelWithProvider(pick);
|
|
987
|
+
if (!resolved) {
|
|
988
|
+
console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
if (resolved.provider !== curProvider) {
|
|
992
|
+
// Cross-provider switch
|
|
993
|
+
const setupInfo = findSetupProvider(resolved.provider);
|
|
994
|
+
const envVar = setupInfo?.env || providerEnvKey(resolved.provider);
|
|
995
|
+
const provName = setupInfo?.name || resolved.provider;
|
|
996
|
+
if (!process.env[envVar]) {
|
|
997
|
+
console.log(` ${c.yellow}That model requires ${provName}.${c.reset}`);
|
|
998
|
+
const gotKey = await promptForProviderKey(rl, { name: provName, env: envVar });
|
|
999
|
+
if (!gotKey) {
|
|
1000
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
currentModelId = pick;
|
|
1006
|
+
currentModel = resolved.model;
|
|
1007
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
1008
|
+
console.log();
|
|
1009
|
+
printStatusLine();
|
|
941
1010
|
}
|
|
942
1011
|
else {
|
|
943
|
-
|
|
1012
|
+
// List models for current provider
|
|
1013
|
+
const models = getModelsForProvider(curProvider);
|
|
1014
|
+
const provLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
1015
|
+
console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
|
|
1016
|
+
const pad = String(models.length).length;
|
|
1017
|
+
for (let i = 0; i < models.length; i++) {
|
|
1018
|
+
const m = models[i];
|
|
1019
|
+
const num = String(i + 1).padStart(pad);
|
|
1020
|
+
const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
|
|
1021
|
+
const label = m.id === currentModelId
|
|
1022
|
+
? `${c.cyan}${m.id}${c.reset}`
|
|
1023
|
+
: `${c.dim}${m.id}${c.reset}`;
|
|
1024
|
+
console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
|
|
1025
|
+
}
|
|
1026
|
+
console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
|
|
1027
|
+
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}`);
|
|
1028
|
+
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case "provider":
|
|
1033
|
+
case "prov": {
|
|
1034
|
+
const curProvider = detectProvider();
|
|
1035
|
+
const curLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
1036
|
+
console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
|
|
1037
|
+
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
1038
|
+
const p = SETUP_PROVIDERS[i];
|
|
1039
|
+
const isCurrent = p.piProvider === curProvider;
|
|
1040
|
+
const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
|
|
1041
|
+
// Only show ✓ for non-current providers that have a key
|
|
1042
|
+
const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
|
|
1043
|
+
const label = isCurrent
|
|
1044
|
+
? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
|
|
1045
|
+
: `${p.name} ${c.dim}(${p.label})${c.reset}`;
|
|
1046
|
+
console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
|
|
944
1047
|
}
|
|
945
|
-
|
|
946
|
-
|
|
1048
|
+
console.log();
|
|
1049
|
+
const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
|
|
1050
|
+
if (provChoice === null)
|
|
1051
|
+
break; // ESC
|
|
1052
|
+
const idx = parseInt(provChoice, 10) - 1;
|
|
1053
|
+
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
1054
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
947
1055
|
break;
|
|
948
1056
|
}
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
if (!
|
|
952
|
-
|
|
1057
|
+
const chosen = SETUP_PROVIDERS[idx];
|
|
1058
|
+
const gotKey = await promptForProviderKey(rl, chosen);
|
|
1059
|
+
if (!gotKey) {
|
|
1060
|
+
// null (ESC) or false (empty) → cancel
|
|
953
1061
|
break;
|
|
954
1062
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
1063
|
+
// Auto-select first model from new provider
|
|
1064
|
+
const defaultModel = getDefaultModelForProvider(chosen.piProvider);
|
|
1065
|
+
if (defaultModel) {
|
|
1066
|
+
currentModelId = defaultModel;
|
|
1067
|
+
currentModel = resolveModel(currentModelId);
|
|
1068
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
|
|
1069
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
1070
|
+
console.log();
|
|
1071
|
+
printStatusLine();
|
|
968
1072
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
972
|
-
console.log();
|
|
973
|
-
printStatusLine();
|
|
974
|
-
}
|
|
975
|
-
else {
|
|
976
|
-
// List models for current provider
|
|
977
|
-
const models = getModelsForProvider(curProvider);
|
|
978
|
-
const provLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
979
|
-
console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
|
|
980
|
-
const pad = String(models.length).length;
|
|
981
|
-
for (let i = 0; i < models.length; i++) {
|
|
982
|
-
const m = models[i];
|
|
983
|
-
const num = String(i + 1).padStart(pad);
|
|
984
|
-
const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
|
|
985
|
-
const label = m.id === currentModelId
|
|
986
|
-
? `${c.cyan}${m.id}${c.reset}`
|
|
987
|
-
: `${c.dim}${m.id}${c.reset}`;
|
|
988
|
-
console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
|
|
1073
|
+
else {
|
|
1074
|
+
console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
|
|
989
1075
|
}
|
|
990
|
-
console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
|
|
991
|
-
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}`);
|
|
992
|
-
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
|
|
993
|
-
}
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
case "provider":
|
|
997
|
-
case "prov": {
|
|
998
|
-
const curProvider = detectProvider();
|
|
999
|
-
const curLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
1000
|
-
console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
|
|
1001
|
-
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
1002
|
-
const p = SETUP_PROVIDERS[i];
|
|
1003
|
-
const isCurrent = p.piProvider === curProvider;
|
|
1004
|
-
const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
|
|
1005
|
-
// Only show ✓ for non-current providers that have a key
|
|
1006
|
-
const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
|
|
1007
|
-
const label = isCurrent
|
|
1008
|
-
? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
|
|
1009
|
-
: `${p.name} ${c.dim}(${p.label})${c.reset}`;
|
|
1010
|
-
console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
|
|
1011
|
-
}
|
|
1012
|
-
console.log();
|
|
1013
|
-
const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
|
|
1014
|
-
if (provChoice === null)
|
|
1015
|
-
break; // ESC
|
|
1016
|
-
const idx = parseInt(provChoice, 10) - 1;
|
|
1017
|
-
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
1018
|
-
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
1019
1076
|
break;
|
|
1020
1077
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
// null (ESC) or false (empty) → cancel
|
|
1078
|
+
case "trajectories":
|
|
1079
|
+
case "traj":
|
|
1080
|
+
handleTrajectories();
|
|
1025
1081
|
break;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
console.log(
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
else {
|
|
1038
|
-
console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
|
|
1039
|
-
}
|
|
1040
|
-
break;
|
|
1082
|
+
case "clear":
|
|
1083
|
+
printWelcome();
|
|
1084
|
+
break;
|
|
1085
|
+
case "quit":
|
|
1086
|
+
case "q":
|
|
1087
|
+
case "exit":
|
|
1088
|
+
console.log(`\n ${c.dim}Goodbye!${c.reset}\n`);
|
|
1089
|
+
process.exit(0);
|
|
1090
|
+
break;
|
|
1091
|
+
default:
|
|
1092
|
+
console.log(` ${c.red}Unknown command: /${cmd}${c.reset}. Type ${c.cyan}/help${c.reset} for commands.`);
|
|
1041
1093
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
handleTrajectories();
|
|
1045
|
-
break;
|
|
1046
|
-
case "clear":
|
|
1047
|
-
printWelcome();
|
|
1048
|
-
break;
|
|
1049
|
-
case "quit":
|
|
1050
|
-
case "q":
|
|
1051
|
-
case "exit":
|
|
1052
|
-
console.log(`\n ${c.dim}Goodbye!${c.reset}\n`);
|
|
1053
|
-
process.exit(0);
|
|
1054
|
-
break;
|
|
1055
|
-
default:
|
|
1056
|
-
console.log(` ${c.red}Unknown command: /${cmd}${c.reset}. Type ${c.cyan}/help${c.reset} for commands.`);
|
|
1094
|
+
rl.prompt();
|
|
1095
|
+
return;
|
|
1057
1096
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1097
|
+
// @file shorthand
|
|
1098
|
+
let query = expandAtFiles(line);
|
|
1099
|
+
if (!query && line.startsWith("@")) {
|
|
1100
|
+
rl.prompt();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (!query)
|
|
1104
|
+
query = line;
|
|
1105
|
+
// Inline URL detection — extract URL from query, fetch as context
|
|
1106
|
+
if (!contextText) {
|
|
1107
|
+
const urlInline = query.match(/(https?:\/\/\S+)/);
|
|
1108
|
+
if (urlInline) {
|
|
1109
|
+
const url = urlInline[1];
|
|
1110
|
+
const queryWithoutUrl = query.replace(url, "").trim();
|
|
1111
|
+
console.log(` ${c.dim}Fetching ${url}...${c.reset}`);
|
|
1112
|
+
try {
|
|
1113
|
+
const resp = await fetch(url);
|
|
1114
|
+
if (!resp.ok)
|
|
1115
|
+
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
1116
|
+
contextText = await resp.text();
|
|
1117
|
+
contextSource = url;
|
|
1118
|
+
const lines = contextText.split("\n").length;
|
|
1119
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from URL`);
|
|
1120
|
+
if (queryWithoutUrl) {
|
|
1121
|
+
query = queryWithoutUrl;
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
// URL only, no query — prompt for one
|
|
1125
|
+
printStatusLine();
|
|
1126
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
1127
|
+
rl.prompt();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1086
1130
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
1091
|
-
rl.prompt();
|
|
1092
|
-
return;
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
console.log(` ${c.red}Failed to fetch URL: ${err.message}${c.reset}`);
|
|
1133
|
+
console.log(` ${c.dim}Running query as-is...${c.reset}`);
|
|
1093
1134
|
}
|
|
1094
1135
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1136
|
+
}
|
|
1137
|
+
// Auto-detect file paths
|
|
1138
|
+
if (!contextText) {
|
|
1139
|
+
const { filePath, query: extractedQuery } = extractFilePath(query);
|
|
1140
|
+
if (filePath) {
|
|
1141
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
1142
|
+
contextSource = path.basename(filePath);
|
|
1143
|
+
const lines = contextText.split("\n").length;
|
|
1144
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${filePath}${c.reset}`);
|
|
1145
|
+
query = extractedQuery || query;
|
|
1098
1146
|
}
|
|
1099
1147
|
}
|
|
1148
|
+
// Run query
|
|
1149
|
+
await runQuery(query);
|
|
1150
|
+
printStatusLine();
|
|
1151
|
+
console.log();
|
|
1152
|
+
rl.prompt();
|
|
1100
1153
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (filePath) {
|
|
1105
|
-
contextText = fs.readFileSync(filePath, "utf-8");
|
|
1106
|
-
contextSource = path.basename(filePath);
|
|
1107
|
-
const lines = contextText.split("\n").length;
|
|
1108
|
-
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${filePath}${c.reset}`);
|
|
1109
|
-
query = extractedQuery || query;
|
|
1110
|
-
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
console.log(`\n ${c.red}Error: ${err?.message || err}${c.reset}\n`);
|
|
1156
|
+
rl.prompt();
|
|
1111
1157
|
}
|
|
1112
|
-
// Run query
|
|
1113
|
-
await runQuery(query);
|
|
1114
|
-
printStatusLine();
|
|
1115
|
-
console.log();
|
|
1116
|
-
rl.prompt();
|
|
1117
1158
|
});
|
|
1118
1159
|
// Ctrl+C: abort running query, or double-tap to exit
|
|
1119
1160
|
let lastSigint = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rlm-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,14 @@
|
|
|
30
30
|
"cli",
|
|
31
31
|
"agent"
|
|
32
32
|
],
|
|
33
|
+
"author": "Vipul Maheshwari <vim.code.level@gmail.com>",
|
|
33
34
|
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/viplismism/rlm-cli.git"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/viplismism/rlm-cli",
|
|
40
|
+
"bugs": "https://github.com/viplismism/rlm-cli/issues",
|
|
34
41
|
"dependencies": {
|
|
35
42
|
"@mariozechner/pi-ai": "^0.55.1"
|
|
36
43
|
},
|