ripperdoc 0.2.7__tar.gz → 0.2.9__tar.gz

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 (153) hide show
  1. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/PKG-INFO +24 -3
  2. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/README.md +23 -2
  3. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/__init__.py +1 -1
  4. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/cli.py +33 -115
  5. ripperdoc-0.2.9/ripperdoc/cli/commands/__init__.py +146 -0
  6. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/agents_cmd.py +6 -3
  7. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/clear_cmd.py +1 -4
  8. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/config_cmd.py +1 -1
  9. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/context_cmd.py +3 -2
  10. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/doctor_cmd.py +18 -4
  11. ripperdoc-0.2.9/ripperdoc/cli/commands/help_cmd.py +30 -0
  12. ripperdoc-0.2.9/ripperdoc/cli/commands/hooks_cmd.py +610 -0
  13. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/models_cmd.py +26 -9
  14. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/permissions_cmd.py +57 -37
  15. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/resume_cmd.py +6 -4
  16. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/status_cmd.py +4 -4
  17. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/tasks_cmd.py +8 -4
  18. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/file_mention_completer.py +64 -8
  19. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/interrupt_handler.py +3 -4
  20. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/message_display.py +5 -3
  21. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/panels.py +13 -10
  22. ripperdoc-0.2.9/ripperdoc/cli/ui/provider_options.py +247 -0
  23. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/rich_ui.py +196 -77
  24. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/spinner.py +25 -1
  25. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/tool_renderers.py +8 -2
  26. ripperdoc-0.2.9/ripperdoc/cli/ui/wizard.py +215 -0
  27. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/agents.py +9 -3
  28. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/config.py +49 -12
  29. ripperdoc-0.2.9/ripperdoc/core/custom_commands.py +412 -0
  30. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/default_tools.py +11 -2
  31. ripperdoc-0.2.9/ripperdoc/core/hooks/__init__.py +99 -0
  32. ripperdoc-0.2.9/ripperdoc/core/hooks/config.py +301 -0
  33. ripperdoc-0.2.9/ripperdoc/core/hooks/events.py +535 -0
  34. ripperdoc-0.2.9/ripperdoc/core/hooks/executor.py +496 -0
  35. ripperdoc-0.2.9/ripperdoc/core/hooks/integration.py +344 -0
  36. ripperdoc-0.2.9/ripperdoc/core/hooks/manager.py +745 -0
  37. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/permissions.py +40 -8
  38. ripperdoc-0.2.9/ripperdoc/core/providers/anthropic.py +730 -0
  39. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/providers/gemini.py +70 -5
  40. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/providers/openai.py +60 -5
  41. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/query.py +140 -39
  42. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/query_utils.py +2 -0
  43. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/skills.py +9 -3
  44. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/system_prompt.py +4 -2
  45. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/tool.py +9 -5
  46. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/sdk/client.py +2 -2
  47. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/ask_user_question_tool.py +5 -3
  48. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/background_shell.py +2 -1
  49. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/bash_output_tool.py +1 -1
  50. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/bash_tool.py +30 -20
  51. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/dynamic_mcp_tool.py +29 -8
  52. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/enter_plan_mode_tool.py +1 -1
  53. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/exit_plan_mode_tool.py +1 -1
  54. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/file_edit_tool.py +8 -4
  55. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/file_read_tool.py +9 -5
  56. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/file_write_tool.py +9 -5
  57. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/glob_tool.py +3 -2
  58. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/grep_tool.py +3 -2
  59. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/kill_bash_tool.py +1 -1
  60. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/ls_tool.py +1 -1
  61. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/mcp_tools.py +13 -10
  62. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/multi_edit_tool.py +8 -7
  63. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/notebook_edit_tool.py +7 -4
  64. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/skill_tool.py +1 -1
  65. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/task_tool.py +5 -4
  66. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/todo_tool.py +2 -2
  67. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/tool_search_tool.py +3 -2
  68. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/conversation_compaction.py +11 -7
  69. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/file_watch.py +8 -2
  70. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/json_utils.py +2 -1
  71. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/mcp.py +11 -3
  72. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/memory.py +4 -2
  73. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/message_compaction.py +21 -7
  74. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/message_formatting.py +11 -7
  75. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/messages.py +105 -66
  76. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/path_ignore.py +38 -12
  77. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/permissions/path_validation_utils.py +2 -1
  78. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/permissions/shell_command_validation.py +427 -91
  79. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/safe_get_cwd.py +2 -1
  80. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/session_history.py +13 -6
  81. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/todo.py +2 -1
  82. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/token_estimation.py +6 -1
  83. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/PKG-INFO +24 -3
  84. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/SOURCES.txt +13 -0
  85. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_compact.py +2 -6
  86. ripperdoc-0.2.9/tests/test_custom_commands.py +260 -0
  87. ripperdoc-0.2.9/tests/test_file_mention_completer.py +72 -0
  88. ripperdoc-0.2.9/tests/test_hooks_cmd.py +139 -0
  89. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_path_ignore.py +3 -1
  90. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_permissions.py +12 -12
  91. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_query_abort.py +3 -3
  92. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_sdk.py +2 -2
  93. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_shell_permissions.py +18 -12
  94. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_tool_search.py +1 -1
  95. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_tools.py +1 -1
  96. ripperdoc-0.2.7/ripperdoc/cli/commands/__init__.py +0 -82
  97. ripperdoc-0.2.7/ripperdoc/cli/commands/help_cmd.py +0 -20
  98. ripperdoc-0.2.7/ripperdoc/core/providers/anthropic.py +0 -250
  99. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/LICENSE +0 -0
  100. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/pyproject.toml +0 -0
  101. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/__main__.py +0 -0
  102. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/__init__.py +0 -0
  103. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/base.py +0 -0
  104. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/compact_cmd.py +0 -0
  105. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/cost_cmd.py +0 -0
  106. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/exit_cmd.py +0 -0
  107. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/mcp_cmd.py +0 -0
  108. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/memory_cmd.py +0 -0
  109. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/todos_cmd.py +0 -0
  110. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/commands/tools_cmd.py +0 -0
  111. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/__init__.py +0 -0
  112. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/context_display.py +0 -0
  113. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/helpers.py +0 -0
  114. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/cli/ui/thinking_spinner.py +0 -0
  115. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/__init__.py +0 -0
  116. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/commands.py +0 -0
  117. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/providers/__init__.py +0 -0
  118. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/core/providers/base.py +0 -0
  119. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/sdk/__init__.py +0 -0
  120. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/tools/__init__.py +0 -0
  121. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/__init__.py +0 -0
  122. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/bash_constants.py +0 -0
  123. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/bash_output_utils.py +0 -0
  124. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/coerce.py +0 -0
  125. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/context_length_errors.py +0 -0
  126. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/exit_code_handlers.py +0 -0
  127. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/git_utils.py +0 -0
  128. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/log.py +0 -0
  129. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/output_utils.py +0 -0
  130. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/path_utils.py +0 -0
  131. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/permissions/__init__.py +0 -0
  132. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/permissions/tool_permission_utils.py +0 -0
  133. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/prompt.py +0 -0
  134. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/sandbox_utils.py +0 -0
  135. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/session_usage.py +0 -0
  136. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/shell_token_utils.py +0 -0
  137. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc/utils/shell_utils.py +0 -0
  138. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/dependency_links.txt +0 -0
  139. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/entry_points.txt +0 -0
  140. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/requires.txt +0 -0
  141. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/ripperdoc.egg-info/top_level.txt +0 -0
  142. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/setup.cfg +0 -0
  143. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/setup.py +0 -0
  144. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_background_shell_shutdown.py +0 -0
  145. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_cli_commands.py +0 -0
  146. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_config.py +0 -0
  147. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_context_length_errors.py +0 -0
  148. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_context_limits.py +0 -0
  149. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_mcp_config.py +0 -0
  150. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_messages.py +0 -0
  151. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_output_utils.py +0 -0
  152. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_skills.py +0 -0
  153. {ripperdoc-0.2.7 → ripperdoc-0.2.9}/tests/test_todo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripperdoc
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: AI-powered terminal assistant for coding tasks
5
5
  Author: Ripperdoc Team
