ripperdoc 0.2.6__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 +3 -0
- ripperdoc/__main__.py +20 -0
- ripperdoc/cli/__init__.py +1 -0
- ripperdoc/cli/cli.py +405 -0
- ripperdoc/cli/commands/__init__.py +82 -0
- ripperdoc/cli/commands/agents_cmd.py +263 -0
- ripperdoc/cli/commands/base.py +19 -0
- ripperdoc/cli/commands/clear_cmd.py +18 -0
- ripperdoc/cli/commands/compact_cmd.py +23 -0
- ripperdoc/cli/commands/config_cmd.py +31 -0
- ripperdoc/cli/commands/context_cmd.py +144 -0
- ripperdoc/cli/commands/cost_cmd.py +82 -0
- ripperdoc/cli/commands/doctor_cmd.py +221 -0
- ripperdoc/cli/commands/exit_cmd.py +19 -0
- ripperdoc/cli/commands/help_cmd.py +20 -0
- ripperdoc/cli/commands/mcp_cmd.py +70 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +413 -0
- ripperdoc/cli/commands/permissions_cmd.py +302 -0
- ripperdoc/cli/commands/resume_cmd.py +98 -0
- ripperdoc/cli/commands/status_cmd.py +167 -0
- ripperdoc/cli/commands/tasks_cmd.py +278 -0
- ripperdoc/cli/commands/todos_cmd.py +69 -0
- ripperdoc/cli/commands/tools_cmd.py +19 -0
- ripperdoc/cli/ui/__init__.py +1 -0
- ripperdoc/cli/ui/context_display.py +298 -0
- ripperdoc/cli/ui/helpers.py +22 -0
- ripperdoc/cli/ui/rich_ui.py +1557 -0
- ripperdoc/cli/ui/spinner.py +49 -0
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/cli/ui/tool_renderers.py +298 -0
- ripperdoc/core/__init__.py +1 -0
- ripperdoc/core/agents.py +486 -0
- ripperdoc/core/commands.py +33 -0
- ripperdoc/core/config.py +559 -0
- ripperdoc/core/default_tools.py +88 -0
- ripperdoc/core/permissions.py +252 -0
- ripperdoc/core/providers/__init__.py +47 -0
- ripperdoc/core/providers/anthropic.py +250 -0
- ripperdoc/core/providers/base.py +265 -0
- ripperdoc/core/providers/gemini.py +615 -0
- ripperdoc/core/providers/openai.py +487 -0
- ripperdoc/core/query.py +1058 -0
- ripperdoc/core/query_utils.py +622 -0
- ripperdoc/core/skills.py +295 -0
- ripperdoc/core/system_prompt.py +431 -0
- ripperdoc/core/tool.py +240 -0
- ripperdoc/sdk/__init__.py +9 -0
- ripperdoc/sdk/client.py +333 -0
- ripperdoc/tools/__init__.py +1 -0
- ripperdoc/tools/ask_user_question_tool.py +431 -0
- ripperdoc/tools/background_shell.py +389 -0
- ripperdoc/tools/bash_output_tool.py +98 -0
- ripperdoc/tools/bash_tool.py +1016 -0
- ripperdoc/tools/dynamic_mcp_tool.py +428 -0
- ripperdoc/tools/enter_plan_mode_tool.py +226 -0
- ripperdoc/tools/exit_plan_mode_tool.py +153 -0
- ripperdoc/tools/file_edit_tool.py +346 -0
- ripperdoc/tools/file_read_tool.py +203 -0
- ripperdoc/tools/file_write_tool.py +205 -0
- ripperdoc/tools/glob_tool.py +179 -0
- ripperdoc/tools/grep_tool.py +370 -0
- ripperdoc/tools/kill_bash_tool.py +136 -0
- ripperdoc/tools/ls_tool.py +471 -0
- ripperdoc/tools/mcp_tools.py +591 -0
- ripperdoc/tools/multi_edit_tool.py +456 -0
- ripperdoc/tools/notebook_edit_tool.py +386 -0
- ripperdoc/tools/skill_tool.py +205 -0
- ripperdoc/tools/task_tool.py +379 -0
- ripperdoc/tools/todo_tool.py +494 -0
- ripperdoc/tools/tool_search_tool.py +380 -0
- ripperdoc/utils/__init__.py +1 -0
- ripperdoc/utils/bash_constants.py +51 -0
- ripperdoc/utils/bash_output_utils.py +43 -0
- ripperdoc/utils/coerce.py +34 -0
- ripperdoc/utils/context_length_errors.py +252 -0
- ripperdoc/utils/exit_code_handlers.py +241 -0
- ripperdoc/utils/file_watch.py +135 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +176 -0
- ripperdoc/utils/mcp.py +560 -0
- ripperdoc/utils/memory.py +253 -0
- ripperdoc/utils/message_compaction.py +676 -0
- ripperdoc/utils/messages.py +519 -0
- ripperdoc/utils/output_utils.py +258 -0
- ripperdoc/utils/path_ignore.py +677 -0
- ripperdoc/utils/path_utils.py +46 -0
- ripperdoc/utils/permissions/__init__.py +27 -0
- ripperdoc/utils/permissions/path_validation_utils.py +174 -0
- ripperdoc/utils/permissions/shell_command_validation.py +552 -0
- ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +31 -0
- ripperdoc/utils/sandbox_utils.py +38 -0
- ripperdoc/utils/session_history.py +260 -0
- ripperdoc/utils/session_usage.py +117 -0
- ripperdoc/utils/shell_token_utils.py +95 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +203 -0
- ripperdoc/utils/token_estimation.py +34 -0
- ripperdoc-0.2.6.dist-info/METADATA +193 -0
- ripperdoc-0.2.6.dist-info/RECORD +107 -0
- ripperdoc-0.2.6.dist-info/WHEEL +5 -0
- ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
- ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
- ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Slash command to manage permission rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, List, Literal
|
|
7
|
+
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ripperdoc.core.config import (
|
|
13
|
+
config_manager,
|
|
14
|
+
get_global_config,
|
|
15
|
+
get_project_config,
|
|
16
|
+
get_project_local_config,
|
|
17
|
+
save_global_config,
|
|
18
|
+
save_project_config,
|
|
19
|
+
save_project_local_config,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .base import SlashCommand
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
ScopeType = Literal["user", "project", "local"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _shorten_path(path: Path, project_path: Path) -> str:
|
|
29
|
+
"""Return a short, user-friendly path."""
|
|
30
|
+
try:
|
|
31
|
+
return str(path.resolve().relative_to(project_path.resolve()))
|
|
32
|
+
except (ValueError, OSError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
home = Path.home()
|
|
36
|
+
try:
|
|
37
|
+
rel_home = path.resolve().relative_to(home)
|
|
38
|
+
return f"~/{rel_home}"
|
|
39
|
+
except (ValueError, OSError):
|
|
40
|
+
return str(path)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_scope_info(scope: ScopeType, project_path: Path) -> tuple[str, str]:
|
|
44
|
+
"""Return (heading, config_path) for a given scope."""
|
|
45
|
+
if scope == "user":
|
|
46
|
+
return "User (global)", str(Path.home() / ".ripperdoc.json")
|
|
47
|
+
elif scope == "project":
|
|
48
|
+
return "Project (shared)", str(project_path / ".ripperdoc" / "config.json")
|
|
49
|
+
else: # local
|
|
50
|
+
return "Local (private)", str(project_path / ".ripperdoc" / "config.local.json")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_rules_for_scope(
|
|
54
|
+
scope: ScopeType, project_path: Path
|
|
55
|
+
) -> tuple[List[str], List[str]]:
|
|
56
|
+
"""Return (allow_rules, deny_rules) for a given scope."""
|
|
57
|
+
if scope == "user":
|
|
58
|
+
config = get_global_config()
|
|
59
|
+
return list(config.user_allow_rules), list(config.user_deny_rules)
|
|
60
|
+
elif scope == "project":
|
|
61
|
+
config = get_project_config(project_path)
|
|
62
|
+
return list(config.bash_allow_rules), list(config.bash_deny_rules)
|
|
63
|
+
else: # local
|
|
64
|
+
config = get_project_local_config(project_path)
|
|
65
|
+
return list(config.local_allow_rules), list(config.local_deny_rules)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _add_rule(
|
|
69
|
+
scope: ScopeType,
|
|
70
|
+
rule_type: Literal["allow", "deny"],
|
|
71
|
+
rule: str,
|
|
72
|
+
project_path: Path,
|
|
73
|
+
) -> bool:
|
|
74
|
+
"""Add a rule to the specified scope. Returns True if added, False if already exists."""
|
|
75
|
+
if scope == "user":
|
|
76
|
+
config = get_global_config()
|
|
77
|
+
rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
|
|
78
|
+
if rule in rules:
|
|
79
|
+
return False
|
|
80
|
+
rules.append(rule)
|
|
81
|
+
save_global_config(config)
|
|
82
|
+
elif scope == "project":
|
|
83
|
+
config = get_project_config(project_path)
|
|
84
|
+
rules = config.bash_allow_rules if rule_type == "allow" else config.bash_deny_rules
|
|
85
|
+
if rule in rules:
|
|
86
|
+
return False
|
|
87
|
+
rules.append(rule)
|
|
88
|
+
save_project_config(config, project_path)
|
|
89
|
+
else: # local
|
|
90
|
+
config = get_project_local_config(project_path)
|
|
91
|
+
rules = config.local_allow_rules if rule_type == "allow" else config.local_deny_rules
|
|
92
|
+
if rule in rules:
|
|
93
|
+
return False
|
|
94
|
+
rules.append(rule)
|
|
95
|
+
save_project_local_config(config, project_path)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _remove_rule(
|
|
100
|
+
scope: ScopeType,
|
|
101
|
+
rule_type: Literal["allow", "deny"],
|
|
102
|
+
rule: str,
|
|
103
|
+
project_path: Path,
|
|
104
|
+
) -> bool:
|
|
105
|
+
"""Remove a rule from the specified scope. Returns True if removed, False if not found."""
|
|
106
|
+
if scope == "user":
|
|
107
|
+
config = get_global_config()
|
|
108
|
+
rules = config.user_allow_rules if rule_type == "allow" else config.user_deny_rules
|
|
109
|
+
if rule not in rules:
|
|
110
|
+
return False
|
|
111
|
+
rules.remove(rule)
|
|
112
|
+
save_global_config(config)
|
|
113
|
+
elif scope == "project":
|
|
114
|
+
config = get_project_config(project_path)
|
|
115
|
+
rules = config.bash_allow_rules if rule_type == "allow" else config.bash_deny_rules
|
|
116
|
+
if rule not in rules:
|
|
117
|
+
return False
|
|
118
|
+
rules.remove(rule)
|
|
119
|
+
save_project_config(config, project_path)
|
|
120
|
+
else: # local
|
|
121
|
+
config = get_project_local_config(project_path)
|
|
122
|
+
rules = config.local_allow_rules if rule_type == "allow" else config.local_deny_rules
|
|
123
|
+
if rule not in rules:
|
|
124
|
+
return False
|
|
125
|
+
rules.remove(rule)
|
|
126
|
+
save_project_local_config(config, project_path)
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _render_all_rules(console: Any, project_path: Path) -> None:
|
|
131
|
+
"""Display all permission rules from all scopes."""
|
|
132
|
+
table = Table(title="Permission Rules", show_header=True, header_style="bold cyan")
|
|
133
|
+
table.add_column("Scope", style="bold")
|
|
134
|
+
table.add_column("Type", style="dim")
|
|
135
|
+
table.add_column("Rule")
|
|
136
|
+
|
|
137
|
+
has_rules = False
|
|
138
|
+
|
|
139
|
+
for scope in ("user", "project", "local"):
|
|
140
|
+
allow_rules, deny_rules = _get_rules_for_scope(scope, project_path) # type: ignore
|
|
141
|
+
|
|
142
|
+
for rule in allow_rules:
|
|
143
|
+
table.add_row(scope, "[green]allow[/green]", escape(rule))
|
|
144
|
+
has_rules = True
|
|
145
|
+
|
|
146
|
+
for rule in deny_rules:
|
|
147
|
+
table.add_row(scope, "[red]deny[/red]", escape(rule))
|
|
148
|
+
has_rules = True
|
|
149
|
+
|
|
150
|
+
if has_rules:
|
|
151
|
+
console.print(table)
|
|
152
|
+
else:
|
|
153
|
+
console.print("[yellow]No permission rules configured yet.[/yellow]")
|
|
154
|
+
|
|
155
|
+
console.print()
|
|
156
|
+
console.print("[dim]Scopes (in priority order):[/dim]")
|
|
157
|
+
console.print("[dim] - user: Global rules (~/.ripperdoc.json)[/dim]")
|
|
158
|
+
console.print("[dim] - project: Shared project rules (.ripperdoc/config.json)[/dim]")
|
|
159
|
+
console.print("[dim] - local: Private project rules (.ripperdoc/config.local.json)[/dim]")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _render_scope_rules(console: Any, scope: ScopeType, project_path: Path) -> None:
|
|
163
|
+
"""Display rules for a specific scope."""
|
|
164
|
+
heading, config_path = _get_scope_info(scope, project_path)
|
|
165
|
+
allow_rules, deny_rules = _get_rules_for_scope(scope, project_path)
|
|
166
|
+
|
|
167
|
+
table = Table(title=f"{heading} Permission Rules", show_header=True, header_style="bold cyan")
|
|
168
|
+
table.add_column("Type", style="dim")
|
|
169
|
+
table.add_column("Rule")
|
|
170
|
+
|
|
171
|
+
has_rules = False
|
|
172
|
+
for rule in allow_rules:
|
|
173
|
+
table.add_row("[green]allow[/green]", escape(rule))
|
|
174
|
+
has_rules = True
|
|
175
|
+
|
|
176
|
+
for rule in deny_rules:
|
|
177
|
+
table.add_row("[red]deny[/red]", escape(rule))
|
|
178
|
+
has_rules = True
|
|
179
|
+
|
|
180
|
+
if has_rules:
|
|
181
|
+
console.print(table)
|
|
182
|
+
else:
|
|
183
|
+
console.print(f"[yellow]No {scope} rules configured.[/yellow]")
|
|
184
|
+
|
|
185
|
+
console.print(f"[dim]Config file: {escape(config_path)}[/dim]")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
189
|
+
project_path = getattr(ui, "project_path", Path.cwd())
|
|
190
|
+
args = trimmed_arg.strip().split()
|
|
191
|
+
|
|
192
|
+
# No args: show all rules
|
|
193
|
+
if not args:
|
|
194
|
+
_render_all_rules(ui.console, project_path)
|
|
195
|
+
ui.console.print()
|
|
196
|
+
ui.console.print("[dim]Usage:[/dim]")
|
|
197
|
+
ui.console.print("[dim] /permissions - Show all rules[/dim]")
|
|
198
|
+
ui.console.print("[dim] /permissions <scope> - Show rules for scope[/dim]")
|
|
199
|
+
ui.console.print("[dim] /permissions add <scope> <type> <rule> - Add a rule[/dim]")
|
|
200
|
+
ui.console.print("[dim] /permissions remove <scope> <type> <rule> - Remove a rule[/dim]")
|
|
201
|
+
ui.console.print("[dim]Scopes: user, project, local[/dim]")
|
|
202
|
+
ui.console.print("[dim]Types: allow, deny[/dim]")
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
# Parse command
|
|
206
|
+
action = args[0].lower()
|
|
207
|
+
scope_aliases = {
|
|
208
|
+
"user": "user",
|
|
209
|
+
"global": "user",
|
|
210
|
+
"project": "project",
|
|
211
|
+
"workspace": "project",
|
|
212
|
+
"local": "local",
|
|
213
|
+
"private": "local",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Single scope display
|
|
217
|
+
if action in scope_aliases:
|
|
218
|
+
scope = scope_aliases[action]
|
|
219
|
+
_render_scope_rules(ui.console, scope, project_path) # type: ignore
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
# Add rule
|
|
223
|
+
if action == "add":
|
|
224
|
+
if len(args) < 4:
|
|
225
|
+
ui.console.print("[red]Usage: /permissions add <scope> <type> <rule>[/red]")
|
|
226
|
+
ui.console.print("[dim]Example: /permissions add local allow npm test[/dim]")
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
scope_arg = args[1].lower()
|
|
230
|
+
if scope_arg not in scope_aliases:
|
|
231
|
+
ui.console.print(f"[red]Unknown scope: {escape(scope_arg)}[/red]")
|
|
232
|
+
ui.console.print("[dim]Available scopes: user, project, local[/dim]")
|
|
233
|
+
return True
|
|
234
|
+
scope: ScopeType = scope_aliases[scope_arg] # type: ignore
|
|
235
|
+
|
|
236
|
+
rule_type_arg = args[2].lower()
|
|
237
|
+
if rule_type_arg not in ("allow", "deny"):
|
|
238
|
+
ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
|
|
239
|
+
ui.console.print("[dim]Available types: allow, deny[/dim]")
|
|
240
|
+
return True
|
|
241
|
+
rule_type: Literal["allow", "deny"] = rule_type_arg # type: ignore
|
|
242
|
+
|
|
243
|
+
rule = " ".join(args[3:])
|
|
244
|
+
if _add_rule(scope, rule_type, rule, project_path):
|
|
245
|
+
ui.console.print(
|
|
246
|
+
Panel(
|
|
247
|
+
f"Added [{'green' if rule_type == 'allow' else 'red'}]{rule_type}[/] rule to {scope}:\n{escape(rule)}",
|
|
248
|
+
title="/permissions",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
ui.console.print(f"[yellow]Rule already exists in {scope}.[/yellow]")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
# Remove rule
|
|
256
|
+
if action in ("remove", "rm", "delete", "del"):
|
|
257
|
+
if len(args) < 4:
|
|
258
|
+
ui.console.print("[red]Usage: /permissions remove <scope> <type> <rule>[/red]")
|
|
259
|
+
ui.console.print("[dim]Example: /permissions remove local allow npm test[/dim]")
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
scope_arg = args[1].lower()
|
|
263
|
+
if scope_arg not in scope_aliases:
|
|
264
|
+
ui.console.print(f"[red]Unknown scope: {escape(scope_arg)}[/red]")
|
|
265
|
+
ui.console.print("[dim]Available scopes: user, project, local[/dim]")
|
|
266
|
+
return True
|
|
267
|
+
scope = scope_aliases[scope_arg] # type: ignore
|
|
268
|
+
|
|
269
|
+
rule_type_arg = args[2].lower()
|
|
270
|
+
if rule_type_arg not in ("allow", "deny"):
|
|
271
|
+
ui.console.print(f"[red]Unknown rule type: {escape(rule_type_arg)}[/red]")
|
|
272
|
+
ui.console.print("[dim]Available types: allow, deny[/dim]")
|
|
273
|
+
return True
|
|
274
|
+
rule_type = rule_type_arg # type: ignore
|
|
275
|
+
|
|
276
|
+
rule = " ".join(args[3:])
|
|
277
|
+
if _remove_rule(scope, rule_type, rule, project_path):
|
|
278
|
+
ui.console.print(
|
|
279
|
+
Panel(
|
|
280
|
+
f"Removed [{'green' if rule_type == 'allow' else 'red'}]{rule_type}[/] rule from {scope}:\n{escape(rule)}",
|
|
281
|
+
title="/permissions",
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
ui.console.print(f"[yellow]Rule not found in {scope}.[/yellow]")
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
# Unknown command
|
|
289
|
+
ui.console.print(f"[red]Unknown action: {escape(action)}[/red]")
|
|
290
|
+
ui.console.print("[dim]Available actions: add, remove, or a scope name[/dim]")
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
command = SlashCommand(
|
|
295
|
+
name="permissions",
|
|
296
|
+
description="Manage permission rules for tools",
|
|
297
|
+
handler=_handle,
|
|
298
|
+
aliases=(),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rich.markup import escape
|
|
6
|
+
|
|
7
|
+
from ripperdoc.utils.session_history import (
|
|
8
|
+
SessionSummary,
|
|
9
|
+
list_session_summaries,
|
|
10
|
+
load_session_messages,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .base import SlashCommand
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _format_time(dt: datetime) -> str:
|
|
17
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
|
|
21
|
+
sessions = list_session_summaries(ui.project_path)
|
|
22
|
+
if not sessions:
|
|
23
|
+
ui.console.print("[yellow]No saved sessions found for this project.[/yellow]")
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
# If a numeric arg is provided, try to resolve immediately.
|
|
27
|
+
if arg.strip():
|
|
28
|
+
if arg.isdigit():
|
|
29
|
+
idx = int(arg)
|
|
30
|
+
if 0 <= idx < len(sessions):
|
|
31
|
+
return sessions[idx]
|
|
32
|
+
ui.console.print(
|
|
33
|
+
f"[red]Invalid session index {escape(str(idx))}. "
|
|
34
|
+
f"Choose 0-{len(sessions) - 1}.[/red]"
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
# Treat arg as session id if it matches.
|
|
38
|
+
match = next((s for s in sessions if s.session_id.startswith(arg.strip())), None)
|
|
39
|
+
if match:
|
|
40
|
+
return match
|
|
41
|
+
ui.console.print(f"[red]No session found matching '{escape(arg)}'.[/red]")
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
ui.console.print("\n[bold]Saved sessions:[/bold]")
|
|
45
|
+
for idx, summary in enumerate(sessions):
|
|
46
|
+
ui.console.print(
|
|
47
|
+
f" [{idx}] {summary.session_id} "
|
|
48
|
+
f"({summary.message_count} messages, "
|
|
49
|
+
f"{_format_time(summary.created_at)} → {_format_time(summary.updated_at)}) "
|
|
50
|
+
f"{escape(summary.first_prompt)}",
|
|
51
|
+
markup=False,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
choice_text = ui.console.input("\nSelect a session index (Enter to cancel): ").strip()
|
|
55
|
+
if not choice_text:
|
|
56
|
+
return None
|
|
57
|
+
if not choice_text.isdigit():
|
|
58
|
+
ui.console.print("[red]Please enter a number.[/red]")
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
idx = int(choice_text)
|
|
62
|
+
if idx < 0 or idx >= len(sessions):
|
|
63
|
+
ui.console.print(
|
|
64
|
+
f"[red]Invalid session index {escape(str(idx))}. Choose 0-{len(sessions) - 1}.[/red]"
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
67
|
+
return sessions[idx]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _handle(ui: Any, arg: str) -> bool:
|
|
71
|
+
summary = _choose_session(ui, arg)
|
|
72
|
+
if not summary:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
messages = load_session_messages(ui.project_path, summary.session_id)
|
|
76
|
+
if not messages:
|
|
77
|
+
ui.console.print("[red]Failed to load messages for the selected session.[/red]")
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
ui.conversation_messages = messages
|
|
81
|
+
ui._saved_conversation = None
|
|
82
|
+
ui._set_session(summary.session_id)
|
|
83
|
+
ui.replay_conversation(messages)
|
|
84
|
+
ui.console.print(
|
|
85
|
+
f"[green]✓ Resumed session {escape(summary.session_id)} "
|
|
86
|
+
f"with {len(messages)} messages.[/green]"
|
|
87
|
+
)
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
command = SlashCommand(
|
|
92
|
+
name="resume",
|
|
93
|
+
description="Resume a previous session conversation",
|
|
94
|
+
handler=_handle,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["command"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from ripperdoc import __version__
|
|
7
|
+
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
8
|
+
from ripperdoc.core.config import (
|
|
9
|
+
ModelProfile,
|
|
10
|
+
ProviderType,
|
|
11
|
+
api_base_env_candidates,
|
|
12
|
+
api_key_env_candidates,
|
|
13
|
+
get_global_config,
|
|
14
|
+
)
|
|
15
|
+
from ripperdoc.utils.memory import MAX_CONTENT_LENGTH, MemoryFile, collect_all_memory_files
|
|
16
|
+
|
|
17
|
+
from .base import SlashCommand
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _auth_token_display(profile: Optional[ModelProfile]) -> Tuple[str, Optional[str]]:
|
|
21
|
+
"""Return a safe auth token summary and the env var used, if any."""
|
|
22
|
+
if not profile:
|
|
23
|
+
return ("Not configured", None)
|
|
24
|
+
|
|
25
|
+
provider_value = (
|
|
26
|
+
profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
env_candidates = api_key_env_candidates(profile.provider)
|
|
30
|
+
provider_env = f"{provider_value.upper()}_API_KEY" if provider_value else None
|
|
31
|
+
if provider_env and provider_env not in env_candidates:
|
|
32
|
+
env_candidates = [provider_env, *env_candidates]
|
|
33
|
+
|
|
34
|
+
env_var = next((name for name in env_candidates if os.environ.get(name)), None)
|
|
35
|
+
if env_var:
|
|
36
|
+
return (f"{env_var} (env)", env_var)
|
|
37
|
+
if profile.api_key or getattr(profile, "auth_token", None):
|
|
38
|
+
return ("Configured in profile", None)
|
|
39
|
+
return ("Missing", None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _api_base_display(profile: Optional[ModelProfile]) -> str:
|
|
43
|
+
"""Return a human-readable API base URL line."""
|
|
44
|
+
if not profile:
|
|
45
|
+
return "API base URL: Not configured"
|
|
46
|
+
|
|
47
|
+
label_map = {
|
|
48
|
+
ProviderType.ANTHROPIC: "Anthropic base URL",
|
|
49
|
+
ProviderType.OPENAI_COMPATIBLE: "OpenAI-compatible base URL",
|
|
50
|
+
ProviderType.GEMINI: "Gemini base URL",
|
|
51
|
+
}
|
|
52
|
+
label = label_map.get(profile.provider, "API base URL")
|
|
53
|
+
provider_value = (
|
|
54
|
+
profile.provider.value if hasattr(profile.provider, "value") else str(profile.provider)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
env_candidates = api_base_env_candidates(profile.provider)
|
|
58
|
+
provider_env = f"{provider_value.upper()}_BASE_URL" if provider_value else None
|
|
59
|
+
if provider_env and provider_env not in env_candidates:
|
|
60
|
+
env_candidates = [provider_env, *env_candidates]
|
|
61
|
+
|
|
62
|
+
base_url = profile.api_base
|
|
63
|
+
if not base_url:
|
|
64
|
+
base_url = next(
|
|
65
|
+
(os.environ.get(name) for name in env_candidates if os.environ.get(name)), None
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return f"{label}: {base_url or 'default'}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _memory_status_lines(memory_files: List[MemoryFile]) -> List[str]:
|
|
72
|
+
"""Summarize AGENTS memory files and any issues."""
|
|
73
|
+
if not memory_files:
|
|
74
|
+
return ["None detected"]
|
|
75
|
+
|
|
76
|
+
lines = [f"{len(memory_files)} file(s) loaded"]
|
|
77
|
+
oversized = [memory for memory in memory_files if len(memory.content) > MAX_CONTENT_LENGTH]
|
|
78
|
+
for memory in oversized:
|
|
79
|
+
lines.append(f"! {memory.path} ({len(memory.content)} chars > {MAX_CONTENT_LENGTH})")
|
|
80
|
+
return lines
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _setting_sources_summary(
|
|
84
|
+
config: Any,
|
|
85
|
+
profile: Optional[ModelProfile],
|
|
86
|
+
memory_files: List[MemoryFile],
|
|
87
|
+
auth_env_var: Optional[str],
|
|
88
|
+
safe_mode: bool,
|
|
89
|
+
verbose: bool,
|
|
90
|
+
project_path: Path,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Describe where settings for this session were sourced from."""
|
|
93
|
+
sources: list[str] = []
|
|
94
|
+
if (Path.home() / ".ripperdoc.json").exists():
|
|
95
|
+
sources.append("User settings")
|
|
96
|
+
|
|
97
|
+
project_config_path = project_path / ".ripperdoc" / "config.json"
|
|
98
|
+
if project_config_path.exists():
|
|
99
|
+
sources.append("Project settings")
|
|
100
|
+
|
|
101
|
+
if any(memory.type == "Local" for memory in memory_files):
|
|
102
|
+
sources.append("Local memory")
|
|
103
|
+
if any(memory.type == "Project" for memory in memory_files):
|
|
104
|
+
sources.append("Project memory")
|
|
105
|
+
if auth_env_var:
|
|
106
|
+
sources.append("Environment variables")
|
|
107
|
+
|
|
108
|
+
config_safe_mode = getattr(config, "safe_mode", True)
|
|
109
|
+
config_verbose = getattr(config, "verbose", False)
|
|
110
|
+
if safe_mode != config_safe_mode or verbose != config_verbose:
|
|
111
|
+
sources.append("Command line arguments")
|
|
112
|
+
|
|
113
|
+
if profile and profile.api_key and not auth_env_var:
|
|
114
|
+
sources.append("Profile configuration")
|
|
115
|
+
|
|
116
|
+
if not sources:
|
|
117
|
+
sources.append("Defaults")
|
|
118
|
+
|
|
119
|
+
unique_sources = list(dict.fromkeys(sources))
|
|
120
|
+
return ", ".join(unique_sources)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
124
|
+
config = get_global_config()
|
|
125
|
+
profile = get_profile_for_pointer("main")
|
|
126
|
+
memory_files = collect_all_memory_files()
|
|
127
|
+
|
|
128
|
+
auth_summary, auth_env_var = _auth_token_display(profile)
|
|
129
|
+
api_base_summary = _api_base_display(profile)
|
|
130
|
+
memory_lines = _memory_status_lines(memory_files)
|
|
131
|
+
setting_sources = _setting_sources_summary(
|
|
132
|
+
config,
|
|
133
|
+
profile,
|
|
134
|
+
memory_files,
|
|
135
|
+
auth_env_var,
|
|
136
|
+
ui.safe_mode,
|
|
137
|
+
ui.verbose,
|
|
138
|
+
ui.project_path,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
model_label = profile.model if profile else "Not configured"
|
|
142
|
+
|
|
143
|
+
ui.console.print()
|
|
144
|
+
ui.console.rule()
|
|
145
|
+
ui.console.print(" Status:\n")
|
|
146
|
+
ui.console.print(f" Version: {__version__}")
|
|
147
|
+
ui.console.print(f" Session ID: {ui.session_id}")
|
|
148
|
+
ui.console.print(f" cwd: {Path.cwd()}")
|
|
149
|
+
ui.console.print(f" Auth token: {auth_summary}")
|
|
150
|
+
ui.console.print(f" {api_base_summary}")
|
|
151
|
+
ui.console.print()
|
|
152
|
+
ui.console.print(f" Model: {model_label}")
|
|
153
|
+
ui.console.print(" Memory:")
|
|
154
|
+
for line in memory_lines:
|
|
155
|
+
ui.console.print(f" {line}")
|
|
156
|
+
ui.console.print(f" Setting sources: {setting_sources}")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
command = SlashCommand(
|
|
161
|
+
name="status",
|
|
162
|
+
description="Show session status",
|
|
163
|
+
handler=_handle,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["command"]
|