rlm-cli 0.2.15 → 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 +276 -52
- 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
|
@@ -296,48 +296,80 @@ function printWelcome() {
|
|
|
296
296
|
// ── Help ────────────────────────────────────────────────────────────────────
|
|
297
297
|
function printCommandHelp() {
|
|
298
298
|
console.log(`
|
|
299
|
-
${c.bold}Context${c.reset}
|
|
300
|
-
${c.cyan}/file${c.reset} <path>
|
|
301
|
-
${c.cyan}/
|
|
302
|
-
${c.cyan}/
|
|
303
|
-
${c.cyan}/
|
|
304
|
-
${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
|
|
305
308
|
|
|
306
|
-
${c.bold}
|
|
307
|
-
${c.cyan}
|
|
308
|
-
${c.cyan}
|
|
309
|
-
${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, ...)
|
|
310
319
|
|
|
311
320
|
${c.bold}Tools${c.reset}
|
|
312
|
-
${c.cyan}/trajectories${c.reset}
|
|
321
|
+
${c.cyan}/trajectories${c.reset} List saved runs
|
|
313
322
|
|
|
314
323
|
${c.bold}General${c.reset}
|
|
315
|
-
${c.cyan}/clear${c.reset}
|
|
316
|
-
${c.cyan}/help${c.reset}
|
|
317
|
-
${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
|
|
318
327
|
|
|
319
|
-
|
|
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
|
|
320
335
|
`);
|
|
321
336
|
}
|
|
322
337
|
// ── Slash command handlers ──────────────────────────────────────────────────
|
|
323
338
|
async function handleFile(arg) {
|
|
324
339
|
if (!arg) {
|
|
325
|
-
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}`);
|
|
326
342
|
return;
|
|
327
343
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
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}`);
|
|
331
348
|
return;
|
|
332
349
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|
|
338
360
|
}
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
}
|
|
341
373
|
}
|
|
342
374
|
}
|
|
343
375
|
async function handleUrl(arg) {
|
|
@@ -379,20 +411,35 @@ function handlePaste(rl) {
|
|
|
379
411
|
}
|
|
380
412
|
function handleContext() {
|
|
381
413
|
if (!contextText) {
|
|
382
|
-
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}`);
|
|
383
415
|
return;
|
|
384
416
|
}
|
|
385
417
|
const lines = contextText.split("\n").length;
|
|
386
|
-
|
|
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`);
|
|
387
420
|
console.log(` ${c.bold}Source:${c.reset} ${contextSource}`);
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
}
|
|
393
432
|
}
|
|
394
|
-
|
|
395
|
-
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
|
+
}
|
|
396
443
|
}
|
|
397
444
|
}
|
|
398
445
|
function handleTrajectories() {
|
|
@@ -581,6 +628,166 @@ function getModelsForProvider(providerName) {
|
|
|
581
628
|
function truncateStr(text, max) {
|
|
582
629
|
return text.length <= max ? text : text.slice(0, max - 3) + "...";
|
|
583
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
|
+
}
|
|
584
791
|
// ── Run RLM query ───────────────────────────────────────────────────────────
|
|
585
792
|
async function runQuery(query) {
|
|
586
793
|
const effectiveContext = contextText || query;
|
|
@@ -783,28 +990,45 @@ function extractFilePath(input) {
|
|
|
783
990
|
return { filePath: null, query: input };
|
|
784
991
|
}
|
|
785
992
|
function expandAtFiles(input) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
contextSource = atMatch[1];
|
|
793
|
-
const lines = contextText.split("\n").length;
|
|
794
|
-
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${atMatch[1]}${c.reset}`);
|
|
795
|
-
return atMatch[2] || "";
|
|
796
|
-
}
|
|
797
|
-
catch (err) {
|
|
798
|
-
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
799
|
-
return "";
|
|
800
|
-
}
|
|
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));
|
|
801
999
|
}
|
|
802
1000
|
else {
|
|
803
|
-
|
|
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}`);
|
|
804
1021
|
return "";
|
|
805
1022
|
}
|
|
806
1023
|
}
|
|
807
|
-
|
|
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(" ");
|
|
808
1032
|
}
|
|
809
1033
|
// ── Auto-detect URLs ────────────────────────────────────────────────────────
|
|
810
1034
|
async function detectAndLoadUrl(input) {
|