rlm-cli 0.2.14 → 0.2.16
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 +35 -0
- package/dist/interactive.js +295 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,41 @@ Or set context first, then ask multiple questions:
|
|
|
83
83
|
|
|
84
84
|
Type `/help` inside the terminal for all commands.
|
|
85
85
|
|
|
86
|
+
### Loading Context
|
|
87
|
+
|
|
88
|
+
You can load single files, multiple files, entire directories, or glob patterns as context.
|
|
89
|
+
|
|
90
|
+
**Single file:**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
> @src/main.ts what does this do?
|
|
94
|
+
> /file src/main.ts
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Multiple files:**
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
> @src/main.ts @src/config.ts how do these interact?
|
|
101
|
+
> /file src/main.ts src/config.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Directory** (recursive — skips `node_modules`, `.git`, `dist`, binaries, etc.):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
> @src/ summarize this codebase
|
|
108
|
+
> /file src/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Glob patterns:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
> @src/**/*.ts list all exports
|
|
115
|
+
> /file src/**/*.ts
|
|
116
|
+
> /file lib/*.{js,ts}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Safety limits: max 100 files, max 10MB total. Use `/context` to see what's loaded.
|
|
120
|
+
|
|
86
121
|
### Single-Shot Mode
|
|
87
122
|
|
|
88
123
|
For scripting or one-off queries:
|
package/dist/interactive.js
CHANGED
|
@@ -84,6 +84,7 @@ const W = Math.min(process.stdout.columns || 80, 100);
|
|
|
84
84
|
// ── Session state ───────────────────────────────────────────────────────────
|
|
85
85
|
let currentModelId = DEFAULT_MODEL;
|
|
86
86
|
let currentModel;
|
|
87
|
+
let currentProviderName = "";
|
|
87
88
|
let contextText = "";
|
|
88
89
|
let contextSource = "";
|
|
89
90
|
let queryCount = 0;
|
|
@@ -106,8 +107,8 @@ function resolveModel(modelId) {
|
|
|
106
107
|
const PROVIDER_KEYS = {
|
|
107
108
|
anthropic: "ANTHROPIC_API_KEY",
|
|
108
109
|
openai: "OPENAI_API_KEY",
|
|
109
|
-
google: "
|
|
110
|
-
"google-gemini-cli": "
|
|
110
|
+
google: "GEMINI_API_KEY",
|
|
111
|
+
"google-gemini-cli": "GEMINI_API_KEY",
|
|
111
112
|
"google-vertex": "GOOGLE_VERTEX_API_KEY",
|
|
112
113
|
groq: "GROQ_API_KEY",
|
|
113
114
|
xai: "XAI_API_KEY",
|
|
@@ -120,7 +121,7 @@ const PROVIDER_KEYS = {
|
|
|
120
121
|
const SETUP_PROVIDERS = [
|
|
121
122
|
{ name: "Anthropic", label: "Claude", env: "ANTHROPIC_API_KEY", piProvider: "anthropic" },
|
|
122
123
|
{ name: "OpenAI", label: "GPT", env: "OPENAI_API_KEY", piProvider: "openai" },
|
|
123
|
-
{ name: "Google", label: "Gemini", env: "
|
|
124
|
+
{ name: "Google", label: "Gemini", env: "GEMINI_API_KEY", piProvider: "google" },
|
|
124
125
|
{ name: "Groq", label: "Groq", env: "GROQ_API_KEY", piProvider: "groq" },
|
|
125
126
|
{ name: "xAI", label: "Grok", env: "XAI_API_KEY", piProvider: "xai" },
|
|
126
127
|
{ name: "Mistral", label: "Mistral", env: "MISTRAL_API_KEY", piProvider: "mistral" },
|
|
@@ -273,7 +274,7 @@ ${c.dim} Recursive Language Models — arXiv:2512.24601${c.reset}
|
|
|
273
274
|
}
|
|
274
275
|
// ── Status line ─────────────────────────────────────────────────────────────
|
|
275
276
|
function printStatusLine() {
|
|
276
|
-
const provider = detectProvider();
|
|
277
|
+
const provider = currentProviderName || detectProvider();
|
|
277
278
|
const modelShort = currentModelId.length > 35
|
|
278
279
|
? currentModelId.slice(0, 32) + "..."
|
|
279
280
|
: currentModelId;
|
|
@@ -295,48 +296,80 @@ function printWelcome() {
|
|
|
295
296
|
// ── Help ────────────────────────────────────────────────────────────────────
|
|
296
297
|
function printCommandHelp() {
|
|
297
298
|
console.log(`
|
|
298
|
-
${c.bold}Context${c.reset}
|
|
299
|
-
${c.cyan}/file${c.reset} <path>
|
|
300
|
-
${c.cyan}/
|
|
301
|
-
${c.cyan}/
|
|
302
|
-
${c.cyan}/
|
|
303
|
-
${c.cyan}/
|
|
299
|
+
${c.bold}Loading Context${c.reset}
|
|
300
|
+
${c.cyan}/file${c.reset} <path> Load a single file
|
|
301
|
+
${c.cyan}/file${c.reset} <p1> <p2> ... Load multiple files
|
|
302
|
+
${c.cyan}/file${c.reset} <dir>/ Load all files in a directory (recursive)
|
|
303
|
+
${c.cyan}/file${c.reset} src/**/*.ts Load files matching a glob pattern
|
|
304
|
+
${c.cyan}/url${c.reset} <url> Fetch URL as context
|
|
305
|
+
${c.cyan}/paste${c.reset} Multi-line paste mode (type EOF to finish)
|
|
306
|
+
${c.cyan}/context${c.reset} Show loaded context info + file list
|
|
307
|
+
${c.cyan}/clear-context${c.reset} Unload context
|
|
304
308
|
|
|
305
|
-
${c.bold}
|
|
306
|
-
${c.cyan}
|
|
307
|
-
${c.cyan}
|
|
308
|
-
${c.cyan}
|
|
309
|
+
${c.bold}@ Shorthand${c.reset} ${c.dim}(inline file loading)${c.reset}
|
|
310
|
+
${c.cyan}@file.ts${c.reset} <query> Load file and ask in one shot
|
|
311
|
+
${c.cyan}@a.ts @b.ts${c.reset} <query> Load multiple files + query
|
|
312
|
+
${c.cyan}@src/${c.reset} <query> Load directory + query
|
|
313
|
+
${c.cyan}@src/**/*.ts${c.reset} <query> Load glob + query
|
|
314
|
+
|
|
315
|
+
${c.bold}Model & Provider${c.reset}
|
|
316
|
+
${c.cyan}/model${c.reset} List models for current provider
|
|
317
|
+
${c.cyan}/model${c.reset} <#|id> Switch model by number or ID
|
|
318
|
+
${c.cyan}/provider${c.reset} Switch provider (Anthropic, OpenAI, Google, ...)
|
|
309
319
|
|
|
310
320
|
${c.bold}Tools${c.reset}
|
|
311
|
-
${c.cyan}/trajectories${c.reset}
|
|
321
|
+
${c.cyan}/trajectories${c.reset} List saved runs
|
|
312
322
|
|
|
313
323
|
${c.bold}General${c.reset}
|
|
314
|
-
${c.cyan}/clear${c.reset}
|
|
315
|
-
${c.cyan}/help${c.reset}
|
|
316
|
-
${c.cyan}/quit${c.reset}
|
|
324
|
+
${c.cyan}/clear${c.reset} Clear screen
|
|
325
|
+
${c.cyan}/help${c.reset} Show this help
|
|
326
|
+
${c.cyan}/quit${c.reset} Exit
|
|
317
327
|
|
|
318
|
-
|
|
328
|
+
${c.bold}Tips${c.reset}
|
|
329
|
+
${c.dim}•${c.reset} Just type a question — no context needed for general queries
|
|
330
|
+
${c.dim}•${c.reset} Paste a URL directly to fetch it as context
|
|
331
|
+
${c.dim}•${c.reset} Paste 4+ lines of text to set it as context
|
|
332
|
+
${c.dim}•${c.reset} ${c.bold}Ctrl+C${c.reset} stops a running query, ${c.bold}Ctrl+C twice${c.reset} exits
|
|
333
|
+
${c.dim}•${c.reset} Directories skip node_modules, .git, dist, binaries, etc.
|
|
334
|
+
${c.dim}•${c.reset} Limits: ${MAX_FILES} files max, ${MAX_TOTAL_BYTES / 1024 / 1024}MB total
|
|
319
335
|
`);
|
|
320
336
|
}
|
|
321
337
|
// ── Slash command handlers ──────────────────────────────────────────────────
|
|
322
338
|
async function handleFile(arg) {
|
|
323
339
|
if (!arg) {
|
|
324
|
-
console.log(` ${c.red}Usage: /file <path
|
|
340
|
+
console.log(` ${c.red}Usage: /file <path|dir|glob> [...]${c.reset}`);
|
|
341
|
+
console.log(` ${c.dim}Examples: /file src/main.ts | /file src/ | /file src/**/*.ts${c.reset}`);
|
|
325
342
|
return;
|
|
326
343
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
344
|
+
const args = arg.split(/\s+/).filter(Boolean);
|
|
345
|
+
const filePaths = resolveFileArgs(args);
|
|
346
|
+
if (filePaths.length === 0) {
|
|
347
|
+
console.log(` ${c.red}No files found.${c.reset}`);
|
|
330
348
|
return;
|
|
331
349
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
350
|
+
if (filePaths.length === 1) {
|
|
351
|
+
try {
|
|
352
|
+
contextText = fs.readFileSync(filePaths[0], "utf-8");
|
|
353
|
+
contextSource = path.relative(process.cwd(), filePaths[0]) || filePaths[0];
|
|
354
|
+
const lines = contextText.split("\n").length;
|
|
355
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from ${c.underline}${contextSource}${c.reset}`);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
359
|
+
}
|
|
337
360
|
}
|
|
338
|
-
|
|
339
|
-
|
|
361
|
+
else {
|
|
362
|
+
const { text, count, totalBytes } = loadMultipleFiles(filePaths);
|
|
363
|
+
contextText = text;
|
|
364
|
+
contextSource = `${count} files`;
|
|
365
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${count}${c.reset} files (${(totalBytes / 1024).toFixed(1)}KB total)`);
|
|
366
|
+
// Show file list
|
|
367
|
+
for (const fp of filePaths.slice(0, 20)) {
|
|
368
|
+
console.log(` ${c.dim}•${c.reset} ${path.relative(process.cwd(), fp)}`);
|
|
369
|
+
}
|
|
370
|
+
if (filePaths.length > 20) {
|
|
371
|
+
console.log(` ${c.dim}... and ${filePaths.length - 20} more${c.reset}`);
|
|
372
|
+
}
|
|
340
373
|
}
|
|
341
374
|
}
|
|
342
375
|
async function handleUrl(arg) {
|
|
@@ -378,20 +411,35 @@ function handlePaste(rl) {
|
|
|
378
411
|
}
|
|
379
412
|
function handleContext() {
|
|
380
413
|
if (!contextText) {
|
|
381
|
-
console.log(` ${c.dim}No context loaded. Use /file, /url, or /paste.${c.reset}`);
|
|
414
|
+
console.log(` ${c.dim}No context loaded. Use /file, /url, @file, or /paste.${c.reset}`);
|
|
382
415
|
return;
|
|
383
416
|
}
|
|
384
417
|
const lines = contextText.split("\n").length;
|
|
385
|
-
|
|
418
|
+
const sizeKB = (contextText.length / 1024).toFixed(1);
|
|
419
|
+
console.log(` ${c.bold}Context:${c.reset} ${contextText.length.toLocaleString()} chars (${sizeKB}KB), ${lines.toLocaleString()} lines`);
|
|
386
420
|
console.log(` ${c.bold}Source:${c.reset} ${contextSource}`);
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
421
|
+
// For multi-file context, extract and display individual file paths
|
|
422
|
+
const fileSeparators = contextText.match(/^=== .+ ===$/gm);
|
|
423
|
+
if (fileSeparators && fileSeparators.length > 1) {
|
|
424
|
+
console.log(` ${c.bold}Files:${c.reset} ${fileSeparators.length}`);
|
|
425
|
+
for (const sep of fileSeparators.slice(0, 20)) {
|
|
426
|
+
const name = sep.replace(/^=== /, "").replace(/ ===$/, "");
|
|
427
|
+
console.log(` ${c.dim}•${c.reset} ${name}`);
|
|
428
|
+
}
|
|
429
|
+
if (fileSeparators.length > 20) {
|
|
430
|
+
console.log(` ${c.dim}... and ${fileSeparators.length - 20} more${c.reset}`);
|
|
431
|
+
}
|
|
392
432
|
}
|
|
393
|
-
|
|
394
|
-
console.log(
|
|
433
|
+
else {
|
|
434
|
+
console.log();
|
|
435
|
+
const preview = contextText.slice(0, 500);
|
|
436
|
+
const previewLines = preview.split("\n").slice(0, 8);
|
|
437
|
+
for (const l of previewLines) {
|
|
438
|
+
console.log(` ${c.dim}│${c.reset} ${l}`);
|
|
439
|
+
}
|
|
440
|
+
if (contextText.length > 500) {
|
|
441
|
+
console.log(` ${c.dim}│ ...${c.reset}`);
|
|
442
|
+
}
|
|
395
443
|
}
|
|
396
444
|
}
|
|
397
445
|
function handleTrajectories() {
|
|
@@ -580,12 +628,176 @@ function getModelsForProvider(providerName) {
|
|
|
580
628
|
function truncateStr(text, max) {
|
|
581
629
|
return text.length <= max ? text : text.slice(0, max - 3) + "...";
|
|
582
630
|
}
|
|
631
|
+
// ── Multi-file context loading ──────────────────────────────────────────────
|
|
632
|
+
const MAX_FILES = 100;
|
|
633
|
+
const MAX_TOTAL_BYTES = 10 * 1024 * 1024; // 10MB
|
|
634
|
+
const BINARY_EXTENSIONS = new Set([
|
|
635
|
+
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".svg",
|
|
636
|
+
".mp3", ".mp4", ".wav", ".ogg", ".flac", ".avi", ".mov", ".mkv",
|
|
637
|
+
".zip", ".gz", ".tar", ".bz2", ".7z", ".rar", ".xz",
|
|
638
|
+
".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a",
|
|
639
|
+
".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
640
|
+
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
|
641
|
+
".pyc", ".pyo", ".class", ".jar",
|
|
642
|
+
".db", ".sqlite", ".sqlite3",
|
|
643
|
+
".DS_Store",
|
|
644
|
+
]);
|
|
645
|
+
const SKIP_DIRS = new Set([
|
|
646
|
+
"node_modules", ".git", "dist", "build", "__pycache__", ".venv",
|
|
647
|
+
"venv", ".next", ".nuxt", "coverage", ".cache", ".tsc-output",
|
|
648
|
+
".svelte-kit", "target", "out",
|
|
649
|
+
]);
|
|
650
|
+
function isBinaryFile(filePath) {
|
|
651
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
652
|
+
if (BINARY_EXTENSIONS.has(ext))
|
|
653
|
+
return true;
|
|
654
|
+
// Quick null-byte check on first 512 bytes
|
|
655
|
+
try {
|
|
656
|
+
const fd = fs.openSync(filePath, "r");
|
|
657
|
+
const buf = Buffer.alloc(512);
|
|
658
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
659
|
+
fs.closeSync(fd);
|
|
660
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
661
|
+
if (buf[i] === 0)
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch { /* unreadable → skip */
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
function walkDir(dir) {
|
|
671
|
+
const results = [];
|
|
672
|
+
let entries;
|
|
673
|
+
try {
|
|
674
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return results;
|
|
678
|
+
}
|
|
679
|
+
for (const entry of entries) {
|
|
680
|
+
if (entry.name.startsWith(".") && entry.name !== ".env")
|
|
681
|
+
continue;
|
|
682
|
+
const full = path.join(dir, entry.name);
|
|
683
|
+
if (entry.isDirectory()) {
|
|
684
|
+
if (SKIP_DIRS.has(entry.name))
|
|
685
|
+
continue;
|
|
686
|
+
results.push(...walkDir(full));
|
|
687
|
+
}
|
|
688
|
+
else if (entry.isFile()) {
|
|
689
|
+
if (!isBinaryFile(full))
|
|
690
|
+
results.push(full);
|
|
691
|
+
}
|
|
692
|
+
if (results.length > MAX_FILES)
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
return results;
|
|
696
|
+
}
|
|
697
|
+
function simpleGlobMatch(pattern, filePath) {
|
|
698
|
+
// Expand {a,b,c} braces into alternatives
|
|
699
|
+
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));
|
|
703
|
+
}
|
|
704
|
+
// Convert glob to regex
|
|
705
|
+
let regex = "^";
|
|
706
|
+
let i = 0;
|
|
707
|
+
while (i < pattern.length) {
|
|
708
|
+
const ch = pattern[i];
|
|
709
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
710
|
+
// ** matches any path segment(s)
|
|
711
|
+
regex += ".*";
|
|
712
|
+
i += 2;
|
|
713
|
+
if (pattern[i] === "/")
|
|
714
|
+
i++; // skip trailing slash after **
|
|
715
|
+
}
|
|
716
|
+
else if (ch === "*") {
|
|
717
|
+
regex += "[^/]*";
|
|
718
|
+
i++;
|
|
719
|
+
}
|
|
720
|
+
else if (ch === "?") {
|
|
721
|
+
regex += "[^/]";
|
|
722
|
+
i++;
|
|
723
|
+
}
|
|
724
|
+
else if (".+^$|()[]\\".includes(ch)) {
|
|
725
|
+
regex += "\\" + ch;
|
|
726
|
+
i++;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
regex += ch;
|
|
730
|
+
i++;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
regex += "$";
|
|
734
|
+
return new RegExp(regex).test(filePath);
|
|
735
|
+
}
|
|
736
|
+
function resolveFileArgs(args) {
|
|
737
|
+
const files = [];
|
|
738
|
+
for (const arg of args) {
|
|
739
|
+
const resolved = path.resolve(arg);
|
|
740
|
+
// Glob pattern (contains * or ?)
|
|
741
|
+
if (arg.includes("*") || arg.includes("?")) {
|
|
742
|
+
// 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();
|
|
745
|
+
const allFiles = walkDir(baseDir);
|
|
746
|
+
for (const f of allFiles) {
|
|
747
|
+
const rel = path.relative(process.cwd(), f);
|
|
748
|
+
if (simpleGlobMatch(arg, rel) || simpleGlobMatch(arg, f)) {
|
|
749
|
+
files.push(f);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
// Directory
|
|
755
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
756
|
+
files.push(...walkDir(resolved));
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// Regular file
|
|
760
|
+
if (fs.existsSync(resolved)) {
|
|
761
|
+
if (!isBinaryFile(resolved))
|
|
762
|
+
files.push(resolved);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
console.log(` ${c.yellow}⚠${c.reset} Not found: ${arg}`);
|
|
766
|
+
}
|
|
767
|
+
return [...new Set(files)]; // deduplicate
|
|
768
|
+
}
|
|
769
|
+
function loadMultipleFiles(filePaths) {
|
|
770
|
+
if (filePaths.length > MAX_FILES) {
|
|
771
|
+
console.log(` ${c.yellow}⚠${c.reset} Too many files (${filePaths.length}). Limit is ${MAX_FILES}.`);
|
|
772
|
+
filePaths = filePaths.slice(0, MAX_FILES);
|
|
773
|
+
}
|
|
774
|
+
const parts = [];
|
|
775
|
+
let totalBytes = 0;
|
|
776
|
+
for (const fp of filePaths) {
|
|
777
|
+
try {
|
|
778
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
779
|
+
if (totalBytes + content.length > MAX_TOTAL_BYTES) {
|
|
780
|
+
console.log(` ${c.yellow}⚠${c.reset} Size limit reached (${(MAX_TOTAL_BYTES / 1024 / 1024).toFixed(0)}MB). Loaded ${parts.length} of ${filePaths.length} files.`);
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
const rel = path.relative(process.cwd(), fp);
|
|
784
|
+
parts.push(`=== ${rel} ===\n${content}`);
|
|
785
|
+
totalBytes += content.length;
|
|
786
|
+
}
|
|
787
|
+
catch { /* skip unreadable */ }
|
|
788
|
+
}
|
|
789
|
+
return { text: parts.join("\n\n"), count: parts.length, totalBytes };
|
|
790
|
+
}
|
|
583
791
|
// ── Run RLM query ───────────────────────────────────────────────────────────
|
|
584
792
|
async function runQuery(query) {
|
|
585
793
|
const effectiveContext = contextText || query;
|
|
586
794
|
const isDirectMode = !contextText;
|
|
587
795
|
if (!currentModel) {
|
|
588
|
-
|
|
796
|
+
const resolved = resolveModelWithProvider(currentModelId);
|
|
797
|
+
if (resolved) {
|
|
798
|
+
currentModel = resolved.model;
|
|
799
|
+
currentProviderName = resolved.provider;
|
|
800
|
+
}
|
|
589
801
|
}
|
|
590
802
|
if (!currentModel) {
|
|
591
803
|
console.log(`\n ${c.red}Model "${currentModelId}" not found.${c.reset}`);
|
|
@@ -778,28 +990,45 @@ function extractFilePath(input) {
|
|
|
778
990
|
return { filePath: null, query: input };
|
|
779
991
|
}
|
|
780
992
|
function expandAtFiles(input) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
}
|
|
993
|
+
// Extract all @tokens from input
|
|
994
|
+
const tokens = [];
|
|
995
|
+
const remaining = [];
|
|
996
|
+
for (const part of input.split(/\s+/)) {
|
|
997
|
+
if (part.startsWith("@") && part.length > 1) {
|
|
998
|
+
tokens.push(part.slice(1));
|
|
796
999
|
}
|
|
797
1000
|
else {
|
|
798
|
-
|
|
1001
|
+
remaining.push(part);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
if (tokens.length === 0)
|
|
1005
|
+
return input;
|
|
1006
|
+
const filePaths = resolveFileArgs(tokens);
|
|
1007
|
+
if (filePaths.length === 0) {
|
|
1008
|
+
console.log(` ${c.red}No files found for: ${tokens.join(", ")}${c.reset}`);
|
|
1009
|
+
return "";
|
|
1010
|
+
}
|
|
1011
|
+
if (filePaths.length === 1) {
|
|
1012
|
+
// Single file — simple load
|
|
1013
|
+
try {
|
|
1014
|
+
contextText = fs.readFileSync(filePaths[0], "utf-8");
|
|
1015
|
+
contextSource = path.relative(process.cwd(), filePaths[0]) || filePaths[0];
|
|
1016
|
+
const lines = contextText.split("\n").length;
|
|
1017
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${contextSource}${c.reset}`);
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
799
1021
|
return "";
|
|
800
1022
|
}
|
|
801
1023
|
}
|
|
802
|
-
|
|
1024
|
+
else {
|
|
1025
|
+
// Multiple files — concatenate with separators
|
|
1026
|
+
const { text, count, totalBytes } = loadMultipleFiles(filePaths);
|
|
1027
|
+
contextText = text;
|
|
1028
|
+
contextSource = `${count} files`;
|
|
1029
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${count}${c.reset} files (${(totalBytes / 1024).toFixed(1)}KB total)`);
|
|
1030
|
+
}
|
|
1031
|
+
return remaining.join(" ");
|
|
803
1032
|
}
|
|
804
1033
|
// ── Auto-detect URLs ────────────────────────────────────────────────────────
|
|
805
1034
|
async function detectAndLoadUrl(input) {
|
|
@@ -874,7 +1103,11 @@ async function interactive() {
|
|
|
874
1103
|
setupRl.close();
|
|
875
1104
|
}
|
|
876
1105
|
// Resolve model
|
|
877
|
-
|
|
1106
|
+
const initialResolved = resolveModelWithProvider(currentModelId);
|
|
1107
|
+
if (initialResolved) {
|
|
1108
|
+
currentModel = initialResolved.model;
|
|
1109
|
+
currentProviderName = initialResolved.provider;
|
|
1110
|
+
}
|
|
878
1111
|
if (!currentModel) {
|
|
879
1112
|
console.log(`\n ${c.red}Model "${currentModelId}" not found.${c.reset}`);
|
|
880
1113
|
console.log(` Check ${c.bold}RLM_MODEL${c.reset} in your .env file.\n`);
|
|
@@ -1004,6 +1237,7 @@ async function interactive() {
|
|
|
1004
1237
|
}
|
|
1005
1238
|
currentModelId = pick;
|
|
1006
1239
|
currentModel = resolved.model;
|
|
1240
|
+
currentProviderName = resolved.provider;
|
|
1007
1241
|
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
1008
1242
|
console.log();
|
|
1009
1243
|
printStatusLine();
|
|
@@ -1064,7 +1298,9 @@ async function interactive() {
|
|
|
1064
1298
|
const defaultModel = getDefaultModelForProvider(chosen.piProvider);
|
|
1065
1299
|
if (defaultModel) {
|
|
1066
1300
|
currentModelId = defaultModel;
|
|
1067
|
-
|
|
1301
|
+
const provResolved = resolveModelWithProvider(currentModelId);
|
|
1302
|
+
currentModel = provResolved?.model;
|
|
1303
|
+
currentProviderName = chosen.piProvider;
|
|
1068
1304
|
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
|
|
1069
1305
|
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
1070
1306
|
console.log();
|