ripperdoc 0.2.8__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 (94) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +257 -123
  3. ripperdoc/cli/commands/__init__.py +2 -1
  4. ripperdoc/cli/commands/agents_cmd.py +138 -8
  5. ripperdoc/cli/commands/clear_cmd.py +9 -4
  6. ripperdoc/cli/commands/config_cmd.py +1 -1
  7. ripperdoc/cli/commands/context_cmd.py +3 -2
  8. ripperdoc/cli/commands/doctor_cmd.py +18 -4
  9. ripperdoc/cli/commands/exit_cmd.py +1 -0
  10. ripperdoc/cli/commands/hooks_cmd.py +27 -53
  11. ripperdoc/cli/commands/models_cmd.py +27 -10
  12. ripperdoc/cli/commands/permissions_cmd.py +27 -9
  13. ripperdoc/cli/commands/resume_cmd.py +9 -3
  14. ripperdoc/cli/commands/stats_cmd.py +244 -0
  15. ripperdoc/cli/commands/status_cmd.py +4 -4
  16. ripperdoc/cli/commands/tasks_cmd.py +8 -4
  17. ripperdoc/cli/ui/file_mention_completer.py +2 -1
  18. ripperdoc/cli/ui/interrupt_handler.py +2 -3
  19. ripperdoc/cli/ui/message_display.py +4 -2
  20. ripperdoc/cli/ui/panels.py +1 -0
  21. ripperdoc/cli/ui/provider_options.py +247 -0
  22. ripperdoc/cli/ui/rich_ui.py +403 -81
  23. ripperdoc/cli/ui/spinner.py +54 -18
  24. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  25. ripperdoc/cli/ui/tool_renderers.py +8 -2
  26. ripperdoc/cli/ui/wizard.py +213 -0
  27. ripperdoc/core/agents.py +19 -6
  28. ripperdoc/core/config.py +51 -17
  29. ripperdoc/core/custom_commands.py +7 -6
  30. ripperdoc/core/default_tools.py +101 -12
  31. ripperdoc/core/hooks/config.py +1 -3
  32. ripperdoc/core/hooks/events.py +27 -28
  33. ripperdoc/core/hooks/executor.py +4 -6
  34. ripperdoc/core/hooks/integration.py +12 -21
  35. ripperdoc/core/hooks/llm_callback.py +59 -0
  36. ripperdoc/core/hooks/manager.py +40 -15
  37. ripperdoc/core/permissions.py +118 -12
  38. ripperdoc/core/providers/anthropic.py +109 -36
  39. ripperdoc/core/providers/gemini.py +70 -5
  40. ripperdoc/core/providers/openai.py +89 -24
  41. ripperdoc/core/query.py +273 -68
  42. ripperdoc/core/query_utils.py +2 -0
  43. ripperdoc/core/skills.py +9 -3
  44. ripperdoc/core/system_prompt.py +4 -2
  45. ripperdoc/core/tool.py +17 -8
  46. ripperdoc/sdk/client.py +79 -4
  47. ripperdoc/tools/ask_user_question_tool.py +5 -3
  48. ripperdoc/tools/background_shell.py +307 -135
  49. ripperdoc/tools/bash_output_tool.py +1 -1
  50. ripperdoc/tools/bash_tool.py +63 -24
  51. ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  52. ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  53. ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  54. ripperdoc/tools/file_edit_tool.py +167 -54
  55. ripperdoc/tools/file_read_tool.py +28 -4
  56. ripperdoc/tools/file_write_tool.py +13 -10
  57. ripperdoc/tools/glob_tool.py +3 -2
  58. ripperdoc/tools/grep_tool.py +3 -2
  59. ripperdoc/tools/kill_bash_tool.py +1 -1
  60. ripperdoc/tools/ls_tool.py +1 -1
  61. ripperdoc/tools/lsp_tool.py +615 -0
  62. ripperdoc/tools/mcp_tools.py +13 -10
  63. ripperdoc/tools/multi_edit_tool.py +8 -7
  64. ripperdoc/tools/notebook_edit_tool.py +7 -4
  65. ripperdoc/tools/skill_tool.py +1 -1
  66. ripperdoc/tools/task_tool.py +519 -69
  67. ripperdoc/tools/todo_tool.py +2 -2
  68. ripperdoc/tools/tool_search_tool.py +3 -2
  69. ripperdoc/utils/conversation_compaction.py +9 -5
  70. ripperdoc/utils/file_watch.py +214 -5
  71. ripperdoc/utils/json_utils.py +2 -1
  72. ripperdoc/utils/lsp.py +806 -0
  73. ripperdoc/utils/mcp.py +11 -3
  74. ripperdoc/utils/memory.py +4 -2
  75. ripperdoc/utils/message_compaction.py +21 -7
  76. ripperdoc/utils/message_formatting.py +14 -7
  77. ripperdoc/utils/messages.py +126 -67
  78. ripperdoc/utils/path_ignore.py +35 -8
  79. ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  80. ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  81. ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
  82. ripperdoc/utils/safe_get_cwd.py +2 -1
  83. ripperdoc/utils/session_heatmap.py +244 -0
  84. ripperdoc/utils/session_history.py +13 -6
  85. ripperdoc/utils/session_stats.py +293 -0
  86. ripperdoc/utils/todo.py +2 -1
  87. ripperdoc/utils/token_estimation.py +6 -1
  88. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
  89. ripperdoc-0.2.10.dist-info/RECORD +129 -0
  90. ripperdoc-0.2.8.dist-info/RECORD +0 -121
  91. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
  92. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
  93. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
  94. {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.2.8"
3
+ __version__ = "0.2.10"
ripperdoc/cli/cli.py CHANGED
@@ -6,6 +6,7 @@ This module provides the command-line interface for the Ripperdoc agent.
6
6
  import asyncio
7
7
  import click
8
8
  import sys
9
+ import time
9
10
  import uuid
10
11
  from pathlib import Path
11
12
  from typing import Any, Dict, List, Optional
@@ -13,27 +14,33 @@ from typing import Any, Dict, List, Optional
13
14
  from ripperdoc import __version__
14
15
  from ripperdoc.core.config import (
15
16
  get_global_config,
16
- save_global_config,
17
17
  get_project_config,
18
- ModelProfile,
19
- ProviderType,
20
18
  )
21
- from ripperdoc.core.default_tools import get_default_tools
19
+ from ripperdoc.cli.ui.wizard import check_onboarding
20
+ from ripperdoc.core.default_tools import get_default_tools, BUILTIN_TOOL_NAMES
22
21
  from ripperdoc.core.query import query, QueryContext
23
22
  from ripperdoc.core.system_prompt import build_system_prompt
24
23
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
25
24
  from ripperdoc.core.hooks.manager import hook_manager
25
+ from ripperdoc.core.hooks.llm_callback import build_hook_llm_callback
26
26
  from ripperdoc.utils.messages import create_user_message
27
27
  from ripperdoc.utils.memory import build_memory_instructions
28
28
  from ripperdoc.core.permissions import make_permission_checker
29
+ from ripperdoc.utils.session_history import (
30
+ SessionHistory,
31
+ list_session_summaries,
32
+ load_session_messages,
33
+ )
29
34
  from ripperdoc.utils.mcp import (
30
35
  load_mcp_servers_async,
31
36
  format_mcp_instructions,
32
37
  shutdown_mcp_runtime,
33
38
  )
39
+ from ripperdoc.utils.lsp import shutdown_lsp_manager
40
+ from ripperdoc.tools.background_shell import shutdown_background_shell
34
41
  from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
35
42
  from ripperdoc.utils.log import enable_session_file_logging, get_logger
36
- from ripperdoc.utils.prompt import prompt_secret
43
+
37
44
 
38
45
  from rich.console import Console
39
46
  from rich.markdown import Markdown
@@ -44,22 +51,64 @@ console = Console()
44
51
  logger = get_logger()
45
52
 
46
53
 
54
+ def parse_tools_option(tools_arg: Optional[str]) -> Optional[List[str]]:
55
+ """Parse the --tools argument.
56
+
57
+ Args:
58
+ tools_arg: The raw tools argument from CLI.
59
+
60
+ Returns:
61
+ None for default (all tools), empty list for "" (no tools),
62
+ or a list of tool names for filtering.
63
+ """
64
+ if tools_arg is None:
65
+ return None # Use all default tools
66
+
67
+ tools_arg = tools_arg.strip()
68
+
69
+ if tools_arg == "":
70
+ return [] # Disable all tools
71
+
72
+ if tools_arg.lower() == "default":
73
+ return None # Use all default tools
74
+
75
+ # Parse comma-separated list
76
+ tool_names = [name.strip() for name in tools_arg.split(",") if name.strip()]
77
+
78
+ # Validate tool names
79
+ invalid_tools = [name for name in tool_names if name not in BUILTIN_TOOL_NAMES]
80
+ if invalid_tools:
81
+ logger.warning(
82
+ "[cli] Unknown tools specified: %s. Available tools: %s",
83
+ ", ".join(invalid_tools),
84
+ ", ".join(BUILTIN_TOOL_NAMES),
85
+ )
86
+
87
+ return tool_names if tool_names else None
88
+
89
+
47
90
  async def run_query(
48
91
  prompt: str,
49
92
  tools: list,
50
- safe_mode: bool = False,
93
+ yolo_mode: bool = False,
51
94
  verbose: bool = False,
52
95
  session_id: Optional[str] = None,
96
+ custom_system_prompt: Optional[str] = None,
97
+ append_system_prompt: Optional[str] = None,
98
+ model: Optional[str] = None,
53
99
  ) -> None:
54
100
  """Run a single query and print the response."""
55
101
 
56
102
  logger.info(
57
103
  "[cli] Running single prompt session",
58
104
  extra={
59
- "safe_mode": safe_mode,
105
+ "yolo_mode": yolo_mode,
60
106
  "verbose": verbose,
61
107
  "session_id": session_id,
62
108
  "prompt_length": len(prompt),
109
+ "model": model,
110
+ "has_custom_system_prompt": custom_system_prompt is not None,
111
+ "has_append_system_prompt": append_system_prompt is not None,
63
112
  },
64
113
  )
65
114
  if prompt:
@@ -69,20 +118,37 @@ async def run_query(
69
118
  )
70
119
 
71
120
  project_path = Path.cwd()
72
- can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
121
+ can_use_tool = None if yolo_mode else make_permission_checker(project_path, yolo_mode=False)
73
122
 
74
123
  # Initialize hook manager
75
124
  hook_manager.set_project_dir(project_path)
76
125
  hook_manager.set_session_id(session_id)
126
+ hook_manager.set_llm_callback(build_hook_llm_callback())
127
+ session_history = SessionHistory(project_path, session_id or str(uuid.uuid4()))
128
+ hook_manager.set_transcript_path(str(session_history.path))
129
+
130
+ def _collect_hook_contexts(result: Any) -> List[str]:
131
+ contexts: List[str] = []
132
+ system_message = getattr(result, "system_message", None)
133
+ additional_context = getattr(result, "additional_context", None)
134
+ if system_message:
135
+ contexts.append(str(system_message))
136
+ if additional_context:
137
+ contexts.append(str(additional_context))
138
+ return contexts
77
139
 
78
140
  # Create initial user message
79
141
  from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
80
142
 
81
143
  messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
144
+ session_history.append(messages[0])
82
145
 
83
146
  # Create query context
84
- query_context = QueryContext(tools=tools, safe_mode=safe_mode, verbose=verbose)
147
+ query_context = QueryContext(
148
+ tools=tools, yolo_mode=yolo_mode, verbose=verbose, model=model or "main"
149
+ )
85
150
 
151
+ session_start_time = time.time()
86
152
  try:
87
153
  context: Dict[str, Any] = {}
88
154
  # System prompt
@@ -105,13 +171,46 @@ async def run_query(
105
171
  memory_instructions = build_memory_instructions()
106
172
  if memory_instructions:
107
173
  additional_instructions.append(memory_instructions)
108
- system_prompt = build_system_prompt(
109
- tools,
110
- prompt,
111
- context,
112
- additional_instructions=additional_instructions or None,
113
- mcp_instructions=mcp_instructions,
114
- )
174
+
175
+ session_start_result = await hook_manager.run_session_start_async("startup")
176
+ session_hook_contexts = _collect_hook_contexts(session_start_result)
177
+ if session_hook_contexts:
178
+ additional_instructions.extend(session_hook_contexts)
179
+
180
+ prompt_hook_result = await hook_manager.run_user_prompt_submit_async(prompt)
181
+ if prompt_hook_result.should_block or not prompt_hook_result.should_continue:
182
+ reason = (
183
+ prompt_hook_result.block_reason
184
+ or prompt_hook_result.stop_reason
185
+ or "Prompt blocked by hook."
186
+ )
187
+ console.print(f"[red]{escape(str(reason))}[/red]")
188
+ return
189
+ prompt_hook_contexts = _collect_hook_contexts(prompt_hook_result)
190
+ if prompt_hook_contexts:
191
+ additional_instructions.extend(prompt_hook_contexts)
192
+
193
+ # Build system prompt based on options:
194
+ # - custom_system_prompt: replaces the default entirely
195
+ # - append_system_prompt: appends to the default system prompt
196
+ if custom_system_prompt:
197
+ # Complete replacement
198
+ system_prompt = custom_system_prompt
199
+ # Still append if both are provided
200
+ if append_system_prompt:
201
+ system_prompt = f"{system_prompt}\n\n{append_system_prompt}"
202
+ else:
203
+ # Build default with optional append
204
+ all_instructions = list(additional_instructions) if additional_instructions else []
205
+ if append_system_prompt:
206
+ all_instructions.append(append_system_prompt)
207
+ system_prompt = build_system_prompt(
208
+ tools,
209
+ prompt,
210
+ context,
211
+ additional_instructions=all_instructions or None,
212
+ mcp_instructions=mcp_instructions,
213
+ )
115
214
 
116
215
  # Run the query
117
216
  try:
@@ -160,6 +259,7 @@ async def run_query(
160
259
 
161
260
  # Add message to history
162
261
  messages.append(message) # type: ignore[arg-type]
262
+ session_history.append(message) # type: ignore[arg-type]
163
263
 
164
264
  except KeyboardInterrupt:
165
265
  console.print("\n[yellow]Interrupted by user[/yellow]")
@@ -169,7 +269,8 @@ async def run_query(
169
269
  console.print(f"[red]Error: {escape(str(e))}[/red]")
170
270
  logger.warning(
171
271
  "[cli] Unhandled error while running prompt: %s: %s",
172
- type(e).__name__, e,
272
+ type(e).__name__,
273
+ e,
173
274
  extra={"session_id": session_id},
174
275
  )
175
276
  if verbose:
@@ -181,107 +282,31 @@ async def run_query(
181
282
  extra={"session_id": session_id, "message_count": len(messages)},
182
283
  )
183
284
  finally:
184
- await shutdown_mcp_runtime()
185
- logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
186
-
187
-
188
- def check_onboarding() -> bool:
189
- """Check if onboarding is complete and run if needed."""
190
- config = get_global_config()
191
-
192
- if config.has_completed_onboarding:
193
- return True
194
-
195
- console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
196
- console.print("Let's set up your AI model configuration.\n")
197
-
198
- # Simple onboarding
199
- provider_choices = [
200
- *[p.value for p in ProviderType],
201
- "openai",
202
- "deepseek",
203
- "mistral",
204
- "kimi",
205
- "qwen",
206
- "glm",
207
- "custom",
208
- ]
209
- provider_choice = click.prompt(
210
- "Choose your model protocol",
211
- type=click.Choice(provider_choices),
212
- default=ProviderType.ANTHROPIC.value,
213
- )
214
-
215
- api_base = None
216
- if provider_choice == "custom":
217
- provider_choice = click.prompt(
218
- "Protocol family (for API compatibility)",
219
- type=click.Choice([p.value for p in ProviderType]),
220
- default=ProviderType.OPENAI_COMPATIBLE.value,
221
- )
222
- api_base = click.prompt("API Base URL")
223
-
224
- api_key = ""
225
- while not api_key:
226
- api_key = prompt_secret("Enter your API key").strip()
227
- if not api_key:
228
- console.print("[red]API key is required.[/red]")
229
-
230
- provider = ProviderType(provider_choice)
231
-
232
- # Get model name
233
- if provider == ProviderType.ANTHROPIC:
234
- model = click.prompt("Model name", default="claude-3-5-sonnet-20241022")
235
- elif provider == ProviderType.OPENAI_COMPATIBLE:
236
- default_model = "gpt-4o-mini"
237
- if provider_choice == "deepseek":
238
- default_model = "deepseek-chat"
239
- api_base = api_base or "https://api.deepseek.com"
240
- model = click.prompt("Model name", default=default_model)
241
- if api_base is None:
242
- api_base = (
243
- click.prompt("API base URL (optional)", default="", show_default=False) or None
285
+ duration = max(time.time() - session_start_time, 0.0)
286
+ try:
287
+ await hook_manager.run_session_end_async(
288
+ "other", duration_seconds=duration, message_count=len(messages)
244
289
  )
245
- elif provider == ProviderType.GEMINI:
246
- console.print(
247
- "[yellow]Gemini protocol support is not yet available; configuration is saved for "
248
- "future support.[/yellow]"
249
- )
250
- model = click.prompt("Model name", default="gemini-1.5-pro")
251
- if api_base is None:
252
- api_base = (
253
- click.prompt("API base URL (optional)", default="", show_default=False) or None
290
+ except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
291
+ logger.warning(
292
+ "[cli] SessionEnd hook failed: %s: %s",
293
+ type(exc).__name__,
294
+ exc,
295
+ extra={"session_id": session_id},
254
296
  )
255
- else:
256
- model = click.prompt("Model name")
257
-
258
- context_window_input = click.prompt(
259
- "Context window in tokens (optional, press Enter to skip)", default="", show_default=False
260
- )
261
- context_window = None
262
- if context_window_input.strip():
297
+ await shutdown_mcp_runtime()
298
+ await shutdown_lsp_manager()
299
+ # Shutdown background shell manager
263
300
  try:
264
- context_window = int(context_window_input.strip())
265
- except ValueError:
266
- console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
267
-
268
- # Create model profile
269
- config.model_profiles["default"] = ModelProfile(
270
- provider=provider,
271
- model=model,
272
- api_key=api_key,
273
- api_base=api_base,
274
- context_window=context_window,
275
- )
276
-
277
- config.has_completed_onboarding = True
278
- config.last_onboarding_version = __version__
279
-
280
- save_global_config(config)
281
-
282
- console.print("\n[green]✓ Configuration saved![/green]\n")
301
+ shutdown_background_shell(force=True)
302
+ except (OSError, RuntimeError) as exc:
303
+ logger.debug(
304
+ "[cli] Failed to shut down background shell: %s: %s",
305
+ type(exc).__name__,
306
+ exc,
307
+ )
308
+ logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
283
309
 
284
- return True
285
310
 
286
311
 
287
312
  @click.group(invoke_without_command=True)
@@ -293,10 +318,59 @@ def check_onboarding() -> bool:
293
318
  help="YOLO mode: skip all permission prompts for tools",
294
319
  )
295
320
  @click.option("--verbose", is_flag=True, help="Verbose output")
321
+ @click.option(
322
+ "--show-full-thinking/--hide-full-thinking",
323
+ default=None,
324
+ help="Show full reasoning content instead of truncated preview",
325
+ )
296
326
  @click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
327
+ @click.option(
328
+ "--tools",
329
+ type=str,
330
+ default=None,
331
+ help=(
332
+ 'Specify the list of available tools. Use "" to disable all tools, '
333
+ '"default" to use all tools, or specify tool names (e.g. "Bash,Edit,Read").'
334
+ ),
335
+ )
336
+ @click.option(
337
+ "--system-prompt",
338
+ type=str,
339
+ default=None,
340
+ help="System prompt to use for the session (replaces default).",
341
+ )
342
+ @click.option(
343
+ "--append-system-prompt",
344
+ type=str,
345
+ default=None,
346
+ help="Additional instructions to append to the system prompt.",
347
+ )
348
+ @click.option(
349
+ "--model",
350
+ type=str,
351
+ default=None,
352
+ help="Model profile for the current session.",
353
+ )
354
+ @click.option(
355
+ "-c",
356
+ "--continue",
357
+ "continue_session",
358
+ is_flag=True,
359
+ help="Continue the most recent conversation in the current directory.",
360
+ )
297
361
  @click.pass_context
298
362
  def cli(
299
- ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, prompt: Optional[str]
363
+ ctx: click.Context,
364
+ cwd: Optional[str],
365
+ yolo: bool,
366
+ verbose: bool,
367
+ show_full_thinking: Optional[bool],
368
+ prompt: Optional[str],
369
+ tools: Optional[str],
370
+ system_prompt: Optional[str],
371
+ append_system_prompt: Optional[str],
372
+ model: Optional[str],
373
+ continue_session: bool,
300
374
  ) -> None:
301
375
  """Ripperdoc - AI-powered coding agent"""
302
376
  session_id = str(uuid.uuid4())
@@ -334,16 +408,60 @@ def cli(
334
408
  # Initialize project configuration for the current working directory
335
409
  get_project_config(project_path)
336
410
 
337
- safe_mode = not yolo
411
+ yolo_mode = yolo
412
+ # Parse --tools option
413
+ allowed_tools = parse_tools_option(tools)
414
+
415
+ # Handle --continue option: load the most recent session
416
+ resume_messages = None
417
+ if continue_session:
418
+ summaries = list_session_summaries(project_path)
419
+ if summaries:
420
+ most_recent = summaries[0]
421
+ session_id = most_recent.session_id
422
+ resume_messages = load_session_messages(project_path, session_id)
423
+ logger.info(
424
+ "[cli] Continuing session",
425
+ extra={
426
+ "session_id": session_id,
427
+ "message_count": len(resume_messages),
428
+ "last_prompt": most_recent.last_prompt,
429
+ },
430
+ )
431
+ console.print(f"[dim]Continuing session: {most_recent.last_prompt}[/dim]")
432
+ else:
433
+ logger.warning("[cli] No previous sessions found to continue")
434
+ console.print("[yellow]No previous sessions found in this directory.[/yellow]")
435
+
338
436
  logger.debug(
339
437
  "[cli] Configuration initialized",
340
- extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
438
+ extra={
439
+ "session_id": session_id,
440
+ "yolo_mode": yolo_mode,
441
+ "verbose": verbose,
442
+ "allowed_tools": allowed_tools,
443
+ "model": model,
444
+ "has_system_prompt": system_prompt is not None,
445
+ "has_append_system_prompt": append_system_prompt is not None,
446
+ "continue_session": continue_session,
447
+ },
341
448
  )
342
449
 
343
450
  # If prompt is provided, run directly
344
451
  if prompt:
345
- tools = get_default_tools()
346
- asyncio.run(run_query(prompt, tools, safe_mode, verbose, session_id=session_id))
452
+ tool_list = get_default_tools(allowed_tools=allowed_tools)
453
+ asyncio.run(
454
+ run_query(
455
+ prompt,
456
+ tool_list,
457
+ yolo_mode,
458
+ verbose,
459
+ session_id=session_id,
460
+ custom_system_prompt=system_prompt,
461
+ append_system_prompt=append_system_prompt,
462
+ model=model,
463
+ )
464
+ )
347
465
  return
348
466
 
349
467
  # If no command specified, start interactive REPL with Rich interface
@@ -352,10 +470,16 @@ def cli(
352
470
  from ripperdoc.cli.ui.rich_ui import main_rich
353
471
 
354
472
  main_rich(
355
- safe_mode=safe_mode,
473
+ yolo_mode=yolo_mode,
356
474
  verbose=verbose,
475
+ show_full_thinking=show_full_thinking,
357
476
  session_id=session_id,
358
477
  log_file_path=log_file,
478
+ allowed_tools=allowed_tools,
479
+ custom_system_prompt=system_prompt,
480
+ append_system_prompt=append_system_prompt,
481
+ model=model,
482
+ resume_messages=resume_messages,
359
483
  )
360
484
  return
361
485
 
@@ -370,7 +494,8 @@ def config_cmd() -> None:
370
494
  console.print(f"Onboarding Complete: {config.has_completed_onboarding}")
371
495
  console.print(f"Theme: {config.theme}")
372
496
  console.print(f"Verbose: {config.verbose}")
373
- console.print(f"Safe Mode: {config.safe_mode}\n")
497
+ console.print(f"Yolo Mode: {config.yolo_mode}")
498
+ console.print(f"Show Full Thinking: {config.show_full_thinking}\n")
374
499
 
375
500
  if config.model_profiles:
376
501
  console.print("[bold]Model Profiles:[/bold]")
@@ -397,11 +522,20 @@ def main() -> None:
397
522
  sys.exit(130)
398
523
  except SystemExit:
399
524
  raise
400
- except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError, click.ClickException) as e:
525
+ except (
526
+ RuntimeError,
527
+ ValueError,
528
+ TypeError,
529
+ OSError,
530
+ IOError,
531
+ ConnectionError,
532
+ click.ClickException,
533
+ ) as e:
401
534
  console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
402
535
  logger.warning(
403
536
  "[cli] Fatal error in main CLI entrypoint: %s: %s",
404
- type(e).__name__, e,
537
+ type(e).__name__,
538
+ e,
405
539
  )
406
540
  sys.exit(1)
407
541
 
@@ -21,6 +21,7 @@ from .mcp_cmd import command as mcp_command
21
21
  from .models_cmd import command as models_command
22
22
  from .permissions_cmd import command as permissions_command
23
23
  from .resume_cmd import command as resume_command
24
+ from .stats_cmd import command as stats_command
24
25
  from .tasks_cmd import command as tasks_command
25
26
  from .status_cmd import command as status_command
26
27
  from .todos_cmd import command as todos_command
@@ -29,7 +30,6 @@ from .tools_cmd import command as tools_command
29
30
  from ripperdoc.core.custom_commands import (
30
31
  CustomCommandDefinition,
31
32
  load_all_custom_commands,
32
- find_custom_command,
33
33
  expand_command_content,
34
34
  )
35
35
 
@@ -52,6 +52,7 @@ ALL_COMMANDS: List[SlashCommand] = [
52
52
  models_command,
53
53
  exit_command,
54
54
  status_command,
55
+ stats_command,
55
56
  doctor_command,
56
57
  memory_command,
57
58
  permissions_command,