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.
Files changed (107) hide show
  1. ripperdoc/__init__.py +3 -0
  2. ripperdoc/__main__.py +20 -0
  3. ripperdoc/cli/__init__.py +1 -0
  4. ripperdoc/cli/cli.py +405 -0
  5. ripperdoc/cli/commands/__init__.py +82 -0
  6. ripperdoc/cli/commands/agents_cmd.py +263 -0
  7. ripperdoc/cli/commands/base.py +19 -0
  8. ripperdoc/cli/commands/clear_cmd.py +18 -0
  9. ripperdoc/cli/commands/compact_cmd.py +23 -0
  10. ripperdoc/cli/commands/config_cmd.py +31 -0
  11. ripperdoc/cli/commands/context_cmd.py +144 -0
  12. ripperdoc/cli/commands/cost_cmd.py +82 -0
  13. ripperdoc/cli/commands/doctor_cmd.py +221 -0
  14. ripperdoc/cli/commands/exit_cmd.py +19 -0
  15. ripperdoc/cli/commands/help_cmd.py +20 -0
  16. ripperdoc/cli/commands/mcp_cmd.py +70 -0
  17. ripperdoc/cli/commands/memory_cmd.py +202 -0
  18. ripperdoc/cli/commands/models_cmd.py +413 -0
  19. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  20. ripperdoc/cli/commands/resume_cmd.py +98 -0
  21. ripperdoc/cli/commands/status_cmd.py +167 -0
  22. ripperdoc/cli/commands/tasks_cmd.py +278 -0
  23. ripperdoc/cli/commands/todos_cmd.py +69 -0
  24. ripperdoc/cli/commands/tools_cmd.py +19 -0
  25. ripperdoc/cli/ui/__init__.py +1 -0
  26. ripperdoc/cli/ui/context_display.py +298 -0
  27. ripperdoc/cli/ui/helpers.py +22 -0
  28. ripperdoc/cli/ui/rich_ui.py +1557 -0
  29. ripperdoc/cli/ui/spinner.py +49 -0
  30. ripperdoc/cli/ui/thinking_spinner.py +128 -0
  31. ripperdoc/cli/ui/tool_renderers.py +298 -0
  32. ripperdoc/core/__init__.py +1 -0
  33. ripperdoc/core/agents.py +486 -0
  34. ripperdoc/core/commands.py +33 -0
  35. ripperdoc/core/config.py +559 -0
  36. ripperdoc/core/default_tools.py +88 -0
  37. ripperdoc/core/permissions.py +252 -0
  38. ripperdoc/core/providers/__init__.py +47 -0
  39. ripperdoc/core/providers/anthropic.py +250 -0
  40. ripperdoc/core/providers/base.py +265 -0
  41. ripperdoc/core/providers/gemini.py +615 -0
  42. ripperdoc/core/providers/openai.py +487 -0
  43. ripperdoc/core/query.py +1058 -0
  44. ripperdoc/core/query_utils.py +622 -0
  45. ripperdoc/core/skills.py +295 -0
  46. ripperdoc/core/system_prompt.py +431 -0
  47. ripperdoc/core/tool.py +240 -0
  48. ripperdoc/sdk/__init__.py +9 -0
  49. ripperdoc/sdk/client.py +333 -0
  50. ripperdoc/tools/__init__.py +1 -0
  51. ripperdoc/tools/ask_user_question_tool.py +431 -0
  52. ripperdoc/tools/background_shell.py +389 -0
  53. ripperdoc/tools/bash_output_tool.py +98 -0
  54. ripperdoc/tools/bash_tool.py +1016 -0
  55. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  56. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  57. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  58. ripperdoc/tools/file_edit_tool.py +346 -0
  59. ripperdoc/tools/file_read_tool.py +203 -0
  60. ripperdoc/tools/file_write_tool.py +205 -0
  61. ripperdoc/tools/glob_tool.py +179 -0
  62. ripperdoc/tools/grep_tool.py +370 -0
  63. ripperdoc/tools/kill_bash_tool.py +136 -0
  64. ripperdoc/tools/ls_tool.py +471 -0
  65. ripperdoc/tools/mcp_tools.py +591 -0
  66. ripperdoc/tools/multi_edit_tool.py +456 -0
  67. ripperdoc/tools/notebook_edit_tool.py +386 -0
  68. ripperdoc/tools/skill_tool.py +205 -0
  69. ripperdoc/tools/task_tool.py +379 -0
  70. ripperdoc/tools/todo_tool.py +494 -0
  71. ripperdoc/tools/tool_search_tool.py +380 -0
  72. ripperdoc/utils/__init__.py +1 -0
  73. ripperdoc/utils/bash_constants.py +51 -0
  74. ripperdoc/utils/bash_output_utils.py +43 -0
  75. ripperdoc/utils/coerce.py +34 -0
  76. ripperdoc/utils/context_length_errors.py +252 -0
  77. ripperdoc/utils/exit_code_handlers.py +241 -0
  78. ripperdoc/utils/file_watch.py +135 -0
  79. ripperdoc/utils/git_utils.py +274 -0
  80. ripperdoc/utils/json_utils.py +27 -0
  81. ripperdoc/utils/log.py +176 -0
  82. ripperdoc/utils/mcp.py +560 -0
  83. ripperdoc/utils/memory.py +253 -0
  84. ripperdoc/utils/message_compaction.py +676 -0
  85. ripperdoc/utils/messages.py +519 -0
  86. ripperdoc/utils/output_utils.py +258 -0
  87. ripperdoc/utils/path_ignore.py +677 -0
  88. ripperdoc/utils/path_utils.py +46 -0
  89. ripperdoc/utils/permissions/__init__.py +27 -0
  90. ripperdoc/utils/permissions/path_validation_utils.py +174 -0
  91. ripperdoc/utils/permissions/shell_command_validation.py +552 -0
  92. ripperdoc/utils/permissions/tool_permission_utils.py +279 -0
  93. ripperdoc/utils/prompt.py +17 -0
  94. ripperdoc/utils/safe_get_cwd.py +31 -0
  95. ripperdoc/utils/sandbox_utils.py +38 -0
  96. ripperdoc/utils/session_history.py +260 -0
  97. ripperdoc/utils/session_usage.py +117 -0
  98. ripperdoc/utils/shell_token_utils.py +95 -0
  99. ripperdoc/utils/shell_utils.py +159 -0
  100. ripperdoc/utils/todo.py +203 -0
  101. ripperdoc/utils/token_estimation.py +34 -0
  102. ripperdoc-0.2.6.dist-info/METADATA +193 -0
  103. ripperdoc-0.2.6.dist-info/RECORD +107 -0
  104. ripperdoc-0.2.6.dist-info/WHEEL +5 -0
  105. ripperdoc-0.2.6.dist-info/entry_points.txt +3 -0
  106. ripperdoc-0.2.6.dist-info/licenses/LICENSE +53 -0
  107. ripperdoc-0.2.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,413 @@
1
+ from typing import Any, Optional
2
+
3
+ from rich.markup import escape
4
+
5
+ from ripperdoc.cli.ui.helpers import get_profile_for_pointer
6
+ from ripperdoc.core.config import (
7
+ ModelProfile,
8
+ ProviderType,
9
+ add_model_profile,
10
+ delete_model_profile,
11
+ get_global_config,
12
+ set_model_pointer,
13
+ )
14
+ from ripperdoc.utils.log import get_logger
15
+ from ripperdoc.utils.prompt import prompt_secret
16
+
17
+ from .base import SlashCommand
18
+
19
+ logger = get_logger()
20
+
21
+
22
+ def _handle(ui: Any, trimmed_arg: str) -> bool:
23
+ console = ui.console
24
+ tokens = trimmed_arg.split()
25
+ subcmd = tokens[0].lower() if tokens else ""
26
+ config = get_global_config()
27
+ logger.info(
28
+ "[models_cmd] Handling /models command",
29
+ extra={"subcommand": subcmd or "list", "session_id": getattr(ui, "session_id", None)},
30
+ )
31
+
32
+ def print_models_usage() -> None:
33
+ console.print("[bold]/models[/bold] — list configured models")
34
+ console.print("[bold]/models add <name>[/bold] — add or update a model profile")
35
+ console.print("[bold]/models edit <name>[/bold] — edit an existing model profile")
36
+ console.print("[bold]/models delete <name>[/bold] — delete a model profile")
37
+ console.print("[bold]/models use <name>[/bold] — set the main model pointer")
38
+ console.print("[bold]/models use <pointer> <name>[/bold] — set a specific pointer (main/task/reasoning/quick)")
39
+
40
+ def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
41
+ raw = console.input(prompt_text).strip()
42
+ if not raw:
43
+ return default_value
44
+ try:
45
+ return int(raw)
46
+ except ValueError:
47
+ console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
48
+ return default_value
49
+
50
+ def parse_float(prompt_text: str, default_value: float) -> float:
51
+ raw = console.input(prompt_text).strip()
52
+ if not raw:
53
+ return default_value
54
+ try:
55
+ return float(raw)
56
+ except ValueError:
57
+ console.print("[yellow]Invalid number, keeping previous value.[/yellow]")
58
+ return default_value
59
+
60
+ if subcmd in ("help", "-h", "--help"):
61
+ print_models_usage()
62
+ return True
63
+
64
+ if subcmd in ("add", "create"):
65
+ profile_name = tokens[1] if len(tokens) > 1 else console.input("Profile name: ").strip()
66
+ if not profile_name:
67
+ console.print("[red]Model profile name is required.[/red]")
68
+ print_models_usage()
69
+ return True
70
+
71
+ overwrite = False
72
+ existing_profile = config.model_profiles.get(profile_name)
73
+ if existing_profile:
74
+ confirm = (
75
+ console.input(f"Profile '{profile_name}' exists. Overwrite? [y/N]: ")
76
+ .strip()
77
+ .lower()
78
+ )
79
+ if confirm not in ("y", "yes"):
80
+ return True
81
+ overwrite = True
82
+
83
+ current_profile = get_profile_for_pointer("main")
84
+ default_provider = (
85
+ (current_profile.provider.value) if current_profile else ProviderType.ANTHROPIC.value
86
+ )
87
+ provider_input = (
88
+ console.input(
89
+ f"Protocol ({', '.join(p.value for p in ProviderType)}) [{default_provider}]: "
90
+ )
91
+ .strip()
92
+ .lower()
93
+ or default_provider
94
+ )
95
+ try:
96
+ provider = ProviderType(provider_input)
97
+ except ValueError:
98
+ console.print(f"[red]Invalid provider: {escape(provider_input)}[/red]")
99
+ print_models_usage()
100
+ return True
101
+
102
+ default_model = (
103
+ existing_profile.model
104
+ if existing_profile
105
+ else (current_profile.model if current_profile else "")
106
+ )
107
+ model_prompt = f"Model name to send{f' [{default_model}]' if default_model else ''}: "
108
+ model_name = console.input(model_prompt).strip() or default_model
109
+ if not model_name:
110
+ console.print("[red]Model name is required.[/red]")
111
+ return True
112
+
113
+ api_key_input = prompt_secret("API key (leave blank to keep unset)").strip()
114
+ api_key = api_key_input or (existing_profile.api_key if existing_profile else None)
115
+
116
+ auth_token = existing_profile.auth_token if existing_profile else None
117
+ if provider == ProviderType.ANTHROPIC:
118
+ auth_token_input = prompt_secret(
119
+ "Auth token (Anthropic only, leave blank to keep unset)"
120
+ ).strip()
121
+ auth_token = auth_token_input or auth_token
122
+ else:
123
+ auth_token = None
124
+
125
+ api_base_default = existing_profile.api_base if existing_profile else ""
126
+ api_base = (
127
+ console.input(
128
+ f"API base (optional){f' [{api_base_default}]' if api_base_default else ''}: "
129
+ ).strip()
130
+ or api_base_default
131
+ or None
132
+ )
133
+
134
+ max_tokens_default = existing_profile.max_tokens if existing_profile else 4096
135
+ max_tokens = (
136
+ parse_int(
137
+ f"Max output tokens [{max_tokens_default}]: ",
138
+ max_tokens_default,
139
+ )
140
+ or max_tokens_default
141
+ )
142
+
143
+ temp_default = existing_profile.temperature if existing_profile else 0.7
144
+ temperature = parse_float(
145
+ f"Temperature [{temp_default}]: ",
146
+ temp_default,
147
+ )
148
+
149
+ context_window_default = existing_profile.context_window if existing_profile else None
150
+ context_prompt = "Context window tokens (optional"
151
+ if context_window_default:
152
+ context_prompt += f", current {context_window_default}"
153
+ context_prompt += "): "
154
+ context_window = parse_int(context_prompt, context_window_default)
155
+
156
+ default_set_main = (
157
+ not config.model_profiles
158
+ or getattr(config.model_pointers, "main", "") not in config.model_profiles
159
+ )
160
+ set_main_input = (
161
+ console.input(f"Set as main model? [{'Y' if default_set_main else 'y'}/N]: ")
162
+ .strip()
163
+ .lower()
164
+ )
165
+ set_as_main = set_main_input in ("y", "yes") if set_main_input else default_set_main
166
+
167
+ profile = ModelProfile(
168
+ provider=provider,
169
+ model=model_name,
170
+ api_key=api_key,
171
+ api_base=api_base,
172
+ max_tokens=max_tokens,
173
+ temperature=temperature,
174
+ context_window=context_window,
175
+ auth_token=auth_token,
176
+ )
177
+
178
+ try:
179
+ add_model_profile(
180
+ profile_name,
181
+ profile,
182
+ overwrite=overwrite,
183
+ set_as_main=set_as_main,
184
+ )
185
+ except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
186
+ console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
187
+ logger.warning(
188
+ "[models_cmd] Failed to save model profile: %s: %s",
189
+ type(exc).__name__, exc,
190
+ extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
191
+ )
192
+ return True
193
+
194
+ marker = " (main)" if set_as_main else ""
195
+ console.print(f"[green]✓ Model '{escape(profile_name)}' saved{marker}[/green]")
196
+ return True
197
+
198
+ if subcmd in ("edit", "update"):
199
+ profile_name = tokens[1] if len(tokens) > 1 else console.input("Profile to edit: ").strip()
200
+ existing_profile = config.model_profiles.get(profile_name or "")
201
+ if not profile_name or not existing_profile:
202
+ console.print("[red]Model profile not found.[/red]")
203
+ print_models_usage()
204
+ return True
205
+
206
+ provider_default = existing_profile.provider.value
207
+ provider_input = (
208
+ console.input(
209
+ f"Protocol ({', '.join(p.value for p in ProviderType)}) [{provider_default}]: "
210
+ )
211
+ .strip()
212
+ .lower()
213
+ or provider_default
214
+ )
215
+ try:
216
+ provider = ProviderType(provider_input)
217
+ except ValueError:
218
+ console.print(f"[red]Invalid provider: {escape(provider_input)}[/red]")
219
+ return True
220
+
221
+ model_name = (
222
+ console.input(f"Model name to send [{existing_profile.model}]: ").strip()
223
+ or existing_profile.model
224
+ )
225
+
226
+ api_key_label = "[set]" if existing_profile.api_key else "[not set]"
227
+ api_key_prompt = f"API key {api_key_label} (Enter=keep, '-'=clear)"
228
+ api_key_input = prompt_secret(api_key_prompt).strip()
229
+ if api_key_input == "-":
230
+ api_key = None
231
+ elif api_key_input:
232
+ api_key = api_key_input
233
+ else:
234
+ api_key = existing_profile.api_key
235
+
236
+ auth_token = existing_profile.auth_token
237
+ if (
238
+ provider == ProviderType.ANTHROPIC
239
+ or existing_profile.provider == ProviderType.ANTHROPIC
240
+ ):
241
+ auth_label = "[set]" if auth_token else "[not set]"
242
+ auth_prompt = f"Auth token (Anthropic only) {auth_label} (Enter=keep, '-'=clear)"
243
+ auth_token_input = prompt_secret(auth_prompt).strip()
244
+ if auth_token_input == "-":
245
+ auth_token = None
246
+ elif auth_token_input:
247
+ auth_token = auth_token_input
248
+ else:
249
+ auth_token = None
250
+
251
+ api_base = (
252
+ console.input(f"API base (optional) [{existing_profile.api_base or ''}]: ").strip()
253
+ or existing_profile.api_base
254
+ )
255
+ if api_base == "":
256
+ api_base = None
257
+
258
+ max_tokens = (
259
+ parse_int(
260
+ f"Max output tokens [{existing_profile.max_tokens}]: ",
261
+ existing_profile.max_tokens,
262
+ )
263
+ or existing_profile.max_tokens
264
+ )
265
+
266
+ temperature = parse_float(
267
+ f"Temperature [{existing_profile.temperature}]: ",
268
+ existing_profile.temperature,
269
+ )
270
+
271
+ context_window = parse_int(
272
+ f"Context window tokens [{existing_profile.context_window or 'unset'}]: ",
273
+ existing_profile.context_window,
274
+ )
275
+
276
+ updated_profile = ModelProfile(
277
+ provider=provider,
278
+ model=model_name,
279
+ api_key=api_key,
280
+ api_base=api_base,
281
+ max_tokens=max_tokens,
282
+ temperature=temperature,
283
+ context_window=context_window,
284
+ auth_token=auth_token,
285
+ )
286
+
287
+ try:
288
+ add_model_profile(
289
+ profile_name,
290
+ updated_profile,
291
+ overwrite=True,
292
+ set_as_main=False,
293
+ )
294
+ except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
295
+ console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
296
+ logger.warning(
297
+ "[models_cmd] Failed to update model profile: %s: %s",
298
+ type(exc).__name__, exc,
299
+ extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
300
+ )
301
+ return True
302
+
303
+ console.print(f"[green]✓ Model '{escape(profile_name)}' updated[/green]")
304
+ return True
305
+
306
+ if subcmd in ("delete", "del", "remove"):
307
+ target = tokens[1] if len(tokens) > 1 else console.input("Model to delete: ").strip()
308
+ if not target:
309
+ console.print("[red]Model name is required.[/red]")
310
+ print_models_usage()
311
+ return True
312
+ try:
313
+ delete_model_profile(target)
314
+ console.print(f"[green]✓ Deleted model '{escape(target)}'[/green]")
315
+ except KeyError as exc:
316
+ console.print(f"[yellow]{escape(str(exc))}[/yellow]")
317
+ except (OSError, IOError, PermissionError) as exc:
318
+ console.print(f"[red]Failed to delete model: {escape(str(exc))}[/red]")
319
+ print_models_usage()
320
+ logger.warning(
321
+ "[models_cmd] Failed to delete model profile: %s: %s",
322
+ type(exc).__name__, exc,
323
+ extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
324
+ )
325
+ return True
326
+
327
+ if subcmd in ("use", "main", "set-main"):
328
+ # Support both "/models use <profile>" and "/models use <pointer> <profile>"
329
+ valid_pointers = {"main", "task", "reasoning", "quick"}
330
+
331
+ if len(tokens) >= 3:
332
+ # /models use <pointer> <profile>
333
+ pointer = tokens[1].lower()
334
+ target = tokens[2]
335
+ if pointer not in valid_pointers:
336
+ console.print(f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]")
337
+ print_models_usage()
338
+ return True
339
+ elif len(tokens) >= 2:
340
+ # Check if second token is a pointer or a profile
341
+ if tokens[1].lower() in valid_pointers:
342
+ pointer = tokens[1].lower()
343
+ target = console.input(f"Model to use for '{pointer}': ").strip()
344
+ else:
345
+ # /models use <profile> (defaults to main)
346
+ pointer = "main"
347
+ target = tokens[1]
348
+ else:
349
+ pointer = console.input("Pointer (main/task/reasoning/quick) [main]: ").strip().lower() or "main"
350
+ if pointer not in valid_pointers:
351
+ console.print(f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]")
352
+ return True
353
+ target = console.input(f"Model to use for '{pointer}': ").strip()
354
+
355
+ if not target:
356
+ console.print("[red]Model name is required.[/red]")
357
+ print_models_usage()
358
+ return True
359
+ try:
360
+ set_model_pointer(pointer, target)
361
+ console.print(f"[green]✓ Pointer '{escape(pointer)}' set to '{escape(target)}'[/green]")
362
+ except (ValueError, KeyError, OSError, IOError, PermissionError) as exc:
363
+ console.print(f"[red]{escape(str(exc))}[/red]")
364
+ print_models_usage()
365
+ logger.warning(
366
+ "[models_cmd] Failed to set model pointer: %s: %s",
367
+ type(exc).__name__, exc,
368
+ extra={"pointer": pointer, "profile": target, "session_id": getattr(ui, "session_id", None)},
369
+ )
370
+ return True
371
+
372
+ print_models_usage()
373
+ pointer_map = config.model_pointers.model_dump()
374
+ if not config.model_profiles:
375
+ console.print(" • No models configured")
376
+ return True
377
+
378
+ console.print("\n[bold]Configured Models:[/bold]")
379
+ for name, profile in config.model_profiles.items():
380
+ markers = [ptr for ptr, value in pointer_map.items() if value == name]
381
+ marker_text = f" ({', '.join(markers)})" if markers else ""
382
+ console.print(f" • {escape(name)}{marker_text}", markup=False)
383
+ console.print(f" protocol: {profile.provider.value}", markup=False)
384
+ console.print(f" model: {profile.model}", markup=False)
385
+ if profile.api_base:
386
+ console.print(f" api_base: {profile.api_base}", markup=False)
387
+ if profile.context_window:
388
+ console.print(f" context: {profile.context_window} tokens", markup=False)
389
+ console.print(
390
+ f" max_tokens: {profile.max_tokens}, temperature: {profile.temperature}",
391
+ markup=False,
392
+ )
393
+ console.print(f" api_key: {'***' if profile.api_key else 'Not set'}", markup=False)
394
+ if profile.provider == ProviderType.ANTHROPIC:
395
+ console.print(
396
+ f" auth_token: {'***' if getattr(profile, 'auth_token', None) else 'Not set'}",
397
+ markup=False,
398
+ )
399
+ if profile.openai_tool_mode:
400
+ console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
401
+ pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
402
+ console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
403
+ return True
404
+
405
+
406
+ command = SlashCommand(
407
+ name="models",
408
+ description="Manage models: list/create/delete/use",
409
+ handler=_handle,
410
+ )
411
+
412
+
413
+ __all__ = ["command"]