6
6
  License: Apache-2.0
@@ -35,9 +35,28 @@ Requires-Dist: black>=23.0.0; extra == "dev"
35
35
  Requires-Dist: ruff>=0.1.0; extra == "dev"
36
36
  Dynamic: license-file
37
37
 
38
- # Ripperdoc - AI-Powered Terminal Assistant
38
+ <div align="center">
39
39
 
40
- Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an interactive interface for AI-assisted development, file management, and command execution.
40
+ # Ripperdoc
41
+
42
+ _an open-source, extensible AI coding agent that runs in your terminal_
43
+
44
+ <p align="center">
45
+ <a href="https://opensource.org/licenses/Apache-2.0">
46
+ <img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg">
47
+ </a>
48
+ <a href="https://www.python.org/downloads/">
49
+ <img src="https://img.shields.io/badge/python-3.10+-blue.svg">
50
+ </a>
51
+ <a href="https://github.com/quantmew/ripperdoc/stargazers">
52
+ <img src="https://img.shields.io/github/stars/quantmew/ripperdoc.svg" alt="GitHub stars">
53
+ </a>
54
+ </p>
55
+ </div>
56
+
57
+ Ripperdoc is your on-machine AI coding assistant, similar to [Claude Code](https://claude.com/claude-code), [Codex](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Aider](https://github.com/paul-gauthier/aider), and [Goose](https://github.com/block/goose). It can write code, refactor projects, execute shell commands, and manage files - all through natural language conversations in your terminal.
58
+
59
+ Designed for maximum flexibility, Ripperdoc works with **any LLM** (Anthropic Claude, OpenAI, DeepSeek, local models via OpenAI-compatible APIs), supports **custom hooks** to intercept and control tool execution, and offers both an interactive CLI and a **Python SDK** for headless automation.
41
60
 
42
61
  [中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
43
62
 
@@ -60,6 +79,8 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
60
79
  - **MCP Server Support** - Integration with Model Context Protocol servers
61
80
  - **Session Management** - Persistent session history and usage tracking
62
81
  - **Jupyter Notebook Support** - Edit .ipynb files directly
82
+ - **Hooks System** - Execute custom scripts at lifecycle events with decision control
83
+ - **Custom Commands** - Define reusable slash commands with parameter substitution
63
84
 
64
85
  ## Installation
65
86
 
@@ -1,6 +1,25 @@
1
- # Ripperdoc - AI-Powered Terminal Assistant
1
+ <div align="center">
2
2
 
3
- Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an interactive interface for AI-assisted development, file management, and command execution.
3
+ # Ripperdoc
4
+
5
+ _an open-source, extensible AI coding agent that runs in your terminal_
6
+
7
+ <p align="center">
8
+ <a href="https://opensource.org/licenses/Apache-2.0">
9
+ <img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg">
10
+ </a>
11
+ <a href="https://www.python.org/downloads/">
12
+ <img src="https://img.shields.io/badge/python-3.10+-blue.svg">
13
+ </a>
14
+ <a href="https://github.com/quantmew/ripperdoc/stargazers">
15
+ <img src="https://img.shields.io/github/stars/quantmew/ripperdoc.svg" alt="GitHub stars">
16
+ </a>
17
+ </p>
18
+ </div>
19
+
20
+ Ripperdoc is your on-machine AI coding assistant, similar to [Claude Code](https://claude.com/claude-code), [Codex](https://github.com/openai/codex), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Aider](https://github.com/paul-gauthier/aider), and [Goose](https://github.com/block/goose). It can write code, refactor projects, execute shell commands, and manage files - all through natural language conversations in your terminal.
21
+
22
+ Designed for maximum flexibility, Ripperdoc works with **any LLM** (Anthropic Claude, OpenAI, DeepSeek, local models via OpenAI-compatible APIs), supports **custom hooks** to intercept and control tool execution, and offers both an interactive CLI and a **Python SDK** for headless automation.
4
23
 
5
24
  [中文文档](README_CN.md) | [Contributing](CONTRIBUTING.md) | [Documentation](docs/)
6
25
 
@@ -23,6 +42,8 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
23
42
  - **MCP Server Support** - Integration with Model Context Protocol servers
24
43
  - **Session Management** - Persistent session history and usage tracking
25
44
  - **Jupyter Notebook Support** - Edit .ipynb files directly
45
+ - **Hooks System** - Execute custom scripts at lifecycle events with decision control
46
+ - **Custom Commands** - Define reusable slash commands with parameter substitution
26
47
 
27
48
  ## Installation
28
49
 
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.2.7"
3
+ __version__ = "0.2.9"
@@ -13,15 +13,14 @@ from typing import Any, Dict, List, Optional
13
13
  from ripperdoc import __version__
14
14
  from ripperdoc.core.config import (
15
15
  get_global_config,
16
- save_global_config,
17
16
  get_project_config,
18
- ModelProfile,
19
- ProviderType,
20
17
  )
18
+ from ripperdoc.cli.ui.wizard import check_onboarding
21
19
  from ripperdoc.core.default_tools import get_default_tools
22
20
  from ripperdoc.core.query import query, QueryContext
23
21
  from ripperdoc.core.system_prompt import build_system_prompt
24
22
  from ripperdoc.core.skills import build_skill_summary, load_all_skills
23
+ from ripperdoc.core.hooks.manager import hook_manager
25
24
  from ripperdoc.utils.messages import create_user_message
26
25
  from ripperdoc.utils.memory import build_memory_instructions
27
26
  from ripperdoc.core.permissions import make_permission_checker
@@ -32,7 +31,7 @@ from ripperdoc.utils.mcp import (
32
31
  )
33
32
  from ripperdoc.tools.mcp_tools import load_dynamic_mcp_tools_async, merge_tools_with_dynamic
34
33
  from ripperdoc.utils.log import enable_session_file_logging, get_logger
35
- from ripperdoc.utils.prompt import prompt_secret
34
+
36
35
 
37
36
  from rich.console import Console
38
37
  from rich.markdown import Markdown
@@ -46,7 +45,7 @@ logger = get_logger()
46
45
  async def run_query(
47
46
  prompt: str,
48
47
  tools: list,
49
- safe_mode: bool = False,
48
+ yolo_mode: bool = False,
50
49
  verbose: bool = False,
51
50
  session_id: Optional[str] = None,
52
51
  ) -> None:
@@ -55,7 +54,7 @@ async def run_query(
55
54
  logger.info(
56
55
  "[cli] Running single prompt session",
57
56
  extra={
58
- "safe_mode": safe_mode,
57
+ "yolo_mode": yolo_mode,
59
58
  "verbose": verbose,
60
59
  "session_id": session_id,
61
60
  "prompt_length": len(prompt),
@@ -68,7 +67,11 @@ async def run_query(
68
67
  )
69
68
 
70
69
  project_path = Path.cwd()
71
- can_use_tool = make_permission_checker(project_path, safe_mode) if safe_mode else None
70
+ can_use_tool = None if yolo_mode else make_permission_checker(project_path, yolo_mode=False)
71
+
72
+ # Initialize hook manager
73
+ hook_manager.set_project_dir(project_path)
74
+ hook_manager.set_session_id(session_id)
72
75
 
73
76
  # Create initial user message
74
77
  from ripperdoc.utils.messages import UserMessage, AssistantMessage, ProgressMessage
@@ -76,7 +79,7 @@ async def run_query(
76
79
  messages: List[UserMessage | AssistantMessage | ProgressMessage] = [create_user_message(prompt)]
77
80
 
78
81
  # Create query context
79
- query_context = QueryContext(tools=tools, safe_mode=safe_mode, verbose=verbose)
82
+ query_context = QueryContext(tools=tools, yolo_mode=yolo_mode, verbose=verbose)
80
83
 
81
84
  try:
82
85
  context: Dict[str, Any] = {}
@@ -164,7 +167,8 @@ async def run_query(
164
167
  console.print(f"[red]Error: {escape(str(e))}[/red]")
165
168
  logger.warning(
166
169
  "[cli] Unhandled error while running prompt: %s: %s",
167
- type(e).__name__, e,
170
+ type(e).__name__,
171
+ e,
168
172
  extra={"session_id": session_id},
169
173
  )
170
174
  if verbose:
@@ -180,104 +184,6 @@ async def run_query(
180
184
  logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
181
185
 
182
186
 
183
- def check_onboarding() -> bool:
184
- """Check if onboarding is complete and run if needed."""
185
- config = get_global_config()
186
-
187
- if config.has_completed_onboarding:
188
- return True
189
-
190
- console.print("[bold cyan]Welcome to Ripperdoc![/bold cyan]\n")
191
- console.print("Let's set up your AI model configuration.\n")
192
-
193
- # Simple onboarding
194
- provider_choices = [
195
- *[p.value for p in ProviderType],
196
- "openai",
197
- "deepseek",
198
- "mistral",
199
- "kimi",
200
- "qwen",
201
- "glm",
202
- "custom",
203
- ]
204
- provider_choice = click.prompt(
205
- "Choose your model protocol",
206
- type=click.Choice(provider_choices),
207
- default=ProviderType.ANTHROPIC.value,
208
- )
209
-
210
- api_base = None
211
- if provider_choice == "custom":
212
- provider_choice = click.prompt(
213
- "Protocol family (for API compatibility)",
214
- type=click.Choice([p.value for p in ProviderType]),
215
- default=ProviderType.OPENAI_COMPATIBLE.value,
216
- )
217
- api_base = click.prompt("API Base URL")
218
-
219
- api_key = ""
220
- while not api_key:
221
- api_key = prompt_secret("Enter your API key").strip()
222
- if not api_key:
223
- console.print("[red]API key is required.[/red]")
224
-
225
- provider = ProviderType(provider_choice)
226
-
227
- # Get model name
228
- if provider == ProviderType.ANTHROPIC:
229
- model = click.prompt("Model name", default="claude-3-5-sonnet-20241022")
230
- elif provider == ProviderType.OPENAI_COMPATIBLE:
231
- default_model = "gpt-4o-mini"
232
- if provider_choice == "deepseek":
233
- default_model = "deepseek-chat"
234
- api_base = api_base or "https://api.deepseek.com"
235
- model = click.prompt("Model name", default=default_model)
236
- if api_base is None:
237
- api_base = (
238
- click.prompt("API base URL (optional)", default="", show_default=False) or None
239
- )
240
- elif provider == ProviderType.GEMINI:
241
- console.print(
242
- "[yellow]Gemini protocol support is not yet available; configuration is saved for "
243
- "future support.[/yellow]"
244
- )
245
- model = click.prompt("Model name", default="gemini-1.5-pro")
246
- if api_base is None:
247
- api_base = (
248
- click.prompt("API base URL (optional)", default="", show_default=False) or None
249
- )
250
- else:
251
- model = click.prompt("Model name")
252
-
253
- context_window_input = click.prompt(
254
- "Context window in tokens (optional, press Enter to skip)", default="", show_default=False
255
- )
256
- context_window = None
257
- if context_window_input.strip():
258
- try:
259
- context_window = int(context_window_input.strip())
260
- except ValueError:
261
- console.print("[yellow]Invalid context window, using auto-detected defaults.[/yellow]")
262
-
263
- # Create model profile
264
- config.model_profiles["default"] = ModelProfile(
265
- provider=provider,
266
- model=model,
267
- api_key=api_key,
268
- api_base=api_base,
269
- context_window=context_window,
270
- )
271
-
272
- config.has_completed_onboarding = True
273
- config.last_onboarding_version = __version__
274
-
275
- save_global_config(config)
276
-
277
- console.print("\n[green]✓ Configuration saved![/green]\n")
278
-
279
- return True
280
-
281
187
 
282
188
  @click.group(invoke_without_command=True)
283
189
  @click.version_option(version=__version__)
@@ -288,10 +194,11 @@ def check_onboarding() -> bool:
288
194
  help="YOLO mode: skip all permission prompts for tools",
289
195
  )
290
196
  @click.option("--verbose", is_flag=True, help="Verbose output")
197
+ @click.option("--show-full-thinking", is_flag=True, help="Show full reasoning content instead of truncated preview")
291
198
  @click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
292
199
  @click.pass_context
293
200
  def cli(
294
- ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, prompt: Optional[str]
201
+ ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, show_full_thinking: bool, prompt: Optional[str]
295
202
  ) -> None:
296
203
  """Ripperdoc - AI-powered coding agent"""
297
204
  session_id = str(uuid.uuid4())
@@ -329,16 +236,16 @@ def cli(
329
236
  # Initialize project configuration for the current working directory
330
237
  get_project_config(project_path)
331
238
 
332
- safe_mode = not yolo
239
+ yolo_mode = yolo
333
240
  logger.debug(
334
241
  "[cli] Configuration initialized",
335
- extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
242
+ extra={"session_id": session_id, "yolo_mode": yolo_mode, "verbose": verbose},
336
243
  )
337
244
 
338
245
  # If prompt is provided, run directly
339
246
  if prompt:
340
247
  tools = get_default_tools()
341
- asyncio.run(run_query(prompt, tools, safe_mode, verbose, session_id=session_id))
248
+ asyncio.run(run_query(prompt, tools, yolo_mode, verbose, session_id=session_id))
342
249
  return
343
250
 
344
251
  # If no command specified, start interactive REPL with Rich interface
@@ -347,8 +254,9 @@ def cli(
347
254
  from ripperdoc.cli.ui.rich_ui import main_rich
348
255
 
349
256
  main_rich(
350
- safe_mode=safe_mode,
257
+ yolo_mode=yolo_mode,
351
258
  verbose=verbose,
259
+ show_full_thinking=show_full_thinking,
352
260
  session_id=session_id,
353
261
  log_file_path=log_file,
354
262
  )
@@ -365,7 +273,8 @@ def config_cmd() -> None:
365
273
  console.print(f"Onboarding Complete: {config.has_completed_onboarding}")
366
274
  console.print(f"Theme: {config.theme}")
367
275
  console.print(f"Verbose: {config.verbose}")
368
- console.print(f"Safe Mode: {config.safe_mode}\n")
276
+ console.print(f"Yolo Mode: {config.yolo_mode}")
277
+ console.print(f"Show Full Thinking: {config.show_full_thinking}\n")
369
278
 
370
279
  if config.model_profiles:
371
280
  console.print("[bold]Model Profiles:[/bold]")
@@ -392,11 +301,20 @@ def main() -> None:
392
301
  sys.exit(130)
393
302
  except SystemExit:
394
303
  raise
395
- except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError, click.ClickException) as e:
304
+ except (
305
+ RuntimeError,
306
+ ValueError,
307
+ TypeError,
308
+ OSError,
309
+ IOError,
310
+ ConnectionError,
311
+ click.ClickException,
312
+ ) as e:
396
313
  console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
397
314
  logger.warning(
398
315
  "[cli] Fatal error in main CLI entrypoint: %s: %s",
399
- type(e).__name__, e,
316
+ type(e).__name__,
317
+ e,
400
318
  )
401
319
  sys.exit(1)
402
320
 
@@ -0,0 +1,146 @@
1
+ """Slash command registry with custom command support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional, Tuple, Union
7
+
8
+ from .base import SlashCommand
9
+ from .agents_cmd import command as agents_command
10
+ from .clear_cmd import command as clear_command
11
+ from .compact_cmd import command as compact_command
12
+ from .config_cmd import command as config_command
13
+ from .cost_cmd import command as cost_command
14
+ from .context_cmd import command as context_command
15
+ from .doctor_cmd import command as doctor_command
16
+ from .exit_cmd import command as exit_command
17
+ from .help_cmd import command as help_command
18
+ from .hooks_cmd import command as hooks_command
19
+ from .memory_cmd import command as memory_command
20
+ from .mcp_cmd import command as mcp_command
21
+ from .models_cmd import command as models_command
22
+ from .permissions_cmd import command as permissions_command
23
+ from .resume_cmd import command as resume_command
24
+ from .tasks_cmd import command as tasks_command
25
+ from .status_cmd import command as status_command
26
+ from .todos_cmd import command as todos_command
27
+ from .tools_cmd import command as tools_command
28
+
29
+ from ripperdoc.core.custom_commands import (
30
+ CustomCommandDefinition,
31
+ load_all_custom_commands,
32
+ expand_command_content,
33
+ )
34
+
35
+
36
+ def _build_registry(commands: List[SlashCommand]) -> Dict[str, SlashCommand]:
37
+ """Map command names and aliases to SlashCommand entries."""
38
+ registry: Dict[str, SlashCommand] = {}
39
+ for cmd in commands:
40
+ registry[cmd.name] = cmd
41
+ for alias in cmd.aliases:
42
+ registry[alias] = cmd
43
+ return registry
44
+
45
+
46
+ ALL_COMMANDS: List[SlashCommand] = [
47
+ help_command,
48
+ clear_command,
49
+ config_command,
50
+ tools_command,
51
+ models_command,
52
+ exit_command,
53
+ status_command,
54
+ doctor_command,
55
+ memory_command,
56
+ permissions_command,
57
+ tasks_command,
58
+ todos_command,
59
+ mcp_command,
60
+ hooks_command,
61
+ cost_command,
62
+ context_command,
63
+ compact_command,
64
+ resume_command,
65
+ agents_command,
66
+ ]
67
+
68
+ COMMAND_REGISTRY: Dict[str, SlashCommand] = _build_registry(ALL_COMMANDS)
69
+
70
+ # Cache for custom commands
71
+ _custom_commands_cache: Optional[Tuple[Path, List[CustomCommandDefinition]]] = None
72
+
73
+
74
+ def _get_custom_commands(project_path: Optional[Path] = None) -> List[CustomCommandDefinition]:
75
+ """Get custom commands with caching."""
76
+ global _custom_commands_cache
77
+ current_path = (project_path or Path.cwd()).resolve()
78
+
79
+ # Return cached commands if same project
80
+ if _custom_commands_cache and _custom_commands_cache[0] == current_path:
81
+ return _custom_commands_cache[1]
82
+
83
+ # Load and cache
84
+ result = load_all_custom_commands(project_path=current_path)
85
+ _custom_commands_cache = (current_path, result.commands)
86
+ return result.commands
87
+
88
+
89
+ def refresh_custom_commands(project_path: Optional[Path] = None) -> List[CustomCommandDefinition]:
90
+ """Force reload custom commands."""
91
+ global _custom_commands_cache
92
+ _custom_commands_cache = None
93
+ return _get_custom_commands(project_path)
94
+
95
+
96
+ def list_slash_commands() -> List[SlashCommand]:
97
+ """Return the ordered list of base slash commands (no aliases)."""
98
+ return ALL_COMMANDS
99
+
100
+
101
+ def list_custom_commands(project_path: Optional[Path] = None) -> List[CustomCommandDefinition]:
102
+ """Return all loaded custom commands."""
103
+ return _get_custom_commands(project_path)
104
+
105
+
106
+ def get_slash_command(name: str) -> SlashCommand | None:
107
+ """Return a built-in command by name or alias."""
108
+ return COMMAND_REGISTRY.get(name)
109
+
110
+
111
+ def get_custom_command(
112
+ name: str, project_path: Optional[Path] = None
113
+ ) -> CustomCommandDefinition | None:
114
+ """Return a custom command by name."""
115
+ commands = _get_custom_commands(project_path)
116
+ return next((cmd for cmd in commands if cmd.name == name), None)
117
+
118
+
119
+ def slash_command_completions(
120
+ project_path: Optional[Path] = None,
121
+ ) -> List[Tuple[str, Union[SlashCommand, CustomCommandDefinition]]]:
122
+ """Return (name, command) pairs for completion including aliases and custom commands."""
123
+ completions: List[Tuple[str, Union[SlashCommand, CustomCommandDefinition]]] = []
124
+
125
+ # Add built-in commands
126
+ completions.extend(list(COMMAND_REGISTRY.items()))
127
+
128
+ # Add custom commands
129
+ custom_cmds = _get_custom_commands(project_path)
130
+ for cmd in custom_cmds:
131
+ completions.append((cmd.name, cmd))
132
+
133
+ return completions
134
+
135
+
136
+ __all__ = [
137
+ "SlashCommand",
138
+ "CustomCommandDefinition",
139
+ "list_slash_commands",
140
+ "list_custom_commands",
141
+ "get_slash_command",
142
+ "get_custom_command",
143
+ "slash_command_completions",
144
+ "refresh_custom_commands",
145
+ "expand_command_content",
146
+ ]
@@ -116,7 +116,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
116
116
  print_agents_usage()
117
117
  logger.warning(
118
118
  "[agents_cmd] Failed to create agent: %s: %s",
119
- type(exc).__name__, exc,
119
+ type(exc).__name__,
120
+ exc,
120
121
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
121
122
  )
122
123
  return True
@@ -148,7 +149,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
148
149
  print_agents_usage()
149
150
  logger.warning(
150
151
  "[agents_cmd] Failed to delete agent: %s: %s",
151
- type(exc).__name__, exc,
152
+ type(exc).__name__,
153
+ exc,
152
154
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
153
155
  )
154
156
  return True
@@ -226,7 +228,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
226
228
  print_agents_usage()
227
229
  logger.warning(
228
230
  "[agents_cmd] Failed to update agent: %s: %s",
229
- type(exc).__name__, exc,
231
+ type(exc).__name__,
232
+ exc,
230
233
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
231
234
  )
232
235
  return True
@@ -9,10 +9,7 @@ def _handle(ui: Any, _: str) -> bool:
9
9
 
10
10
 
11
11
  command = SlashCommand(
12
- name="clear",
13
- description="Clear conversation history",
14
- handler=_handle,
15
- aliases=("new",)
12
+ name="clear", description="Clear conversation history", handler=_handle, aliases=("new",)
16
13
  )
17
14
 
18
15
 
@@ -16,7 +16,7 @@ def _handle(ui: Any, _: str) -> bool:
16
16
  ui.console.print(
17
17
  f"\n[bold]Model (main -> {escape(str(main_pointer))}):[/bold] {escape(str(model_label))}"
18
18
  )
19
- ui.console.print(f"[bold]Safe Mode:[/bold] {escape(str(ui.safe_mode))}")
19
+ ui.console.print(f"[bold]Yolo Mode:[/bold] {escape(str(ui.yolo_mode))}")
20
20
  ui.console.print(f"[bold]Verbose:[/bold] {escape(str(ui.verbose))}")
21
21
  return True
22
22
 
@@ -40,7 +40,7 @@ def _handle(ui: Any, _: str) -> bool:
40
40
  if not ui.query_context:
41
41
  ui.query_context = QueryContext(
42
42
  tools=ui.get_default_tools(),
43
- safe_mode=ui.safe_mode,
43
+ yolo_mode=ui.yolo_mode,
44
44
  verbose=ui.verbose,
45
45
  )
46
46
 
@@ -126,7 +126,8 @@ def _handle(ui: Any, _: str) -> bool:
126
126
  except (OSError, RuntimeError, AttributeError, TypeError) as exc:
127
127
  logger.warning(
128
128
  "[context_cmd] Failed to summarize MCP tools: %s: %s",
129
- type(exc).__name__, exc,
129
+ type(exc).__name__,
130
+ exc,
130
131
  extra={"session_id": getattr(ui, "session_id", None)},
131
132
  )
132
133
  for line in lines:
@@ -125,10 +125,17 @@ def _mcp_status(
125
125
  servers = asyncio.run(_load())
126
126
  else:
127
127
  servers = runner(_load())
128
- except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc: # pragma: no cover - defensive
128
+ except (
129
+ OSError,
130
+ RuntimeError,
131
+ ConnectionError,
132
+ ValueError,
133
+ TypeError,
134
+ ) as exc: # pragma: no cover - defensive
129
135
  logger.warning(
130
136
  "[doctor] Failed to load MCP servers: %s: %s",
131
- type(exc).__name__, exc,
137
+ type(exc).__name__,
138
+ exc,
132
139
  exc_info=exc,
133
140
  )
134
141
  rows.append(_status_row("MCP", "error", f"Failed to load MCP config: {exc}"))
@@ -161,10 +168,17 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
161
168
  return _status_row(
162
169
  "Project config", "ok", f".ripperdoc/config.json loaded for {project_path}"
163
170
  )
164
- except (OSError, IOError, json.JSONDecodeError, ValueError, TypeError) as exc: # pragma: no cover - defensive
171
+ except (
172
+ OSError,
173
+ IOError,
174
+ json.JSONDecodeError,
175
+ ValueError,
176
+ TypeError,
177
+ ) as exc: # pragma: no cover - defensive
165
178
  logger.warning(
166
179
  "[doctor] Failed to load project config: %s: %s",
167
- type(exc).__name__, exc,
180
+ type(exc).__name__,
181
+ exc,
168
182
  exc_info=exc,
169
183
  )
170
184
  return _status_row(
@@ -0,0 +1,30 @@
1
+ from typing import Any
2
+ from .base import SlashCommand
3
+
4
+
5
+ def _handle(ui: Any, _: str) -> bool:
6
+ ui.console.print("\n[bold]Available Slash Commands:[/bold]")
7
+ for cmd in ui.command_list:
8
+ alias_text = f" (aliases: {', '.join(cmd.aliases)})" if cmd.aliases else ""
9
+ ui.console.print(f" /{cmd.name:<12} - {cmd.description}{alias_text}")
10
+
11
+ # Show custom commands if any
12
+ custom_cmds = getattr(ui, "_custom_command_list", [])
13
+ if custom_cmds:
14
+ ui.console.print("\n[bold]Custom Commands:[/bold]")
15
+ for cmd in sorted(custom_cmds, key=lambda c: c.name):
16
+ hint = f" {cmd.argument_hint}" if cmd.argument_hint else ""
17
+ location = f" ({cmd.location.value})" if cmd.location else ""
18
+ ui.console.print(f" /{cmd.name:<12}{hint} - {cmd.description}{location}")
19
+
20
+ return True
21
+
22
+
23
+ command = SlashCommand(
24
+ name="help",
25
+ description="Show available slash commands",
26
+ handler=_handle,
27
+ )
28
+
29
+
30
+ __all__ = ["command"]