ripperdoc 0.2.9__py3-none-any.whl → 0.2.10__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.
Files changed (45) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +235 -14
  3. ripperdoc/cli/commands/__init__.py +2 -0
  4. ripperdoc/cli/commands/agents_cmd.py +132 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/exit_cmd.py +1 -0
  7. ripperdoc/cli/commands/models_cmd.py +3 -3
  8. ripperdoc/cli/commands/resume_cmd.py +4 -0
  9. ripperdoc/cli/commands/stats_cmd.py +244 -0
  10. ripperdoc/cli/ui/panels.py +1 -0
  11. ripperdoc/cli/ui/rich_ui.py +295 -24
  12. ripperdoc/cli/ui/spinner.py +30 -18
  13. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  14. ripperdoc/cli/ui/wizard.py +6 -8
  15. ripperdoc/core/agents.py +10 -3
  16. ripperdoc/core/config.py +3 -6
  17. ripperdoc/core/default_tools.py +90 -10
  18. ripperdoc/core/hooks/events.py +4 -0
  19. ripperdoc/core/hooks/llm_callback.py +59 -0
  20. ripperdoc/core/permissions.py +78 -4
  21. ripperdoc/core/providers/openai.py +29 -19
  22. ripperdoc/core/query.py +192 -31
  23. ripperdoc/core/tool.py +9 -4
  24. ripperdoc/sdk/client.py +77 -2
  25. ripperdoc/tools/background_shell.py +305 -134
  26. ripperdoc/tools/bash_tool.py +42 -13
  27. ripperdoc/tools/file_edit_tool.py +159 -50
  28. ripperdoc/tools/file_read_tool.py +20 -0
  29. ripperdoc/tools/file_write_tool.py +7 -8
  30. ripperdoc/tools/lsp_tool.py +615 -0
  31. ripperdoc/tools/task_tool.py +514 -65
  32. ripperdoc/utils/conversation_compaction.py +1 -1
  33. ripperdoc/utils/file_watch.py +206 -3
  34. ripperdoc/utils/lsp.py +806 -0
  35. ripperdoc/utils/message_formatting.py +5 -2
  36. ripperdoc/utils/messages.py +21 -1
  37. ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
  38. ripperdoc/utils/session_heatmap.py +244 -0
  39. ripperdoc/utils/session_stats.py +293 -0
  40. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
  41. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/RECORD +45 -39
  42. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
  43. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
  44. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
  45. {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,244 @@
1
+ """Statistics command to show session usage patterns."""
2
+
3
+ from typing import Any
4
+
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+
8
+ from ripperdoc.utils.session_heatmap import render_heatmap
9
+ from ripperdoc.utils.session_stats import (
10
+ collect_session_stats,
11
+ format_duration,
12
+ format_large_number,
13
+ )
14
+
15
+ from .base import SlashCommand
16
+
17
+
18
+ def _format_number(n: int) -> str:
19
+ """Format number with thousands separator."""
20
+ return f"{n:,}"
21
+
22
+
23
+ def _truncate_model_name(model: str, max_len: int = 15) -> str:
24
+ """Truncate model name with ellipsis if too long."""
25
+ if len(model) <= max_len:
26
+ return model
27
+ return model[: max_len - 1] + "…"
28
+
29
+
30
+ def _get_token_comparison(total_tokens: int) -> str:
31
+ """Get a fun comparison for total tokens based on famous books.
32
+
33
+ Reference data from famous books' approximate token counts.
34
+ """
35
+ books = [
36
+ ("The Old Man and the Sea", 35000),
37
+ ("Animal Farm", 39000),
38
+ ("The Great Gatsby", 62000),
39
+ ("Brave New World", 83000),
40
+ ("Harry Potter and the Philosopher's Stone", 103000),
41
+ ("The Hobbit", 123000),
42
+ ("1984", 123000),
43
+ ("To Kill a Mockingbird", 130000),
44
+ ("Pride and Prejudice", 156000),
45
+ ("Anna Karenina", 468000),
46
+ ("Don Quixote", 520000),
47
+ ("The Lord of the Rings", 576000),
48
+ ("War and Peace", 730000),
49
+ ]
50
+
51
+ if total_tokens == 0:
52
+ return "Start chatting to build your stats!"
53
+
54
+ # Find the best matching book
55
+ for i, (name, tokens) in enumerate(books):
56
+ if total_tokens < tokens:
57
+ if i == 0:
58
+ # Less than the smallest book
59
+ percentage = (total_tokens / tokens) * 100
60
+ return f"You've used {percentage:.0f}% of the tokens in {name}"
61
+ else:
62
+ # Between two books
63
+ prev_name, prev_tokens = books[i - 1]
64
+ if total_tokens >= prev_tokens * 1.5:
65
+ # Closer to current book
66
+ multiplier = total_tokens / tokens
67
+ return f"You've used ~{multiplier:.1f}x the tokens in {name}"
68
+ else:
69
+ # Closer to previous book
70
+ multiplier = total_tokens / prev_tokens
71
+ return f"You've used ~{multiplier:.1f}x the tokens in {prev_name}"
72
+
73
+ # More than the largest book
74
+ largest_name, largest_tokens = books[-1]
75
+ multiplier = total_tokens / largest_tokens
76
+ return f"You've used ~{multiplier:.1f}x the tokens in {largest_name}!"
77
+
78
+
79
+ def _get_duration_comparison(duration_minutes: int) -> str:
80
+ """Get a fun comparison for session duration based on common activities.
81
+
82
+ Reference data for activity durations in minutes.
83
+ """
84
+ activities = [
85
+ ("a TED talk", 18),
86
+ ("an episode of The Office", 22),
87
+ ("a half marathon (average time)", 120),
88
+ ("the movie Inception", 148),
89
+ ("a transatlantic flight", 420),
90
+ ]
91
+
92
+ if duration_minutes == 0:
93
+ return ""
94
+
95
+ # Find the best matching activity
96
+ for i, (name, minutes) in enumerate(activities):
97
+ if duration_minutes < minutes:
98
+ if i == 0:
99
+ # Less than the shortest activity
100
+ percentage = (duration_minutes / minutes) * 100
101
+ return f"Longest session: {percentage:.0f}% of {name}"
102
+ else:
103
+ # Between two activities
104
+ prev_name, prev_minutes = activities[i - 1]
105
+ if duration_minutes >= prev_minutes * 1.5:
106
+ # Closer to current activity
107
+ multiplier = duration_minutes / minutes
108
+ return f"Longest session: ~{multiplier:.1f}x {name}"
109
+ else:
110
+ # Closer to previous activity
111
+ multiplier = duration_minutes / prev_minutes
112
+ return f"Longest session: ~{multiplier:.1f}x {prev_name}"
113
+
114
+ # More than the longest activity
115
+ longest_name, longest_minutes = activities[-1]
116
+ multiplier = duration_minutes / longest_minutes
117
+ return f"Longest session: ~{multiplier:.1f}x {longest_name}!"
118
+
119
+
120
+ def _handle(ui: Any, _args: str) -> bool:
121
+ """Handle the stats command."""
122
+ # Get project path from UI
123
+ project_path = getattr(ui, "project_path", None)
124
+ if project_path is None:
125
+ ui.console.print("[yellow]No project context available[/yellow]")
126
+ return True
127
+
128
+ # Collect statistics
129
+ console = ui.console
130
+ days = 365 # Look back full year for complete heatmap
131
+ weeks_count = 52 # Display 52 weeks (1 year)
132
+ stats = collect_session_stats(project_path, days=days)
133
+
134
+ if stats.total_sessions == 0:
135
+ console.print("[yellow]No session data found[/yellow]")
136
+ return True
137
+
138
+ # Create overview panel
139
+ console.print()
140
+ console.print("[bold]Activity Heatmap[/bold]")
141
+ console.print()
142
+
143
+ # Render heatmap with full year (52 weeks)
144
+ render_heatmap(console, stats.daily_activity, weeks_count=weeks_count)
145
+ console.print()
146
+
147
+ # Create two-column layout for statistics
148
+ # Left column: Model and tokens
149
+ # Right column: Sessions and streaks
150
+ table = Table.grid(padding=(0, 4))
151
+ table.add_column(style="cyan", justify="left")
152
+ table.add_column(justify="left")
153
+ table.add_column(style="cyan", justify="left")
154
+ table.add_column(justify="left")
155
+
156
+ # Row 1: Favorite model | Sessions
157
+ favorite_model_display = ""
158
+ if stats.favorite_model:
159
+ favorite_model_display = _truncate_model_name(stats.favorite_model)
160
+ table.add_row(
161
+ "Favorite model:",
162
+ favorite_model_display or "N/A",
163
+ "Sessions:",
164
+ f"{_format_number(stats.total_sessions)}",
165
+ )
166
+
167
+ # Row 2: Total tokens | Longest session
168
+ total_tokens_display = format_large_number(stats.total_tokens) if stats.total_tokens > 0 else "0"
169
+ longest_session_display = ""
170
+ if stats.longest_session_duration.total_seconds() > 0:
171
+ longest_session_display = format_duration(stats.longest_session_duration)
172
+ table.add_row(
173
+ "Total tokens:",
174
+ total_tokens_display,
175
+ "Longest session:",
176
+ longest_session_display or "N/A",
177
+ )
178
+
179
+ # Row 3: empty | Current streak
180
+ table.add_row(
181
+ "",
182
+ "",
183
+ "Current streak:",
184
+ f"{stats.current_streak} days",
185
+ )
186
+
187
+ # Row 4: empty | Longest streak
188
+ table.add_row(
189
+ "",
190
+ "",
191
+ "Longest streak:",
192
+ f"{stats.longest_streak} days",
193
+ )
194
+
195
+ # Row 5: empty | Active days
196
+ table.add_row(
197
+ "",
198
+ "",
199
+ "Active days:",
200
+ f"{stats.active_days}/{stats.total_days}",
201
+ )
202
+
203
+ # Row 6: empty | Peak hour
204
+ peak_hour_str = f"{stats.peak_hour:02d}:00-{stats.peak_hour + 1:02d}:00"
205
+ table.add_row(
206
+ "",
207
+ "",
208
+ "Peak hour:",
209
+ peak_hour_str,
210
+ )
211
+
212
+ # Create the main panel with statistics
213
+ # Match heatmap width: WEEKDAY_LABEL_WIDTH (8) + weeks_count (52) = 60
214
+ heatmap_width = 8 + weeks_count + 16
215
+ console.print(
216
+ Panel(table, title="Statistics", border_style="blue", width=heatmap_width)
217
+ )
218
+ console.print()
219
+
220
+ # Fun comparisons
221
+ # Token comparison
222
+ token_comparison = _get_token_comparison(stats.total_tokens)
223
+ if token_comparison:
224
+ console.print(f"[dim]{token_comparison}[/dim]")
225
+
226
+ # Duration comparison
227
+ duration_minutes = int(stats.longest_session_duration.total_seconds() / 60)
228
+ duration_comparison = _get_duration_comparison(duration_minutes)
229
+ if duration_comparison:
230
+ console.print(f"[dim]{duration_comparison}[/dim]")
231
+
232
+ console.print(f"[dim]Stats from the last {days} days[/dim]")
233
+ console.print()
234
+
235
+ return True
236
+
237
+
238
+ command = SlashCommand(
239
+ name="stats",
240
+ description="Show session statistics and activity patterns",
241
+ handler=_handle,
242
+ )
243
+
244
+ __all__ = ["command"]
@@ -55,6 +55,7 @@ def print_shortcuts(console: Console) -> None:
55
55
  pairs: List[Tuple[str, str]] = [
56
56
  ("? for shortcuts", "! for bash mode"),
57
57
  ("/ for commands", "@ for file mention"),
58
+ ("Alt+Enter for newline", "Enter to submit"),
58
59
  ]
59
60
  console.print("[dim]Shortcuts[/dim]")
60
61
  for left, right in pairs: