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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +235 -14
- ripperdoc/cli/commands/__init__.py +2 -0
- ripperdoc/cli/commands/agents_cmd.py +132 -5
- ripperdoc/cli/commands/clear_cmd.py +8 -0
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/models_cmd.py +3 -3
- ripperdoc/cli/commands/resume_cmd.py +4 -0
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/rich_ui.py +295 -24
- ripperdoc/cli/ui/spinner.py +30 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/wizard.py +6 -8
- ripperdoc/core/agents.py +10 -3
- ripperdoc/core/config.py +3 -6
- ripperdoc/core/default_tools.py +90 -10
- ripperdoc/core/hooks/events.py +4 -0
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/permissions.py +78 -4
- ripperdoc/core/providers/openai.py +29 -19
- ripperdoc/core/query.py +192 -31
- ripperdoc/core/tool.py +9 -4
- ripperdoc/sdk/client.py +77 -2
- ripperdoc/tools/background_shell.py +305 -134
- ripperdoc/tools/bash_tool.py +42 -13
- ripperdoc/tools/file_edit_tool.py +159 -50
- ripperdoc/tools/file_read_tool.py +20 -0
- ripperdoc/tools/file_write_tool.py +7 -8
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/task_tool.py +514 -65
- ripperdoc/utils/conversation_compaction.py +1 -1
- ripperdoc/utils/file_watch.py +206 -3
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/message_formatting.py +5 -2
- ripperdoc/utils/messages.py +21 -1
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_stats.py +293 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/RECORD +45 -39
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.9.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {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"]
|
ripperdoc/cli/ui/panels.py
CHANGED
|
@@ -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:
|