rlm-cli 0.2.17 → 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 = "^";
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/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.17",
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": {