← Files The LegalQuants CompanionARCHIVED FILE

skills/lq-reflect/scripts/session_reader_cli.py

5.85 KB · Oct 2, 2026 · 00:33 UTC

↓ Download file

"""CLI and metadata discovery for the confirmed-selection session reader."""

import argparse
import datetime as dt
import json
import re
import sys
from pathlib import Path


def project_matches(reader, raw, project):
    # Local metadata check only. Never return raw first-line text to the model.
    for line in raw.split(b"\n", 4)[:4]:
        if len(line) > reader.MAX_LINE:
            continue
        try:
            event = json.loads(line)
            meta = (
                event.get("payload", {})
                if event.get("type") == "session_meta"
                else event
            )
            cwd = meta.get("cwd")
            if isinstance(cwd, str) and Path(cwd).expanduser().resolve().is_relative_to(
                project
            ):
                return True
        except (ValueError, AttributeError, TypeError):
            continue
    return False


def run(reader):
    parser = argparse.ArgumentParser(description=reader.__doc__)
    modes = parser.add_mutually_exclusive_group(required=True)
    modes.add_argument("--list", action="store_true")
    modes.add_argument("--read", action="store_true")
    modes.add_argument(
        "--session",
        help="Legacy exact-name read; still requires a confirmed selection manifest.",
    )
    parser.add_argument("--store", action="append", default=[], type=Path)
    parser.add_argument("--file", action="append", default=[], type=Path)
    parser.add_argument("--project", type=Path)
    parser.add_argument("--exclude", action="append", default=[])
    parser.add_argument("--manifest", type=Path, required=True)
    parser.add_argument("--confirmed", action="store_true")
    parser.add_argument("--excerpt-file", action="store_true")
    parser.add_argument(
        "--lines",
        nargs=2,
        type=int,
        metavar=("START", "END"),
        help="retrieve complete messages from an inclusive range in one transcript",
    )
    parser.add_argument("--window", default="7d")
    parser.add_argument("--since")
    args = parser.parse_args()
    try:
        if args.lines is not None and (args.list or args.excerpt_file):
            raise ValueError(
                "--lines requires a confirmed transcript read, not a list or excerpt."
            )
        if args.list:
            if not args.file and not args.store:
                raise ValueError(
                    "Choose explicit files or a project/session folder. No automatic "
                    "home-wide scan."
                )
            if args.store and not args.project:
                raise ValueError(
                    "Folder discovery requires --project. Alternatively select exact "
                    "--file paths."
                )
            if args.manifest.exists():
                raise ValueError(
                    "Selection manifest already exists; use a fresh filename."
                )
            paths = list(args.file)
            if args.store:
                if not re.fullmatch(r"[1-9][0-9]{0,3}d", args.window):
                    raise ValueError(
                        "window must be a positive number of days, for example 7d."
                    )
                cutoff = (
                    dt.datetime.fromisoformat(args.since)
                    .replace(tzinfo=dt.UTC)
                    .timestamp()
                    if args.since
                    else dt.datetime.now(dt.UTC).timestamp()
                    - int(args.window[:-1]) * 86400
                )
                project = args.project.expanduser().resolve()
                for folder in args.store:
                    for path in sorted(folder.expanduser().rglob("*.jsonl")):
                        if reader.excluded(path, args.exclude):
                            continue
                        # mtime, not filename date: resumed sessions stay visible.
                        if path.stat().st_mtime < cutoff:
                            continue
                        record, raw = reader.snapshot(path)
                        if project_matches(reader, raw, project):
                            paths.append(path)
                        if len(paths) > reader.MAX_FILES:
                            raise ValueError(
                                "Too many matches. Narrow the project or time window."
                            )
            result = reader.selection(paths, args.exclude)
            args.manifest.parent.mkdir(parents=True, exist_ok=True)
            reader.atomic_json(args.manifest, result)
        else:
            if not args.confirmed:
                raise ValueError(
                    "Show the scope and obtain consent before --read --confirmed."
                )
            if args.store or args.file or args.project:
                raise ValueError(
                    "Read only the approved manifest; file discovery cannot be mixed "
                    "with reading."
                )
            manifest = json.loads(args.manifest.read_text(encoding="utf-8"))
            if args.session:
                matches = [
                    r for r in manifest["files"] if Path(r["path"]).name == args.session
                ]
                if len(matches) != 1:
                    raise ValueError(
                        "Exact session name is absent or ambiguous in the approved "
                        "manifest."
                    )
                manifest["files"] = matches
            result = reader.summarise(
                reader.read_selection(manifest, args.exclude),
                args.excerpt_file,
                args.lines,
            )
        sys.stdout.buffer.write(
            (json.dumps(result, ensure_ascii=False) + "\n").encode("utf-8")
        )
    except (OSError, ValueError, TypeError, KeyError) as error:
        print(
            json.dumps({"error": str(error), "transcript_returned": False}),
            file=sys.stderr,
        )
        return 2
    return 0

SHA-256: 0953cac8a6c2f6de18900960e000af0b43d6c1ac99e81dedbfb078eb47bc07eb