rlm-cli 0.2.17 → 0.2.19

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 {
@@ -677,29 +687,40 @@ function walkDir(dir) {
677
687
  return results;
678
688
  }
679
689
  for (const entry of entries) {
690
+ if (results.length >= MAX_FILES)
691
+ break;
680
692
  if (entry.name.startsWith(".") && entry.name !== ".env")
681
693
  continue;
694
+ if (entry.isSymbolicLink())
695
+ continue;
682
696
  const full = path.join(dir, entry.name);
683
697
  if (entry.isDirectory()) {
684
698
  if (SKIP_DIRS.has(entry.name))
685
699
  continue;
686
- results.push(...walkDir(full));
700
+ const sub = walkDir(full, depth + 1);
701
+ const remaining = MAX_FILES - results.length;
702
+ results.push(...sub.slice(0, remaining));
687
703
  }
688
704
  else if (entry.isFile()) {
689
705
  if (!isBinaryFile(full))
690
706
  results.push(full);
691
707
  }
692
- if (results.length > MAX_FILES)
693
- break;
694
708
  }
695
709
  return results;
696
710
  }
697
- function simpleGlobMatch(pattern, filePath) {
698
- // Expand {a,b,c} braces into alternatives
711
+ /** Normalize path separators to forward slash for consistent matching. */
712
+ function toForwardSlash(p) {
713
+ return p.replace(/\\/g, "/");
714
+ }
715
+ function simpleGlobMatch(pattern, filePath, _braceDepth = 0) {
716
+ // Normalize both to forward slashes for cross-platform matching
717
+ pattern = toForwardSlash(pattern);
718
+ filePath = toForwardSlash(filePath);
719
+ // Expand {a,b,c} braces into alternatives (with depth limit)
699
720
  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));
721
+ if (braceMatch && _braceDepth < 5) {
722
+ const alternatives = braceMatch[1].split(",").slice(0, 50);
723
+ return alternatives.some((alt) => simpleGlobMatch(pattern.replace(braceMatch[0], alt.trim()), filePath, _braceDepth + 1));
703
724
  }
704
725
  // Convert glob to regex
705
726
  let regex = "^";
@@ -740,8 +761,9 @@ function resolveFileArgs(args) {
740
761
  // Glob pattern (contains * or ?)
741
762
  if (arg.includes("*") || arg.includes("?")) {
742
763
  // Find the base directory (portion before the first glob char)
743
- const firstGlob = arg.search(/[*?{]/);
744
- const baseDir = firstGlob > 0 ? path.resolve(arg.slice(0, arg.lastIndexOf("/", firstGlob) + 1) || ".") : process.cwd();
764
+ const normalized = toForwardSlash(arg);
765
+ const firstGlob = normalized.search(/[*?{]/);
766
+ const baseDir = firstGlob > 0 ? path.resolve(normalized.slice(0, normalized.lastIndexOf("/", firstGlob) + 1) || ".") : process.cwd();
745
767
  const allFiles = walkDir(baseDir);
746
768
  for (const f of allFiles) {
747
769
  const rel = path.relative(process.cwd(), f);
@@ -971,7 +993,8 @@ function extractFilePath(input) {
971
993
  return { filePath, query };
972
994
  }
973
995
  }
974
- const absPathMatch = input.match(/(\/[^\s]+)/);
996
+ // Unix absolute path (/...) or Windows absolute path (C:\...)
997
+ const absPathMatch = input.match(/((?:\/|[A-Za-z]:[\\\/])[^\s]+)/);
975
998
  if (absPathMatch) {
976
999
  const filePath = absPathMatch[1];
977
1000
  if (fs.existsSync(filePath)) {
@@ -979,7 +1002,8 @@ function extractFilePath(input) {
979
1002
  return { filePath, query };
980
1003
  }
981
1004
  }
982
- const relPathMatch = input.match(/([\w\-\.]+\/[\w\-\./]+\.\w{2,6})/);
1005
+ // Relative path with forward or back slashes
1006
+ const relPathMatch = input.match(/([\w\-\.]+[\/\\][\w\-\.\/\\]+\.\w{2,6})/);
983
1007
  if (relPathMatch) {
984
1008
  const filePath = path.resolve(relPathMatch[1]);
985
1009
  if (fs.existsSync(filePath)) {
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.19",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {