rlm-cli 0.2.16 → 0.2.18

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # rlm-cli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/rlm-cli.svg)](https://www.npmjs.com/package/rlm-cli)
4
+
3
5
  ```
4
6
  ██████╗ ██╗ ███╗ ███╗
5
7
  ██╔══██╗██║ ████╗ ████║
package/dist/cli.js CHANGED
@@ -91,9 +91,15 @@ function parseArgs() {
91
91
  return { modelId, file, url, useStdin, verbose, query };
92
92
  }
93
93
  // ── Helpers ─────────────────────────────────────────────────────────────────
94
+ const MAX_STDIN_BYTES = 50 * 1024 * 1024; // 50MB
94
95
  async function readStdin() {
95
96
  const chunks = [];
97
+ let total = 0;
96
98
  for await (const chunk of process.stdin) {
99
+ total += chunk.length;
100
+ if (total > MAX_STDIN_BYTES) {
101
+ throw new Error(`stdin exceeds ${MAX_STDIN_BYTES / 1024 / 1024}MB limit`);
102
+ }
97
103
  chunks.push(chunk);
98
104
  }
99
105
  return Buffer.concat(chunks).toString("utf-8");
@@ -129,7 +135,13 @@ async function main() {
129
135
  let context;
130
136
  if (args.file) {
131
137
  console.error(`Reading context from file: ${args.file}`);
132
- context = fs.readFileSync(args.file, "utf-8");
138
+ try {
139
+ context = fs.readFileSync(args.file, "utf-8");
140
+ }
141
+ catch (err) {
142
+ console.error(`Error: could not read file "${args.file}": ${err.message}`);
143
+ process.exit(1);
144
+ }
133
145
  }
134
146
  else if (args.url) {
135
147
  console.error(`Fetching context from URL: ${args.url}`);
@@ -652,11 +652,11 @@ function isBinaryFile(filePath) {
652
652
  if (BINARY_EXTENSIONS.has(ext))
653
653
  return true;
654
654
  // Quick null-byte check on first 512 bytes
655
+ let fd;
655
656
  try {
656
- const fd = fs.openSync(filePath, "r");
657
+ fd = fs.openSync(filePath, "r");
657
658
  const buf = Buffer.alloc(512);
658
659
  const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
659
- fs.closeSync(fd);
660
660
  for (let i = 0; i < bytesRead; i++) {
661
661
  if (buf[i] === 0)
662
662
  return true;
@@ -665,9 +665,19 @@ function isBinaryFile(filePath) {
665
665
  catch { /* unreadable → skip */
666
666
  return true;
667
667
  }
668
+ finally {
669
+ if (fd !== undefined)
670
+ try {
671
+ fs.closeSync(fd);
672
+ }
673
+ catch { }
674
+ }
668
675
  return false;
669
676
  }
670
- function walkDir(dir) {
677
+ const MAX_DIR_DEPTH = 30;
678
+ function walkDir(dir, depth = 0) {
679
+ if (depth > MAX_DIR_DEPTH)
680
+ return [];
671
681
  const results = [];
672
682
  let entries;
673
683
  try {
@@ -679,11 +689,13 @@ function walkDir(dir) {
679
689
  for (const entry of entries) {
680
690
  if (entry.name.startsWith(".") && entry.name !== ".env")
681
691
  continue;
692
+ if (entry.isSymbolicLink())
693
+ continue;
682
694
  const full = path.join(dir, entry.name);
683
695
  if (entry.isDirectory()) {
684
696
  if (SKIP_DIRS.has(entry.name))
685
697
  continue;
686
- results.push(...walkDir(full));
698
+ results.push(...walkDir(full, depth + 1));
687
699
  }
688
700
  else if (entry.isFile()) {
689
701
  if (!isBinaryFile(full))
@@ -694,12 +706,12 @@ function walkDir(dir) {
694
706
  }
695
707
  return results;
696
708
  }
697
- function simpleGlobMatch(pattern, filePath) {
698
- // Expand {a,b,c} braces into alternatives
709
+ function simpleGlobMatch(pattern, filePath, _braceDepth = 0) {
710
+ // Expand {a,b,c} braces into alternatives (with depth limit)
699
711
  const braceMatch = pattern.match(/\{([^}]+)\}/);
700
- if (braceMatch) {
701
- const alternatives = braceMatch[1].split(",");
702
- return alternatives.some((alt) => simpleGlobMatch(pattern.replace(braceMatch[0], alt.trim()), filePath));
712
+ if (braceMatch && _braceDepth < 5) {
713
+ const alternatives = braceMatch[1].split(",").slice(0, 50);
714
+ return alternatives.some((alt) => simpleGlobMatch(pattern.replace(braceMatch[0], alt.trim()), filePath, _braceDepth + 1));
703
715
  }
704
716
  // Convert glob to regex
705
717
  let regex = "^";
@@ -1092,6 +1104,7 @@ async function interactive() {
1092
1104
  process.exit(0);
1093
1105
  }
1094
1106
  // Auto-select default model for chosen provider
1107
+ currentProviderName = provider.piProvider;
1095
1108
  const defaultModel = getDefaultModelForProvider(provider.piProvider);
1096
1109
  if (defaultModel) {
1097
1110
  currentModelId = defaultModel;
@@ -1200,7 +1213,7 @@ async function interactive() {
1200
1213
  break;
1201
1214
  case "model":
1202
1215
  case "m": {
1203
- const curProvider = detectProvider();
1216
+ const curProvider = currentProviderName || detectProvider();
1204
1217
  if (arg) {
1205
1218
  // Accept a number (from current provider list) or a model ID
1206
1219
  const curModels = getModelsForProvider(curProvider);
@@ -1265,7 +1278,7 @@ async function interactive() {
1265
1278
  }
1266
1279
  case "provider":
1267
1280
  case "prov": {
1268
- const curProvider = detectProvider();
1281
+ const curProvider = currentProviderName || detectProvider();
1269
1282
  const curLabel = findSetupProvider(curProvider)?.name || curProvider;
1270
1283
  console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
1271
1284
  for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
@@ -1300,7 +1313,7 @@ async function interactive() {
1300
1313
  currentModelId = defaultModel;
1301
1314
  const provResolved = resolveModelWithProvider(currentModelId);
1302
1315
  currentModel = provResolved?.model;
1303
- currentProviderName = chosen.piProvider;
1316
+ currentProviderName = provResolved?.provider || chosen.piProvider;
1304
1317
  console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
1305
1318
  console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
1306
1319
  console.log();
package/dist/repl.js CHANGED
@@ -32,13 +32,18 @@ export class PythonRepl {
32
32
  return;
33
33
  const runtimePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "runtime.py");
34
34
  const pythonCmd = process.platform === "win32" ? "python" : "python3";
35
+ const homeDir = os.homedir();
35
36
  this.proc = spawn(pythonCmd, [runtimePath], {
36
37
  stdio: ["pipe", "pipe", "pipe"],
37
38
  env: {
38
39
  // Only pass what Python actually needs — not API keys or secrets
39
40
  PATH: process.env.PATH,
40
- HOME: os.homedir(),
41
+ HOME: homeDir,
42
+ USERPROFILE: homeDir, // Windows uses USERPROFILE
41
43
  PYTHONUNBUFFERED: "1",
44
+ // Windows needs SystemRoot/SYSTEMROOT for Python to find DLLs
45
+ ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
46
+ ...(process.env.SYSTEMROOT ? { SYSTEMROOT: process.env.SYSTEMROOT } : {}),
42
47
  },
43
48
  });
44
49
  this.rl = readline.createInterface({ input: this.proc.stdout });
@@ -93,7 +98,11 @@ export class PythonRepl {
93
98
  catch {
94
99
  // stdin may already be closed
95
100
  }
96
- this.proc.kill("SIGTERM");
101
+ // SIGTERM is ignored on Windows; use SIGKILL as fallback
102
+ try {
103
+ this.proc.kill(process.platform === "win32" ? "SIGKILL" : "SIGTERM");
104
+ }
105
+ catch { /* already dead */ }
97
106
  }
98
107
  this.cleanup();
99
108
  }
package/dist/rlm.js CHANGED
@@ -247,7 +247,7 @@ export async function runRlmLoop(options) {
247
247
  const errMsg = response.errorMessage;
248
248
  if (errMsg.includes("authentication") || errMsg.includes("401")) {
249
249
  return {
250
- answer: `[API Authentication Error] ${errMsg}\n\nCheck your ANTHROPIC_API_KEY in .env.`,
250
+ answer: `[API Authentication Error] ${errMsg}\n\nCheck your API key in .env or run /provider to reconfigure.`,
251
251
  iterations: iteration,
252
252
  totalSubQueries,
253
253
  completed: false,
package/dist/viewer.js CHANGED
@@ -614,9 +614,17 @@ function syntaxHighlight(code) {
614
614
  async function main() {
615
615
  // Enter alternate screen buffer so output never scrolls the main terminal
616
616
  W(c.altScreenOn);
617
- // Ensure we always leave alt screen on exit
618
- const cleanup = () => W(c.showCursor, c.altScreenOff);
617
+ // Ensure we always restore terminal on exit (alt screen, cursor, raw mode)
618
+ const cleanup = () => {
619
+ try {
620
+ process.stdin.setRawMode(false);
621
+ }
622
+ catch { }
623
+ W(c.showCursor, c.altScreenOff);
624
+ };
619
625
  process.on("exit", cleanup);
626
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
627
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
620
628
  let filePath = process.argv[2];
621
629
  if (!filePath) {
622
630
  const files = listTrajectories();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlm-cli",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {