rlmgrep 0.1.18__tar.gz → 0.1.26__tar.gz
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.
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/PKG-INFO +3 -2
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/README.md +1 -1
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/pyproject.toml +2 -1
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/__init__.py +1 -1
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/cli.py +199 -10
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/ingest.py +30 -6
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/PKG-INFO +3 -2
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/requires.txt +1 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/__main__.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/config.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/file_map.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/interpreter.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/render.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep/rlm.py +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/SOURCES.txt +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/dependency_links.txt +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/entry_points.txt +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/rlmgrep.egg-info/top_level.txt +0 -0
- {rlmgrep-0.1.18 → rlmgrep-0.1.26}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rlmgrep
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: Grep-shaped CLI search powered by DSPy RLM
|
|
5
5
|
Author: rlmgrep
|
|
6
6
|
License: MIT
|
|
@@ -10,6 +10,7 @@ Requires-Dist: dspy>=3.1.1
|
|
|
10
10
|
Requires-Dist: markitdown[all]>=0.1.4
|
|
11
11
|
Requires-Dist: pathspec>=0.12.1
|
|
12
12
|
Requires-Dist: pypdf>=4.0.0
|
|
13
|
+
Requires-Dist: rich>=13.7.0
|
|
13
14
|
|
|
14
15
|
# rlmgrep
|
|
15
16
|
|
|
@@ -18,7 +19,7 @@ Grep-shaped search powered by DSPy RLM. It accepts a natural-language query, sca
|
|
|
18
19
|
## Quickstart
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
|
-
uv tool install rlmgrep
|
|
22
|
+
uv tool install --python 3.11 rlmgrep
|
|
22
23
|
# or from GitHub:
|
|
23
24
|
# uv tool install git+https://github.com/halfprice06/rlmgrep.git
|
|
24
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rlmgrep"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.26"
|
|
4
4
|
description = "Grep-shaped CLI search powered by DSPy RLM"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
|
11
11
|
"markitdown[all]>=0.1.4",
|
|
12
12
|
"pathspec>=0.12.1",
|
|
13
13
|
"pypdf>=4.0.0",
|
|
14
|
+
"rich>=13.7.0",
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
[project.scripts]
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.26"
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import logging
|
|
4
5
|
import os
|
|
5
|
-
import sys
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
|
+
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import dspy
|
|
12
|
+
from rich import box
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.progress import (
|
|
16
|
+
BarColumn,
|
|
17
|
+
Progress,
|
|
18
|
+
SpinnerColumn,
|
|
19
|
+
TaskProgressColumn,
|
|
20
|
+
TextColumn,
|
|
21
|
+
TimeElapsedColumn,
|
|
22
|
+
)
|
|
23
|
+
from rich.text import Text
|
|
11
24
|
from . import __version__
|
|
12
25
|
from .config import ensure_default_config, load_config
|
|
13
26
|
from .file_map import build_file_map
|
|
@@ -23,7 +36,81 @@ from .render import render_matches
|
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
def _warn(msg: str) -> None:
|
|
26
|
-
print(f"rlmgrep: {msg}", file=sys.stderr)
|
|
39
|
+
print(f"rlmgrep: {msg}", file=sys.stderr, flush=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _console() -> Console:
|
|
43
|
+
use_color = sys.stderr.isatty() and not os.getenv("NO_COLOR")
|
|
44
|
+
return Console(stderr=True, force_terminal=use_color, color_system="auto")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _RLMIterationHandler(logging.Handler):
|
|
48
|
+
def __init__(self, console: Console) -> None:
|
|
49
|
+
super().__init__(level=logging.INFO)
|
|
50
|
+
self._console = console
|
|
51
|
+
self._title: str | None = None
|
|
52
|
+
self._lines: list[str] = []
|
|
53
|
+
|
|
54
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
55
|
+
msg = record.getMessage()
|
|
56
|
+
if "RLM iteration" in msg:
|
|
57
|
+
self.flush_panel()
|
|
58
|
+
self._title = "RLM iteration"
|
|
59
|
+
self._lines = [msg]
|
|
60
|
+
return
|
|
61
|
+
if self._title is None:
|
|
62
|
+
self._title = "RLM output"
|
|
63
|
+
self._lines.append(msg)
|
|
64
|
+
|
|
65
|
+
def flush_panel(self) -> None:
|
|
66
|
+
if self._title is None:
|
|
67
|
+
return
|
|
68
|
+
body = "\n".join(self._lines).strip() or " "
|
|
69
|
+
self._console.print(
|
|
70
|
+
Panel(
|
|
71
|
+
Text(body),
|
|
72
|
+
title=self._title,
|
|
73
|
+
border_style="blue",
|
|
74
|
+
box=box.ROUNDED,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
self._title = None
|
|
78
|
+
self._lines = []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _setup_verbose_logging(console: Console) -> _RLMIterationHandler:
|
|
82
|
+
logger = logging.getLogger("dspy.predict.rlm")
|
|
83
|
+
handler = _RLMIterationHandler(console)
|
|
84
|
+
logger.addHandler(handler)
|
|
85
|
+
logger.setLevel(logging.INFO)
|
|
86
|
+
logger.propagate = False
|
|
87
|
+
return handler
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _print_answer(console: Console, answer: str) -> None:
|
|
91
|
+
text = Text(answer.strip())
|
|
92
|
+
panel = Panel(
|
|
93
|
+
text,
|
|
94
|
+
title="Answer",
|
|
95
|
+
border_style="cyan",
|
|
96
|
+
box=box.ROUNDED,
|
|
97
|
+
)
|
|
98
|
+
console.print(panel)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _print_matches(console: Console, lines: list[str], use_color: bool) -> None:
|
|
102
|
+
body = "\n".join(lines).strip()
|
|
103
|
+
if not body:
|
|
104
|
+
body = "No matches"
|
|
105
|
+
text = Text.from_ansi(body) if use_color else Text(body)
|
|
106
|
+
console.print(
|
|
107
|
+
Panel(
|
|
108
|
+
text,
|
|
109
|
+
title="Matches",
|
|
110
|
+
border_style="cyan",
|
|
111
|
+
box=box.ROUNDED,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
27
114
|
|
|
28
115
|
|
|
29
116
|
def _confirm_over_limit(count: int, threshold: int) -> bool:
|
|
@@ -450,6 +537,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
450
537
|
for w in config_warnings:
|
|
451
538
|
_warn(w)
|
|
452
539
|
|
|
540
|
+
console = _console()
|
|
541
|
+
|
|
453
542
|
# Resolve input corpus.
|
|
454
543
|
globs = _split_list(args.globs)
|
|
455
544
|
type_names = _split_list(args.types)
|
|
@@ -511,7 +600,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
511
600
|
extra_ignores.extend(_global_ignore_paths(ignore_root))
|
|
512
601
|
ignore_spec = build_ignore_spec(ignore_root, extra_paths=extra_ignores)
|
|
513
602
|
|
|
514
|
-
|
|
603
|
+
scan_task = None
|
|
604
|
+
scan_count = 0
|
|
605
|
+
scan_progress = None
|
|
606
|
+
if sys.stderr.isatty():
|
|
607
|
+
scan_progress = Progress(
|
|
608
|
+
SpinnerColumn(),
|
|
609
|
+
TextColumn("{task.description}"),
|
|
610
|
+
TextColumn("{task.completed} files"),
|
|
611
|
+
BarColumn(),
|
|
612
|
+
TaskProgressColumn(),
|
|
613
|
+
TimeElapsedColumn(),
|
|
614
|
+
console=console,
|
|
615
|
+
transient=False,
|
|
616
|
+
)
|
|
617
|
+
scan_progress.start()
|
|
618
|
+
scan_task = scan_progress.add_task("Scanning files", total=None)
|
|
619
|
+
|
|
620
|
+
def _scan_update(count: int) -> None:
|
|
621
|
+
nonlocal scan_count
|
|
622
|
+
scan_count = count
|
|
623
|
+
if scan_progress is not None and scan_task is not None:
|
|
624
|
+
scan_progress.update(
|
|
625
|
+
scan_task,
|
|
626
|
+
completed=count,
|
|
627
|
+
description=f"Scanning files ({count})",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
candidates, scanned = collect_candidates(
|
|
515
631
|
input_paths,
|
|
516
632
|
cwd=cwd,
|
|
517
633
|
recursive=args.recursive,
|
|
@@ -520,7 +636,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
520
636
|
include_hidden=args.hidden,
|
|
521
637
|
ignore_spec=ignore_spec,
|
|
522
638
|
ignore_root=ignore_root,
|
|
639
|
+
scan_progress=_scan_update if scan_progress is not None else None,
|
|
523
640
|
)
|
|
641
|
+
if scan_progress is not None and scan_task is not None:
|
|
642
|
+
scan_progress.update(
|
|
643
|
+
scan_task,
|
|
644
|
+
total=scanned or scan_count,
|
|
645
|
+
completed=scanned or scan_count,
|
|
646
|
+
description=f"Scanning files ({scanned or scan_count})",
|
|
647
|
+
)
|
|
648
|
+
scan_progress.stop()
|
|
524
649
|
candidate_count = len(candidates)
|
|
525
650
|
if hard_max is not None and candidate_count > hard_max:
|
|
526
651
|
_warn(
|
|
@@ -535,6 +660,39 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
535
660
|
if not _confirm_over_limit(candidate_count, warn_threshold):
|
|
536
661
|
return 2
|
|
537
662
|
|
|
663
|
+
load_task = None
|
|
664
|
+
load_progress = None
|
|
665
|
+
if sys.stderr.isatty():
|
|
666
|
+
load_progress = Progress(
|
|
667
|
+
SpinnerColumn(),
|
|
668
|
+
TextColumn("{task.description}"),
|
|
669
|
+
TextColumn("{task.completed}/{task.total} files"),
|
|
670
|
+
BarColumn(),
|
|
671
|
+
TaskProgressColumn(),
|
|
672
|
+
TimeElapsedColumn(),
|
|
673
|
+
console=console,
|
|
674
|
+
transient=False,
|
|
675
|
+
)
|
|
676
|
+
load_progress.start()
|
|
677
|
+
load_task = load_progress.add_task(
|
|
678
|
+
"Loading files",
|
|
679
|
+
total=candidate_count,
|
|
680
|
+
completed=0,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
load_done = 0
|
|
684
|
+
|
|
685
|
+
def _load_update(done: int, total: int) -> None:
|
|
686
|
+
nonlocal load_done
|
|
687
|
+
load_done = done
|
|
688
|
+
if load_progress is not None and load_task is not None:
|
|
689
|
+
load_progress.update(
|
|
690
|
+
load_task,
|
|
691
|
+
completed=done,
|
|
692
|
+
total=total,
|
|
693
|
+
description=f"Loading files ({done}/{total})",
|
|
694
|
+
)
|
|
695
|
+
|
|
538
696
|
files, warnings = load_files(
|
|
539
697
|
candidates,
|
|
540
698
|
cwd=cwd,
|
|
@@ -543,7 +701,24 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
543
701
|
enable_audio=md_enable_audio,
|
|
544
702
|
audio_transcriber=audio_transcriber,
|
|
545
703
|
binary_as_text=args.binary_as_text,
|
|
704
|
+
progress=_load_update if load_progress is not None else None,
|
|
546
705
|
)
|
|
706
|
+
if load_progress is not None and load_task is not None:
|
|
707
|
+
load_progress.update(
|
|
708
|
+
load_task,
|
|
709
|
+
completed=load_done,
|
|
710
|
+
total=candidate_count,
|
|
711
|
+
description=f"Loading files ({load_done}/{candidate_count})",
|
|
712
|
+
)
|
|
713
|
+
load_progress.stop()
|
|
714
|
+
|
|
715
|
+
scanned_total = scanned or scan_count
|
|
716
|
+
loaded_total = len(files)
|
|
717
|
+
skipped_total = max(0, candidate_count - loaded_total)
|
|
718
|
+
summary = f"Scanned {scanned_total} files. Loaded {loaded_total}."
|
|
719
|
+
if skipped_total:
|
|
720
|
+
summary += f" Skipped {skipped_total}."
|
|
721
|
+
_warn(summary)
|
|
547
722
|
|
|
548
723
|
for w in warnings:
|
|
549
724
|
_warn(w)
|
|
@@ -620,6 +795,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
620
795
|
|
|
621
796
|
directory = {k: v.text for k, v in files.items()}
|
|
622
797
|
|
|
798
|
+
verbose_handler = None
|
|
799
|
+
if args.verbose:
|
|
800
|
+
verbose_handler = _setup_verbose_logging(console)
|
|
801
|
+
|
|
623
802
|
try:
|
|
624
803
|
proposed, answer = run_rlm(
|
|
625
804
|
directory=directory,
|
|
@@ -634,6 +813,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
634
813
|
except Exception as exc: # pragma: no cover - defensive
|
|
635
814
|
_warn(f"RLM failure: {exc}")
|
|
636
815
|
return 2
|
|
816
|
+
finally:
|
|
817
|
+
if verbose_handler is not None:
|
|
818
|
+
verbose_handler.flush_panel()
|
|
637
819
|
|
|
638
820
|
verified, dropped = verify_matches(proposed, files)
|
|
639
821
|
if dropped:
|
|
@@ -649,7 +831,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
649
831
|
before = args.before if args.before is not None else args.context
|
|
650
832
|
after = args.after if args.after is not None else args.context
|
|
651
833
|
|
|
652
|
-
|
|
834
|
+
stdout_tty = sys.stdout.isatty()
|
|
835
|
+
stderr_tty = sys.stderr.isatty()
|
|
836
|
+
use_color = stdout_tty and not os.getenv("NO_COLOR")
|
|
653
837
|
|
|
654
838
|
output_lines = render_matches(
|
|
655
839
|
files=files,
|
|
@@ -660,13 +844,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
660
844
|
heading=True,
|
|
661
845
|
)
|
|
662
846
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
847
|
+
stdout_console = Console(force_terminal=stdout_tty, color_system="auto")
|
|
848
|
+
if args.answer and answer:
|
|
849
|
+
_print_answer(console, answer)
|
|
850
|
+
|
|
851
|
+
if stdout_tty:
|
|
852
|
+
_print_matches(stdout_console, output_lines, use_color=use_color)
|
|
853
|
+
elif stderr_tty:
|
|
854
|
+
_print_matches(console, output_lines, use_color=False)
|
|
667
855
|
|
|
668
|
-
|
|
669
|
-
|
|
856
|
+
if not stdout_tty:
|
|
857
|
+
for line in output_lines:
|
|
858
|
+
print(line)
|
|
670
859
|
|
|
671
860
|
total_matches = sum(len(lines) for lines in verified.values())
|
|
672
861
|
if total_matches > 0:
|
|
@@ -147,20 +147,33 @@ def _load_file(
|
|
|
147
147
|
return None, None, str(exc)
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
def collect_files(
|
|
150
|
+
def collect_files(
|
|
151
|
+
paths: Iterable[str],
|
|
152
|
+
recursive: bool = True,
|
|
153
|
+
progress: Callable[[int], None] | None = None,
|
|
154
|
+
) -> list[Path]:
|
|
151
155
|
files: list[Path] = []
|
|
156
|
+
scanned = 0
|
|
152
157
|
for raw in paths:
|
|
153
158
|
p = Path(raw)
|
|
154
159
|
if not p.exists():
|
|
155
160
|
continue
|
|
156
161
|
if p.is_dir():
|
|
157
162
|
if recursive:
|
|
158
|
-
|
|
163
|
+
for fp in p.rglob("*"):
|
|
164
|
+
if fp.is_file():
|
|
165
|
+
files.append(fp)
|
|
166
|
+
scanned += 1
|
|
167
|
+
if progress is not None:
|
|
168
|
+
progress(scanned)
|
|
159
169
|
else:
|
|
160
170
|
# No recursion: ignore directories.
|
|
161
171
|
continue
|
|
162
172
|
elif p.is_file():
|
|
163
173
|
files.append(p)
|
|
174
|
+
scanned += 1
|
|
175
|
+
if progress is not None:
|
|
176
|
+
progress(scanned)
|
|
164
177
|
return files
|
|
165
178
|
|
|
166
179
|
|
|
@@ -344,8 +357,10 @@ def collect_candidates(
|
|
|
344
357
|
include_hidden: bool = False,
|
|
345
358
|
ignore_spec: "pathspec.PathSpec | None" = None,
|
|
346
359
|
ignore_root: Path | None = None,
|
|
347
|
-
|
|
348
|
-
|
|
360
|
+
scan_progress: Callable[[int], None] | None = None,
|
|
361
|
+
) -> tuple[list[Path], int]:
|
|
362
|
+
files = collect_files(paths, recursive=recursive, progress=scan_progress)
|
|
363
|
+
scanned = len(files)
|
|
349
364
|
explicit_files: set[Path] = set()
|
|
350
365
|
ignore_root_resolved: Path | None = None
|
|
351
366
|
if ignore_root is not None:
|
|
@@ -384,7 +399,7 @@ def collect_candidates(
|
|
|
384
399
|
continue
|
|
385
400
|
|
|
386
401
|
candidates.append(fp)
|
|
387
|
-
return candidates
|
|
402
|
+
return candidates, scanned
|
|
388
403
|
|
|
389
404
|
|
|
390
405
|
def load_files(
|
|
@@ -395,13 +410,16 @@ def load_files(
|
|
|
395
410
|
enable_audio: bool = False,
|
|
396
411
|
audio_transcriber: Callable[[Path], str] | None = None,
|
|
397
412
|
binary_as_text: bool = False,
|
|
413
|
+
progress: Callable[[int, int], None] | None = None,
|
|
398
414
|
) -> tuple[dict[str, FileRecord], list[str]]:
|
|
399
415
|
records: dict[str, FileRecord] = {}
|
|
400
416
|
warnings: list[str] = []
|
|
401
417
|
image_convert_count = 0
|
|
402
418
|
audio_convert_count = 0
|
|
403
419
|
|
|
404
|
-
|
|
420
|
+
candidate_list = list(candidates)
|
|
421
|
+
total = len(candidate_list)
|
|
422
|
+
for idx, fp in enumerate(candidate_list, start=1):
|
|
405
423
|
try:
|
|
406
424
|
key = fp.relative_to(cwd).as_posix()
|
|
407
425
|
except ValueError:
|
|
@@ -432,15 +450,21 @@ def load_files(
|
|
|
432
450
|
}
|
|
433
451
|
if err not in silent_errors and "No converter attempted a conversion" not in err:
|
|
434
452
|
warnings.append(f"skip {fp}: {err}")
|
|
453
|
+
if progress is not None:
|
|
454
|
+
progress(idx, total)
|
|
435
455
|
continue
|
|
436
456
|
if text is None:
|
|
437
457
|
warnings.append(f"skip {fp}: unreadable")
|
|
458
|
+
if progress is not None:
|
|
459
|
+
progress(idx, total)
|
|
438
460
|
continue
|
|
439
461
|
|
|
440
462
|
lines = text.split("\n")
|
|
441
463
|
if page_map is not None and len(page_map) != len(lines):
|
|
442
464
|
page_map = None
|
|
443
465
|
records[key] = FileRecord(path=key, text=text, lines=lines, page_map=page_map)
|
|
466
|
+
if progress is not None:
|
|
467
|
+
progress(idx, total)
|
|
444
468
|
|
|
445
469
|
if image_convert_count > 5:
|
|
446
470
|
warnings.append(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rlmgrep
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: Grep-shaped CLI search powered by DSPy RLM
|
|
5
5
|
Author: rlmgrep
|
|
6
6
|
License: MIT
|
|
@@ -10,6 +10,7 @@ Requires-Dist: dspy>=3.1.1
|
|
|
10
10
|
Requires-Dist: markitdown[all]>=0.1.4
|
|
11
11
|
Requires-Dist: pathspec>=0.12.1
|
|
12
12
|
Requires-Dist: pypdf>=4.0.0
|
|
13
|
+
Requires-Dist: rich>=13.7.0
|
|
13
14
|
|
|
14
15
|
# rlmgrep
|
|
15
16
|
|
|
@@ -18,7 +19,7 @@ Grep-shaped search powered by DSPy RLM. It accepts a natural-language query, sca
|
|
|
18
19
|
## Quickstart
|
|
19
20
|
|
|
20
21
|
```sh
|
|
21
|
-
uv tool install rlmgrep
|
|
22
|
+
uv tool install --python 3.11 rlmgrep
|
|
22
23
|
# or from GitHub:
|
|
23
24
|
# uv tool install git+https://github.com/halfprice06/rlmgrep.git
|
|
24
25
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|