rlm-cli 0.2.10 → 0.2.11

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 +284 -243
  2. package/package.json +1 -1
@@ -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 TRAJ_DIR = path.resolve(process.cwd(), "trajectories");
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 key = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
204
- if (key === null)
214
+ const rawKey = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
215
+ if (rawKey === null)
205
216
  return null; // ESC
206
- if (!key)
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 credDir = path.join(process.env.HOME || "~", ".rlm");
211
- const credPath = path.join(credDir, "credentials");
225
+ const credPath = path.join(RLM_HOME, "credentials");
212
226
  try {
213
- if (!fs.existsSync(credDir))
214
- fs.mkdirSync(credDir, { recursive: true });
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
- contextText = fs.readFileSync(filePath, "utf-8");
314
- contextSource = arg;
315
- const lines = contextText.split("\n").length;
316
- console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from ${c.underline}${arg}${c.reset}`);
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
- if (!fs.existsSync(TRAJ_DIR))
693
- fs.mkdirSync(TRAJ_DIR, { recursive: true });
694
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
695
- const trajFile = `trajectory-${ts}.json`;
696
- fs.writeFileSync(path.join(TRAJ_DIR, trajFile), JSON.stringify(trajectory, null, 2), "utf-8");
697
- console.log(` ${c.dim}Saved: ${trajFile}${c.reset}\n`);
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
- contextText = fs.readFileSync(filePath, "utf-8");
757
- contextSource = atMatch[1];
758
- const lines = contextText.split("\n").length;
759
- console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${atMatch[1]}${c.reset}`);
760
- return atMatch[2] || "";
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
- if (isRunning)
873
- return; // ignore input while a query is active
874
- const line = rawLine.trim();
875
- // URL auto-detect
876
- if (line.startsWith("http://") || line.startsWith("https://")) {
877
- const loaded = await detectAndLoadUrl(line);
878
- if (loaded) {
879
- printStatusLine();
880
- console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
881
- rl.prompt();
882
- return;
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
- // Multi-line paste detect
886
- if (isMultiLineInput(rawLine)) {
887
- const result = handleMultiLineAsContext(rawLine);
888
- if (result) {
889
- contextText = result.context;
890
- contextSource = "(pasted)";
891
- printStatusLine();
892
- console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
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
- if (!line) {
898
- rl.prompt();
899
- return;
900
- }
901
- // Slash commands
902
- if (line.startsWith("/")) {
903
- const [cmd, ...rest] = line.slice(1).split(/\s+/);
904
- const arg = rest.join(" ");
905
- switch (cmd) {
906
- case "help":
907
- case "h":
908
- printCommandHelp();
909
- break;
910
- case "file":
911
- case "f":
912
- await handleFile(arg);
913
- break;
914
- case "url":
915
- case "u":
916
- await handleUrl(arg);
917
- break;
918
- case "paste":
919
- case "p":
920
- await handlePaste(rl);
921
- break;
922
- case "context":
923
- case "ctx":
924
- handleContext();
925
- break;
926
- case "clear-context":
927
- case "cc":
928
- contextText = "";
929
- contextSource = "";
930
- console.log(` ${c.green}✓${c.reset} Context cleared.`);
931
- break;
932
- case "model":
933
- case "m": {
934
- const curProvider = detectProvider();
935
- if (arg) {
936
- // Accept a number (from current provider list) or a model ID
937
- const curModels = getModelsForProvider(curProvider);
938
- let pick;
939
- if (/^\d+$/.test(arg)) {
940
- pick = curModels[parseInt(arg, 10) - 1]?.id;
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
- pick = arg;
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
- if (!pick) {
946
- console.log(` ${c.red}Invalid selection.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
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
- // Check if this model belongs to a different provider
950
- const resolved = resolveModelWithProvider(pick);
951
- if (!resolved) {
952
- console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
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
- if (resolved.provider !== curProvider) {
956
- // Cross-provider switch
957
- const setupInfo = findSetupProvider(resolved.provider);
958
- const envVar = setupInfo?.env || providerEnvKey(resolved.provider);
959
- const provName = setupInfo?.name || resolved.provider;
960
- if (!process.env[envVar]) {
961
- console.log(` ${c.yellow}That model requires ${provName}.${c.reset}`);
962
- const gotKey = await promptForProviderKey(rl, { name: provName, env: envVar });
963
- if (!gotKey) {
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
- currentModelId = pick;
970
- currentModel = resolved.model;
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
- const chosen = SETUP_PROVIDERS[idx];
1022
- const gotKey = await promptForProviderKey(rl, chosen);
1023
- if (!gotKey) {
1024
- // null (ESC) or false (empty) → cancel
1078
+ case "trajectories":
1079
+ case "traj":
1080
+ handleTrajectories();
1025
1081
  break;
1026
- }
1027
- // Auto-select first model from new provider
1028
- const defaultModel = getDefaultModelForProvider(chosen.piProvider);
1029
- if (defaultModel) {
1030
- currentModelId = defaultModel;
1031
- currentModel = resolveModel(currentModelId);
1032
- console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
1033
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1034
- console.log();
1035
- printStatusLine();
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
- case "trajectories":
1043
- case "traj":
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
- rl.prompt();
1059
- return;
1060
- }
1061
- // @file shorthand
1062
- let query = expandAtFiles(line);
1063
- if (!query && line.startsWith("@")) {
1064
- rl.prompt();
1065
- return;
1066
- }
1067
- if (!query)
1068
- query = line;
1069
- // Inline URL detection — extract URL from query, fetch as context
1070
- if (!contextText) {
1071
- const urlInline = query.match(/(https?:\/\/\S+)/);
1072
- if (urlInline) {
1073
- const url = urlInline[1];
1074
- const queryWithoutUrl = query.replace(url, "").trim();
1075
- console.log(` ${c.dim}Fetching ${url}...${c.reset}`);
1076
- try {
1077
- const resp = await fetch(url);
1078
- if (!resp.ok)
1079
- throw new Error(`${resp.status} ${resp.statusText}`);
1080
- contextText = await resp.text();
1081
- contextSource = url;
1082
- const lines = contextText.split("\n").length;
1083
- console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from URL`);
1084
- if (queryWithoutUrl) {
1085
- query = queryWithoutUrl;
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
- else {
1088
- // URL only, no query — prompt for one
1089
- printStatusLine();
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
- catch (err) {
1096
- console.log(` ${c.red}Failed to fetch URL: ${err.message}${c.reset}`);
1097
- console.log(` ${c.dim}Running query as-is...${c.reset}`);
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
- // Auto-detect file paths
1102
- if (!contextText) {
1103
- const { filePath, query: extractedQuery } = extractFilePath(query);
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.10",
3
+ "version": "0.2.11",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {