fast-resume 1.12.8__py3-none-any.whl

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.
fast_resume/cli.py ADDED
@@ -0,0 +1,327 @@
1
+ """CLI entry point for fast-resume."""
2
+
3
+ import os
4
+
5
+ import click
6
+ import humanize
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from .config import AGENTS, INDEX_DIR
11
+ from .index import TantivyIndex
12
+ from .logging_config import setup_logging
13
+ from .search import SessionSearch
14
+ from .tui import run_tui
15
+
16
+
17
+ @click.command()
18
+ @click.argument("query", required=False, default="")
19
+ @click.option(
20
+ "-a",
21
+ "--agent",
22
+ type=click.Choice(
23
+ [
24
+ "claude",
25
+ "codex",
26
+ "copilot-cli",
27
+ "copilot-vscode",
28
+ "crush",
29
+ "opencode",
30
+ "vibe",
31
+ ]
32
+ ),
33
+ help="Filter by agent",
34
+ )
35
+ @click.option("-d", "--directory", help="Filter by directory (substring match)")
36
+ @click.option("--no-tui", is_flag=True, help="Output list to stdout instead of TUI")
37
+ @click.option(
38
+ "--list", "list_only", is_flag=True, help="Just list sessions, don't resume"
39
+ )
40
+ @click.option("--rebuild", is_flag=True, help="Force rebuild the session index")
41
+ @click.option("--stats", is_flag=True, help="Show index statistics")
42
+ @click.option(
43
+ "--yolo",
44
+ is_flag=True,
45
+ help="Resume sessions with auto-approve/skip-permissions flags",
46
+ )
47
+ @click.option(
48
+ "--no-version-check",
49
+ is_flag=True,
50
+ help="Disable checking for new versions",
51
+ )
52
+ @click.version_option()
53
+ def main(
54
+ query: str,
55
+ agent: str | None,
56
+ directory: str | None,
57
+ no_tui: bool,
58
+ list_only: bool,
59
+ rebuild: bool,
60
+ stats: bool,
61
+ yolo: bool,
62
+ no_version_check: bool,
63
+ ) -> None:
64
+ """Fast fuzzy finder for coding agent session history.
65
+
66
+ Search across Claude Code, Codex CLI, Copilot CLI, Crush, OpenCode, and Vibe sessions.
67
+ Select a session to resume it with the appropriate agent.
68
+
69
+ Supports keyword search syntax:
70
+
71
+ agent:NAME Filter by agent (e.g., agent:claude)
72
+
73
+ agent:A,B Multiple values with OR (e.g., agent:claude,codex)
74
+
75
+ -agent:NAME Exclude agent (or agent:!NAME)
76
+
77
+ dir:PATH Filter by directory substring
78
+
79
+ date:VALUE Filter by date/time (today, <1h, >1d, etc.)
80
+
81
+ Examples:
82
+
83
+ fr agent:claude,codex api # Claude OR Codex sessions
84
+
85
+ fr -agent:vibe # Exclude Vibe sessions
86
+
87
+ fr date:<1d -agent:claude # Last 24h, not Claude
88
+
89
+ fr dir:project date:today # Today's sessions in project
90
+ """
91
+ # Initialize logging for parse errors
92
+ setup_logging()
93
+
94
+ if stats:
95
+ # Sync before showing stats to ensure accurate data
96
+ search = SessionSearch()
97
+ search.get_all_sessions()
98
+ _show_stats()
99
+ return
100
+
101
+ if rebuild:
102
+ # Force rebuild index
103
+ search = SessionSearch()
104
+ search.get_all_sessions(force_refresh=True)
105
+ click.echo("Index rebuilt.")
106
+ if not (no_tui or list_only or query):
107
+ return
108
+
109
+ if no_tui or list_only:
110
+ _list_sessions(query, agent, directory)
111
+ else:
112
+ resume_cmd, resume_dir = run_tui(
113
+ query=query,
114
+ agent_filter=agent,
115
+ yolo=yolo,
116
+ no_version_check=no_version_check,
117
+ )
118
+ if resume_cmd:
119
+ # Change to session directory before running command
120
+ if resume_dir:
121
+ os.chdir(resume_dir)
122
+ # Execute the resume command
123
+ os.execvp(resume_cmd[0], resume_cmd)
124
+
125
+
126
+ def _show_stats() -> None:
127
+ """Display index statistics."""
128
+ console = Console()
129
+ index = TantivyIndex()
130
+ stats = index.get_stats()
131
+
132
+ if stats.total_sessions == 0:
133
+ console.print(
134
+ "[dim]No sessions indexed yet. Run [bold]fr[/bold] to index sessions.[/dim]"
135
+ )
136
+ return
137
+
138
+ # Header
139
+ console.print("\n[bold]Index Statistics[/bold]\n")
140
+
141
+ # Overview table
142
+ overview = Table(show_header=False, box=None, padding=(0, 2))
143
+ overview.add_column("Label", style="dim")
144
+ overview.add_column("Value")
145
+
146
+ overview.add_row("Total sessions", f"[bold]{stats.total_sessions}[/bold]")
147
+ overview.add_row("Total messages", f"{stats.total_messages:,}")
148
+ overview.add_row("Avg messages/session", f"{stats.avg_messages_per_session:.1f}")
149
+ overview.add_row("Index size", humanize.naturalsize(stats.index_size_bytes))
150
+ overview.add_row("Index location", str(INDEX_DIR))
151
+
152
+ if stats.oldest_session and stats.newest_session:
153
+ date_range = (
154
+ f"{stats.oldest_session:%Y-%m-%d} to {stats.newest_session:%Y-%m-%d}"
155
+ )
156
+ overview.add_row("Date range", date_range)
157
+
158
+ console.print(overview)
159
+
160
+ # Data by agent (raw + indexed)
161
+ console.print("\n[bold]Data by Agent[/bold]\n")
162
+ search = SessionSearch()
163
+ agent_table = Table(show_header=True, header_style="bold")
164
+ agent_table.add_column("Agent", no_wrap=True)
165
+ agent_table.add_column("Files", justify="right")
166
+ agent_table.add_column("Disk", justify="right")
167
+ agent_table.add_column("Sessions", justify="right")
168
+ agent_table.add_column("Messages", justify="right")
169
+ agent_table.add_column("Content", justify="right")
170
+ agent_table.add_column("Data Directory")
171
+
172
+ messages_by_agent = stats.messages_by_agent or {}
173
+ content_chars_by_agent = stats.content_chars_by_agent or {}
174
+
175
+ # Collect data for all agents and sort by indexed session count
176
+ agent_data = []
177
+ for adapter in search.adapters:
178
+ raw_stats = adapter.get_raw_stats()
179
+ sessions = stats.sessions_by_agent.get(adapter.name, 0)
180
+ agent_data.append((adapter.name, raw_stats, sessions))
181
+
182
+ # Sort by session count descending
183
+ agent_data.sort(key=lambda x: -x[2])
184
+
185
+ for agent_name, raw_stats, sessions in agent_data:
186
+ agent_config = AGENTS.get(agent_name, {"color": "white"})
187
+ color = agent_config["color"]
188
+ messages = messages_by_agent.get(agent_name, 0)
189
+ content_size = content_chars_by_agent.get(agent_name, 0)
190
+
191
+ if raw_stats.available:
192
+ # Shorten home directory in path
193
+ data_dir = raw_stats.data_dir
194
+ home = os.path.expanduser("~")
195
+ if data_dir.startswith(home):
196
+ data_dir = "~" + data_dir[len(home) :]
197
+
198
+ agent_table.add_row(
199
+ f"[{color}]{agent_name}[/{color}]",
200
+ str(raw_stats.file_count),
201
+ humanize.naturalsize(raw_stats.total_bytes),
202
+ str(sessions) if sessions > 0 else "[dim]0[/dim]",
203
+ f"{messages:,}" if messages > 0 else "[dim]0[/dim]",
204
+ humanize.naturalsize(content_size)
205
+ if content_size > 0
206
+ else "[dim]-[/dim]",
207
+ f"[dim]{data_dir}[/dim]",
208
+ )
209
+ else:
210
+ agent_table.add_row(
211
+ f"[{color}]{agent_name}[/{color}]",
212
+ "[dim]-[/dim]",
213
+ "[dim]-[/dim]",
214
+ "[dim]-[/dim]",
215
+ "[dim]-[/dim]",
216
+ "[dim]-[/dim]",
217
+ "[dim]not found[/dim]",
218
+ )
219
+
220
+ console.print(agent_table)
221
+
222
+ # Activity by day of week
223
+ if stats.sessions_by_weekday:
224
+ console.print("\n[bold]Activity by Day[/bold]\n")
225
+ day_table = Table(show_header=False, box=None, padding=(0, 1))
226
+ day_table.add_column("Day", style="dim", width=4)
227
+ day_table.add_column("Bar")
228
+ day_table.add_column("Count", justify="right", width=4)
229
+
230
+ max_day = (
231
+ max(stats.sessions_by_weekday.values()) if stats.sessions_by_weekday else 1
232
+ )
233
+ for day in ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]:
234
+ count = stats.sessions_by_weekday.get(day, 0)
235
+ bar_width = int((count / max_day) * 20) if max_day else 0
236
+ bar = "[green]" + "█" * bar_width + "[/green]"
237
+ day_table.add_row(day, bar, str(count))
238
+
239
+ console.print(day_table)
240
+
241
+ # Activity by hour
242
+ if stats.sessions_by_hour:
243
+ console.print("\n[bold]Activity by Hour[/bold]\n")
244
+ # Show as a compact sparkline-style display
245
+ max_hour = max(stats.sessions_by_hour.values()) if stats.sessions_by_hour else 1
246
+ blocks = " ▁▂▃▄▅▆▇█"
247
+
248
+ hour_line = ""
249
+ for h in range(24):
250
+ count = stats.sessions_by_hour.get(h, 0)
251
+ idx = int((count / max_hour) * 8) if max_hour else 0
252
+ hour_line += blocks[idx]
253
+
254
+ console.print(f" [dim]0h[/dim] [yellow]{hour_line}[/yellow] [dim]23h[/dim]")
255
+
256
+ # Find peak hours
257
+ sorted_hours = sorted(stats.sessions_by_hour.items(), key=lambda x: -x[1])
258
+ if sorted_hours:
259
+ top_hours = sorted_hours[:3]
260
+ peak_str = ", ".join(f"{h}:00 ({c})" for h, c in top_hours)
261
+ console.print(f" [dim]Peak hours: {peak_str}[/dim]")
262
+
263
+ # Top directories
264
+ if stats.top_directories:
265
+ console.print("\n[bold]Top Directories[/bold]\n")
266
+ dir_table = Table(show_header=True, header_style="bold")
267
+ dir_table.add_column("Directory")
268
+ dir_table.add_column("Sessions", justify="right")
269
+ dir_table.add_column("Messages", justify="right")
270
+
271
+ home = os.path.expanduser("~")
272
+ for directory, sessions, messages in stats.top_directories[:10]:
273
+ display_dir = directory
274
+ if display_dir.startswith(home):
275
+ display_dir = "~" + display_dir[len(home) :]
276
+ dir_table.add_row(display_dir, str(sessions), f"{messages:,}")
277
+
278
+ console.print(dir_table)
279
+
280
+ console.print()
281
+
282
+
283
+ def _list_sessions(query: str, agent: str | None, directory: str | None) -> None:
284
+ """List sessions in terminal without TUI."""
285
+ console = Console()
286
+ search = SessionSearch()
287
+
288
+ sessions = search.search(query, agent_filter=agent, directory_filter=directory)
289
+
290
+ if not sessions:
291
+ console.print("[dim]No sessions found.[/dim]")
292
+ return
293
+
294
+ table = Table(show_header=True, header_style="bold")
295
+ table.add_column("Agent", style="bold")
296
+ table.add_column("Title")
297
+ table.add_column("Directory", style="dim")
298
+ table.add_column("ID", style="dim")
299
+
300
+ for session in sessions[:50]: # Limit output
301
+ agent_config = AGENTS.get(session.agent, {"color": "white"})
302
+ agent_style = agent_config["color"]
303
+
304
+ # Truncate fields
305
+ title = session.title[:50] + "..." if len(session.title) > 50 else session.title
306
+ directory_display = session.directory
307
+ home = os.path.expanduser("~")
308
+ if directory_display.startswith(home):
309
+ directory_display = "~" + directory_display[len(home) :]
310
+ if len(directory_display) > 35:
311
+ directory_display = "..." + directory_display[-32:]
312
+
313
+ table.add_row(
314
+ f"[{agent_style}]{session.agent}[/{agent_style}]",
315
+ title,
316
+ directory_display,
317
+ session.id[:20] + "..." if len(session.id) > 20 else session.id,
318
+ )
319
+
320
+ console.print(table)
321
+ console.print(
322
+ f"\n[dim]Showing {min(len(sessions), 50)} of {len(sessions)} sessions[/dim]"
323
+ )
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()
fast_resume/config.py ADDED
@@ -0,0 +1,30 @@
1
+ """Configuration and constants for fast-resume."""
2
+
3
+ from pathlib import Path
4
+
5
+ # Agent colors and badges (badge is the display name shown in UI)
6
+ AGENTS = {
7
+ "claude": {"color": "#E87B35", "badge": "claude"},
8
+ "codex": {"color": "#00A67E", "badge": "codex"},
9
+ "opencode": {"color": "#CFCECD", "badge": "opencode"},
10
+ "vibe": {"color": "#FF6B35", "badge": "vibe"},
11
+ "crush": {"color": "#6B51FF", "badge": "crush"},
12
+ "copilot-cli": {"color": "#9CA3AF", "badge": "copilot"},
13
+ "copilot-vscode": {"color": "#007ACC", "badge": "vscode"},
14
+ }
15
+
16
+ # Storage paths
17
+ CLAUDE_DIR = Path.home() / ".claude" / "projects"
18
+ CODEX_DIR = Path.home() / ".codex" / "sessions"
19
+ OPENCODE_DIR = Path.home() / ".local" / "share" / "opencode" / "storage"
20
+ VIBE_DIR = Path.home() / ".vibe" / "logs" / "session"
21
+ CRUSH_PROJECTS_FILE = Path.home() / ".local" / "share" / "crush" / "projects.json"
22
+ COPILOT_DIR = Path.home() / ".copilot" / "session-state"
23
+
24
+ # Storage location
25
+ CACHE_DIR = Path.home() / ".cache" / "fast-resume"
26
+ INDEX_DIR = CACHE_DIR / "tantivy_index"
27
+ LOG_FILE = CACHE_DIR / "parse-errors.log"
28
+ SCHEMA_VERSION = (
29
+ 19 # Bump when schema changes (19: indexed timestamp for range queries)
30
+ )