Coverage for little_loops / cli / session.py: 75%
56 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
1"""ll-session: query the unified session store (SQLite + FTS5).
3Wraps :mod:`little_loops.session_store` with a CLI surface so operators can
4search and inspect the per-project ``.ll/session.db`` without re-parsing the
5scattered JSON/markdown sources the analyze-* skills read.
7Subcommands:
8 search FTS5 full-text query with BM25-ranked results
9 recent most recent rows for an event kind (tool, file, issue, loop, correction)
10 backfill seed the database from existing on-disk sources
11"""
13from __future__ import annotations
15import argparse
16from pathlib import Path
18from little_loops.cli.output import configure_output, use_color_enabled
19from little_loops.logger import Logger
20from little_loops.session_store import DEFAULT_DB_PATH, backfill, recent, search
23def _build_parser() -> argparse.ArgumentParser:
24 """Build the argument parser for ll-session."""
25 parser = argparse.ArgumentParser(
26 prog="ll-session",
27 description="Query the unified session store (SQLite + FTS5)",
28 formatter_class=argparse.RawDescriptionHelpFormatter,
29 epilog="""
30Examples:
31 %(prog)s search --fts "rate limit" # Full-text search, BM25-ranked
32 %(prog)s recent --kind loop # Recent loop events
33 %(prog)s backfill # Seed the database from on-disk sources
34""",
35 )
36 parser.add_argument(
37 "--db",
38 type=Path,
39 default=DEFAULT_DB_PATH,
40 metavar="PATH",
41 help="Path to the session database (default: .ll/session.db)",
42 )
44 subparsers = parser.add_subparsers(dest="command", help="Available commands")
46 search_parser = subparsers.add_parser("search", help="FTS5 full-text search")
47 search_parser.add_argument("--fts", required=True, metavar="QUERY", help="FTS5 match query")
48 search_parser.add_argument(
49 "--limit", type=int, default=20, metavar="N", help="Maximum results (default: 20)"
50 )
52 recent_parser = subparsers.add_parser("recent", help="Recent events by kind")
53 recent_parser.add_argument(
54 "--kind",
55 required=True,
56 choices=["tool", "file", "issue", "loop", "correction"],
57 help="Event kind to list",
58 )
59 recent_parser.add_argument(
60 "--limit", type=int, default=20, metavar="N", help="Maximum rows (default: 20)"
61 )
63 subparsers.add_parser("backfill", help="Seed the database from existing on-disk sources")
65 return parser
68def _parse_args() -> argparse.Namespace:
69 """Parse command-line arguments. Exposed for testing."""
70 return _build_parser().parse_args()
73def main_session() -> int:
74 """Entry point for ll-session command.
76 Returns:
77 0 on success, 1 when no subcommand is given or on error.
78 """
79 configure_output()
80 logger = Logger(use_color=use_color_enabled())
82 parser = _build_parser()
83 args = parser.parse_args()
85 if not args.command:
86 parser.print_help()
87 return 1
89 if args.command == "search":
90 try:
91 results = search(args.db, query=args.fts, limit=args.limit)
92 except ValueError as exc:
93 logger.error(str(exc))
94 return 1
95 if not results:
96 print("No matches.")
97 return 0
98 for row in results:
99 anchor = f" ({row['anchor']})" if row.get("anchor") else ""
100 print(f"[{row['kind']}] {row['content']}{anchor}")
101 return 0
103 if args.command == "recent":
104 rows = recent(args.db, kind=args.kind, limit=args.limit)
105 if not rows:
106 print(f"No {args.kind} events.")
107 return 0
108 for row in rows:
109 fields = ", ".join(f"{k}={v}" for k, v in row.items() if k != "id" and v is not None)
110 print(fields)
111 return 0
113 if args.command == "backfill":
114 counts = backfill(args.db)
115 total = sum(counts.values())
116 logger.success(
117 f"Backfilled {total} rows "
118 f"(issues={counts['issues']}, loops={counts['loops']}, tools={counts['tools']})"
119 )
120 return 0
122 return 1