ripperdoc 0.2.4__tar.gz → 0.2.6__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 (134) hide show
  1. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/PKG-INFO +12 -1
  2. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/README.md +11 -0
  3. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/__init__.py +1 -1
  4. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/__main__.py +0 -5
  5. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/cli.py +37 -16
  6. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/__init__.py +2 -0
  7. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/agents_cmd.py +12 -9
  8. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/compact_cmd.py +7 -3
  9. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/context_cmd.py +33 -13
  10. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/doctor_cmd.py +27 -14
  11. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/exit_cmd.py +1 -1
  12. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/mcp_cmd.py +13 -8
  13. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/memory_cmd.py +5 -5
  14. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/models_cmd.py +47 -16
  15. ripperdoc-0.2.6/ripperdoc/cli/commands/permissions_cmd.py +302 -0
  16. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/resume_cmd.py +1 -2
  17. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/tasks_cmd.py +24 -13
  18. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/rich_ui.py +659 -407
  19. ripperdoc-0.2.6/ripperdoc/cli/ui/tool_renderers.py +298 -0
  20. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/agents.py +17 -9
  21. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/config.py +130 -6
  22. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/default_tools.py +7 -2
  23. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/permissions.py +20 -14
  24. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/providers/anthropic.py +107 -4
  25. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/providers/base.py +33 -4
  26. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/providers/gemini.py +169 -50
  27. ripperdoc-0.2.6/ripperdoc/core/providers/openai.py +487 -0
  28. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/query.py +306 -62
  29. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/query_utils.py +50 -6
  30. ripperdoc-0.2.6/ripperdoc/core/skills.py +295 -0
  31. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/system_prompt.py +13 -7
  32. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/tool.py +13 -9
  33. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/sdk/client.py +14 -1
  34. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/ask_user_question_tool.py +20 -22
  35. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/background_shell.py +19 -13
  36. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/bash_tool.py +376 -217
  37. ripperdoc-0.2.6/ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  38. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/enter_plan_mode_tool.py +5 -2
  39. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/exit_plan_mode_tool.py +6 -3
  40. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/file_edit_tool.py +57 -12
  41. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/file_read_tool.py +20 -8
  42. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/file_write_tool.py +53 -15
  43. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/glob_tool.py +10 -9
  44. ripperdoc-0.2.6/ripperdoc/tools/grep_tool.py +370 -0
  45. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/ls_tool.py +6 -6
  46. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/mcp_tools.py +106 -456
  47. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/multi_edit_tool.py +49 -9
  48. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/notebook_edit_tool.py +61 -15
  49. ripperdoc-0.2.6/ripperdoc/tools/skill_tool.py +205 -0
  50. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/task_tool.py +7 -8
  51. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/todo_tool.py +12 -12
  52. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/tool_search_tool.py +5 -6
  53. ripperdoc-0.2.6/ripperdoc/utils/coerce.py +34 -0
  54. ripperdoc-0.2.6/ripperdoc/utils/context_length_errors.py +252 -0
  55. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/file_watch.py +5 -4
  56. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/json_utils.py +4 -4
  57. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/log.py +3 -3
  58. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/mcp.py +36 -15
  59. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/memory.py +9 -6
  60. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/message_compaction.py +16 -11
  61. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/messages.py +73 -8
  62. ripperdoc-0.2.6/ripperdoc/utils/path_ignore.py +677 -0
  63. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/permissions/__init__.py +7 -1
  64. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/permissions/path_validation_utils.py +16 -13
  65. ripperdoc-0.2.6/ripperdoc/utils/permissions/shell_command_validation.py +552 -0
  66. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/prompt.py +1 -1
  67. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/safe_get_cwd.py +5 -2
  68. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/session_history.py +38 -19
  69. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/todo.py +6 -2
  70. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/token_estimation.py +4 -3
  71. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/PKG-INFO +12 -1
  72. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/SOURCES.txt +11 -0
  73. ripperdoc-0.2.6/tests/test_context_length_errors.py +74 -0
  74. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_messages.py +39 -0
  75. ripperdoc-0.2.6/tests/test_path_ignore.py +492 -0
  76. ripperdoc-0.2.6/tests/test_shell_permissions.py +727 -0
  77. ripperdoc-0.2.6/tests/test_skills.py +196 -0
  78. ripperdoc-0.2.4/ripperdoc/core/providers/openai.py +0 -253
  79. ripperdoc-0.2.4/ripperdoc/tools/grep_tool.py +0 -239
  80. ripperdoc-0.2.4/ripperdoc/utils/permissions/shell_command_validation.py +0 -74
  81. ripperdoc-0.2.4/tests/test_shell_permissions.py +0 -124
  82. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/LICENSE +0 -0
  83. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/pyproject.toml +0 -0
  84. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/__init__.py +0 -0
  85. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/base.py +0 -0
  86. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/clear_cmd.py +0 -0
  87. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/config_cmd.py +0 -0
  88. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/cost_cmd.py +0 -0
  89. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/help_cmd.py +0 -0
  90. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/status_cmd.py +0 -0
  91. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/todos_cmd.py +0 -0
  92. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/commands/tools_cmd.py +0 -0
  93. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/__init__.py +0 -0
  94. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/context_display.py +0 -0
  95. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/helpers.py +0 -0
  96. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/spinner.py +0 -0
  97. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/cli/ui/thinking_spinner.py +0 -0
  98. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/__init__.py +0 -0
  99. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/commands.py +0 -0
  100. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/core/providers/__init__.py +0 -0
  101. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/sdk/__init__.py +0 -0
  102. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/__init__.py +0 -0
  103. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/bash_output_tool.py +0 -0
  104. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/tools/kill_bash_tool.py +0 -0
  105. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/__init__.py +0 -0
  106. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/bash_constants.py +0 -0
  107. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/bash_output_utils.py +0 -0
  108. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/exit_code_handlers.py +0 -0
  109. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/git_utils.py +0 -0
  110. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/output_utils.py +0 -0
  111. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/path_utils.py +0 -0
  112. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/permissions/tool_permission_utils.py +0 -0
  113. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/sandbox_utils.py +0 -0
  114. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/session_usage.py +0 -0
  115. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/shell_token_utils.py +0 -0
  116. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc/utils/shell_utils.py +0 -0
  117. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/dependency_links.txt +0 -0
  118. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/entry_points.txt +0 -0
  119. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/requires.txt +0 -0
  120. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/ripperdoc.egg-info/top_level.txt +0 -0
  121. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/setup.cfg +0 -0
  122. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/setup.py +0 -0
  123. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_background_shell_shutdown.py +0 -0
  124. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_cli_commands.py +0 -0
  125. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_config.py +0 -0
  126. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_context_limits.py +0 -0
  127. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_mcp_config.py +0 -0
  128. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_output_utils.py +0 -0
  129. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_permissions.py +0 -0
  130. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_query_abort.py +0 -0
  131. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_sdk.py +0 -0
  132. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_todo.py +0 -0
  133. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_tool_search.py +0 -0
  134. {ripperdoc-0.2.4 → ripperdoc-0.2.6}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ripperdoc
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: AI-powered terminal assistant for coding tasks
5
5
  Author: Ripperdoc Team
6
6
  License: Apache-2.0
@@ -50,6 +50,7 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
50
50
  - **Codebase Understanding** - Analyzes project structure and code relationships
51
51
  - **Command Execution** - Run shell commands with real-time feedback
52
52
  - **Tool System** - Extensible architecture with specialized tools
53
+ - **Agent Skills** - Load SKILL.md bundles to extend the agent on demand
53
54
  - **Subagents** - Delegate tasks to specialized agents with their own tool scopes
54
55
  - **File Operations** - Read, write, edit, search, and manage files
55
56
  - **Todo Tracking** - Plan, read, and update persistent todo lists per project
@@ -117,6 +118,16 @@ See the [examples/](examples/) directory for complete SDK usage examples.
117
118
 
118
119
  Safe mode is the default. Use `--unsafe` to skip permission prompts. Choose `a`/`always` to allow a tool for the current session (not persisted across sessions).
119
120
 
121
+ ### Agent Skills
122
+
123
+ Extend Ripperdoc with reusable Skill bundles:
124
+
125
+ - Personal skills live in `~/.ripperdoc/skills/<skill-name>/SKILL.md`
126
+ - Project skills live in `.ripperdoc/skills/<skill-name>/SKILL.md` and can be checked into git
127
+ - Each `SKILL.md` starts with YAML frontmatter (`name`, `description`, optional `allowed-tools`, `model`, `max-thinking-tokens`, `disable-model-invocation`) followed by the instructions; add supporting files alongside it
128
+ - Model and max-thinking-token hints from skills are applied automatically for the rest of the session after you load them with the `Skill` tool
129
+ - Ripperdoc exposes skill names/descriptions in the system prompt and loads full content on demand via the `Skill` tool
130
+
120
131
  ## Examples
121
132
 
122
133
  ### Code Analysis
@@ -13,6 +13,7 @@ Ripperdoc is an AI-powered terminal assistant for coding tasks, providing an int
13
13
  - **Codebase Understanding** - Analyzes project structure and code relationships
14
14
  - **Command Execution** - Run shell commands with real-time feedback
15
15
  - **Tool System** - Extensible architecture with specialized tools
16
+ - **Agent Skills** - Load SKILL.md bundles to extend the agent on demand
16
17
  - **Subagents** - Delegate tasks to specialized agents with their own tool scopes
17
18
  - **File Operations** - Read, write, edit, search, and manage files
18
19
  - **Todo Tracking** - Plan, read, and update persistent todo lists per project
@@ -80,6 +81,16 @@ See the [examples/](examples/) directory for complete SDK usage examples.
80
81
 
81
82
  Safe mode is the default. Use `--unsafe` to skip permission prompts. Choose `a`/`always` to allow a tool for the current session (not persisted across sessions).
82
83
 
84
+ ### Agent Skills
85
+
86
+ Extend Ripperdoc with reusable Skill bundles:
87
+
88
+ - Personal skills live in `~/.ripperdoc/skills/<skill-name>/SKILL.md`
89
+ - Project skills live in `.ripperdoc/skills/<skill-name>/SKILL.md` and can be checked into git
90
+ - Each `SKILL.md` starts with YAML frontmatter (`name`, `description`, optional `allowed-tools`, `model`, `max-thinking-tokens`, `disable-model-invocation`) followed by the instructions; add supporting files alongside it
91
+ - Model and max-thinking-token hints from skills are applied automatically for the rest of the session after you load them with the `Skill` tool
92
+ - Ripperdoc exposes skill names/descriptions in the system prompt and loads full content on demand via the `Skill` tool
93
+
83
94
  ## Examples
84
95
 
85
96
  ### Code Analysis
@@ -1,3 +1,3 @@
1
1
  """Ripperdoc - AI-powered coding agent."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.6"
@@ -13,11 +13,6 @@ Features:
13
13
  Quick Start:
14
14
  pip install -e .
15
15
  ripperdoc -p "your prompt here"
16
-
17
- For more information:
18
- - README.md: Project overview
19
- - QUICKSTART.md: Quick start guide
20
- - DEVELOPMENT.md: Development guide
21
16
  """
22
17
 
23
18
  from ripperdoc import __version__
@@ -21,6 +21,7 @@ from ripperdoc.core.config import (
21
21
  from ripperdoc.core.default_tools import get_default_tools
22
22
  from ripperdoc.core.query import query, QueryContext
23
23
  from ripperdoc.core.system_prompt import build_system_prompt
24
+ from ripperdoc.core.skills import build_skill_summary, load_all_skills
24
25
  from ripperdoc.utils.messages import create_user_message
25
26
  from ripperdoc.utils.memory import build_memory_instructions
26
27
  from ripperdoc.core.permissions import make_permission_checker
@@ -86,18 +87,26 @@ async def run_query(
86
87
  tools = merge_tools_with_dynamic(tools, dynamic_tools)
87
88
  query_context.tools = tools
88
89
  mcp_instructions = format_mcp_instructions(servers)
89
- base_system_prompt = build_system_prompt(
90
+ skill_result = load_all_skills(Path.cwd())
91
+ for err in skill_result.errors:
92
+ logger.warning(
93
+ "[skills] Failed to load skill",
94
+ extra={"path": str(err.path), "reason": err.reason},
95
+ )
96
+ skill_instructions = build_skill_summary(skill_result.skills)
97
+ additional_instructions: List[str] = []
98
+ if skill_instructions:
99
+ additional_instructions.append(skill_instructions)
100
+ memory_instructions = build_memory_instructions()
101
+ if memory_instructions:
102
+ additional_instructions.append(memory_instructions)
103
+ system_prompt = build_system_prompt(
90
104
  tools,
91
105
  prompt,
92
106
  context,
107
+ additional_instructions=additional_instructions or None,
93
108
  mcp_instructions=mcp_instructions,
94
109
  )
95
- memory_instructions = build_memory_instructions()
96
- system_prompt = (
97
- f"{base_system_prompt}\n\n{memory_instructions}"
98
- if memory_instructions
99
- else base_system_prompt
100
- )
101
110
 
102
111
  # Run the query
103
112
  try:
@@ -112,6 +121,7 @@ async def run_query(
112
121
  Markdown(message.message.content),
113
122
  title="Ripperdoc",
114
123
  border_style="cyan",
124
+ padding=(0, 1),
115
125
  )
116
126
  )
117
127
  else:
@@ -124,6 +134,7 @@ async def run_query(
124
134
  Markdown(block["text"]),
125
135
  title="Ripperdoc",
126
136
  border_style="cyan",
137
+ padding=(0, 1),
127
138
  )
128
139
  )
129
140
  else:
@@ -133,6 +144,7 @@ async def run_query(
133
144
  Markdown(block.text or ""),
134
145
  title="Ripperdoc",
135
146
  border_style="cyan",
147
+ padding=(0, 1),
136
148
  )
137
149
  )
138
150
 
@@ -146,10 +158,14 @@ async def run_query(
146
158
 
147
159
  except KeyboardInterrupt:
148
160
  console.print("\n[yellow]Interrupted by user[/yellow]")
149
- except Exception as e:
161
+ except asyncio.CancelledError:
162
+ console.print("\n[yellow]Operation cancelled[/yellow]")
163
+ except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError) as e:
150
164
  console.print(f"[red]Error: {escape(str(e))}[/red]")
151
- logger.exception(
152
- "[cli] Unhandled error while running prompt", extra={"session_id": session_id}
165
+ logger.warning(
166
+ "[cli] Unhandled error while running prompt: %s: %s",
167
+ type(e).__name__, e,
168
+ extra={"session_id": session_id},
153
169
  )
154
170
  if verbose:
155
171
  import traceback
@@ -267,15 +283,15 @@ def check_onboarding() -> bool:
267
283
  @click.version_option(version=__version__)
268
284
  @click.option("--cwd", type=click.Path(exists=True), help="Working directory")
269
285
  @click.option(
270
- "--unsafe",
286
+ "--yolo",
271
287
  is_flag=True,
272
- help="Disable safe mode (skip permission prompts for tools)",
288
+ help="YOLO mode: skip all permission prompts for tools",
273
289
  )
274
290
  @click.option("--verbose", is_flag=True, help="Verbose output")
275
291
  @click.option("-p", "--prompt", type=str, help="Direct prompt (non-interactive)")
276
292
  @click.pass_context
277
293
  def cli(
278
- ctx: click.Context, cwd: Optional[str], unsafe: bool, verbose: bool, prompt: Optional[str]
294
+ ctx: click.Context, cwd: Optional[str], yolo: bool, verbose: bool, prompt: Optional[str]
279
295
  ) -> None:
280
296
  """Ripperdoc - AI-powered coding agent"""
281
297
  session_id = str(uuid.uuid4())
@@ -313,7 +329,7 @@ def cli(
313
329
  # Initialize project configuration for the current working directory
314
330
  get_project_config(project_path)
315
331
 
316
- safe_mode = not unsafe
332
+ safe_mode = not yolo
317
333
  logger.debug(
318
334
  "[cli] Configuration initialized",
319
335
  extra={"session_id": session_id, "safe_mode": safe_mode, "verbose": verbose},
@@ -374,9 +390,14 @@ def main() -> None:
374
390
  except KeyboardInterrupt:
375
391
  console.print("\n[yellow]Interrupted[/yellow]")
376
392
  sys.exit(130)
377
- except Exception as e:
393
+ except SystemExit:
394
+ raise
395
+ except (RuntimeError, ValueError, TypeError, OSError, IOError, ConnectionError, click.ClickException) as e:
378
396
  console.print(f"[red]Fatal error: {escape(str(e))}[/red]")
379
- logger.exception("[cli] Fatal error in main CLI entrypoint")
397
+ logger.warning(
398
+ "[cli] Fatal error in main CLI entrypoint: %s: %s",
399
+ type(e).__name__, e,
400
+ )
380
401
  sys.exit(1)
381
402
 
382
403
 
@@ -17,6 +17,7 @@ from .help_cmd import command as help_command
17
17
  from .memory_cmd import command as memory_command
18
18
  from .mcp_cmd import command as mcp_command
19
19
  from .models_cmd import command as models_command
20
+ from .permissions_cmd import command as permissions_command
20
21
  from .resume_cmd import command as resume_command
21
22
  from .tasks_cmd import command as tasks_command
22
23
  from .status_cmd import command as status_command
@@ -44,6 +45,7 @@ ALL_COMMANDS: List[SlashCommand] = [
44
45
  status_command,
45
46
  doctor_command,
46
47
  memory_command,
48
+ permissions_command,
47
49
  tasks_command,
48
50
  todos_command,
49
51
  mcp_command,
@@ -111,11 +111,12 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
111
111
  console.print(
112
112
  f"[green]✓ Agent '{escape(agent_name)}' created at {escape(str(path))}[/green]"
113
113
  )
114
- except Exception as exc:
114
+ except (OSError, IOError, PermissionError, ValueError) as exc:
115
115
  console.print(f"[red]Failed to create agent: {escape(str(exc))}[/red]")
116
116
  print_agents_usage()
117
- logger.exception(
118
- "[agents_cmd] Failed to create agent",
117
+ logger.warning(
118
+ "[agents_cmd] Failed to create agent: %s: %s",
119
+ type(exc).__name__, exc,
119
120
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
120
121
  )
121
122
  return True
@@ -142,11 +143,12 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
142
143
  )
143
144
  except FileNotFoundError as exc:
144
145
  console.print(f"[yellow]{escape(str(exc))}[/yellow]")
145
- except Exception as exc:
146
+ except (OSError, IOError, PermissionError, ValueError) as exc:
146
147
  console.print(f"[red]Failed to delete agent: {escape(str(exc))}[/red]")
147
148
  print_agents_usage()
148
- logger.exception(
149
- "[agents_cmd] Failed to delete agent",
149
+ logger.warning(
150
+ "[agents_cmd] Failed to delete agent: %s: %s",
151
+ type(exc).__name__, exc,
150
152
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
151
153
  )
152
154
  return True
@@ -219,11 +221,12 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
219
221
  console.print(
220
222
  f"[green]✓ Agent '{escape(agent_name)}' updated at {escape(str(path))}[/green]"
221
223
  )
222
- except Exception as exc:
224
+ except (OSError, IOError, PermissionError, ValueError) as exc:
223
225
  console.print(f"[red]Failed to update agent: {escape(str(exc))}[/red]")
224
226
  print_agents_usage()
225
- logger.exception(
226
- "[agents_cmd] Failed to update agent",
227
+ logger.warning(
228
+ "[agents_cmd] Failed to update agent: %s: %s",
229
+ type(exc).__name__, exc,
227
230
  extra={"agent": agent_name, "session_id": getattr(ui, "session_id", None)},
228
231
  )
229
232
  return True
@@ -1,11 +1,15 @@
1
- import asyncio
2
-
3
1
  from typing import Any
4
2
  from .base import SlashCommand
5
3
 
6
4
 
7
5
  def _handle(ui: Any, trimmed_arg: str) -> bool:
8
- asyncio.run(ui._run_manual_compact(trimmed_arg))
6
+ runner = getattr(ui, "run_async", None)
7
+ if callable(runner):
8
+ runner(ui._run_manual_compact(trimmed_arg))
9
+ else:
10
+ import asyncio
11
+
12
+ asyncio.run(ui._run_manual_compact(trimmed_arg))
9
13
  return True
10
14
 
11
15
 
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import json
3
2
  from typing import List, Any
4
3
 
@@ -7,6 +6,7 @@ from ripperdoc.cli.ui.context_display import format_tokens
7
6
  from ripperdoc.core.config import get_global_config, provider_protocol
8
7
  from ripperdoc.core.query import QueryContext
9
8
  from ripperdoc.core.system_prompt import build_system_prompt
9
+ from ripperdoc.core.skills import build_skill_summary, load_all_skills
10
10
  from ripperdoc.utils.memory import build_memory_instructions
11
11
  from ripperdoc.utils.message_compaction import (
12
12
  get_remaining_context_tokens,
@@ -18,7 +18,6 @@ from ripperdoc.utils.mcp import (
18
18
  estimate_mcp_tokens,
19
19
  format_mcp_instructions,
20
20
  load_mcp_servers_async,
21
- shutdown_mcp_runtime,
22
21
  )
23
22
  from ripperdoc.utils.log import get_logger
24
23
 
@@ -46,21 +45,41 @@ def _handle(ui: Any, _: str) -> bool:
46
45
  )
47
46
 
48
47
  async def _load_servers() -> List[Any]:
49
- try:
50
- return await load_mcp_servers_async(ui.project_path)
51
- finally:
52
- await shutdown_mcp_runtime()
48
+ return await load_mcp_servers_async(ui.project_path)
53
49
 
54
- servers = asyncio.run(_load_servers())
50
+ runner = getattr(ui, "run_async", None)
51
+ if callable(runner):
52
+ servers = runner(_load_servers())
53
+ else:
54
+ import asyncio
55
+
56
+ servers = asyncio.run(_load_servers())
55
57
  mcp_instructions = format_mcp_instructions(servers)
58
+ skill_result = load_all_skills(ui.project_path)
59
+ for err in skill_result.errors:
60
+ logger.warning(
61
+ "[skills] Failed to load skill",
62
+ extra={
63
+ "path": str(err.path),
64
+ "reason": err.reason,
65
+ "session_id": getattr(ui, "session_id", None),
66
+ },
67
+ )
68
+ skill_instructions = build_skill_summary(skill_result.skills)
69
+ additional_instructions: List[str] = []
70
+ if skill_instructions:
71
+ additional_instructions.append(skill_instructions)
72
+ memory_instructions = build_memory_instructions()
73
+ if memory_instructions:
74
+ additional_instructions.append(memory_instructions)
56
75
  base_system_prompt = build_system_prompt(
57
76
  ui.query_context.tools,
58
77
  "",
59
78
  {},
79
+ additional_instructions=additional_instructions or None,
60
80
  mcp_instructions=mcp_instructions,
61
81
  )
62
- memory_instructions = build_memory_instructions()
63
- memory_tokens = estimate_tokens(memory_instructions) if memory_instructions else 0
82
+ memory_tokens = 0
64
83
  mcp_tokens = estimate_mcp_tokens(servers) if mcp_instructions else 0
65
84
 
66
85
  breakdown = summarize_context_usage(
@@ -99,14 +118,15 @@ def _handle(ui: Any, _: str) -> bool:
99
118
  try:
100
119
  schema = tool.input_schema.model_json_schema()
101
120
  token_est = estimate_tokens(json.dumps(schema, sort_keys=True))
102
- except Exception:
121
+ except (AttributeError, TypeError, ValueError):
103
122
  token_est = 0
104
123
  lines.append(f" └ {display}: {format_tokens(token_est)} tokens")
105
124
  if len(mcp_tools) > 20:
106
125
  lines.append(f" └ ... (+{len(mcp_tools) - 20} more)")
107
- except Exception:
108
- logger.exception(
109
- "[context_cmd] Failed to summarize MCP tools",
126
+ except (OSError, RuntimeError, AttributeError, TypeError) as exc:
127
+ logger.warning(
128
+ "[context_cmd] Failed to summarize MCP tools: %s: %s",
129
+ type(exc).__name__, exc,
110
130
  extra={"session_id": getattr(ui, "session_id", None)},
111
131
  )
112
132
  for line in lines:
@@ -2,9 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
5
+ import json
6
6
  from pathlib import Path
7
- from typing import Any, List, Optional, Tuple
7
+ from typing import Any, Callable, List, Optional, Tuple
8
8
 
9
9
  from rich.markup import escape
10
10
  from rich.panel import Panel
@@ -18,7 +18,7 @@ from ripperdoc.core.config import (
18
18
  )
19
19
  from ripperdoc.cli.ui.helpers import get_profile_for_pointer
20
20
  from ripperdoc.utils.log import get_logger
21
- from ripperdoc.utils.mcp import load_mcp_servers_async, shutdown_mcp_runtime
21
+ from ripperdoc.utils.mcp import load_mcp_servers_async
22
22
  from ripperdoc.utils.sandbox_utils import is_sandbox_available
23
23
 
24
24
  from .base import SlashCommand
@@ -108,21 +108,29 @@ def _sandbox_status() -> Tuple[str, str, str]:
108
108
  return _status_row("Sandbox", "warn", "Sandbox runtime not detected; commands run normally")
109
109
 
110
110
 
111
- def _mcp_status(project_path: Path) -> Tuple[List[Tuple[str, str, str]], List[str]]:
111
+ def _mcp_status(
112
+ project_path: Path, runner: Optional[Callable[[Any], Any]] = None
113
+ ) -> Tuple[List[Tuple[str, str, str]], List[str]]:
112
114
  """Return MCP status rows and errors."""
113
115
  rows: List[Tuple[str, str, str]] = []
114
116
  errors: List[str] = []
115
117
 
116
118
  async def _load() -> List[Any]:
117
- try:
118
- return await load_mcp_servers_async(project_path)
119
- finally:
120
- await shutdown_mcp_runtime()
119
+ return await load_mcp_servers_async(project_path)
121
120
 
122
121
  try:
123
- servers = asyncio.run(_load())
124
- except Exception as exc: # pragma: no cover - defensive
125
- logger.exception("[doctor] Failed to load MCP servers", exc_info=exc)
122
+ if runner is None:
123
+ import asyncio
124
+
125
+ servers = asyncio.run(_load())
126
+ else:
127
+ servers = runner(_load())
128
+ except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc: # pragma: no cover - defensive
129
+ logger.warning(
130
+ "[doctor] Failed to load MCP servers: %s: %s",
131
+ type(exc).__name__, exc,
132
+ exc_info=exc,
133
+ )
126
134
  rows.append(_status_row("MCP", "error", f"Failed to load MCP config: {exc}"))
127
135
  return rows, errors
128
136
 
@@ -153,8 +161,12 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
153
161
  return _status_row(
154
162
  "Project config", "ok", f".ripperdoc/config.json loaded for {project_path}"
155
163
  )
156
- except Exception as exc: # pragma: no cover - defensive
157
- logger.exception("[doctor] Failed to load project config", exc_info=exc)
164
+ except (OSError, IOError, json.JSONDecodeError, ValueError, TypeError) as exc: # pragma: no cover - defensive
165
+ logger.warning(
166
+ "[doctor] Failed to load project config: %s: %s",
167
+ type(exc).__name__, exc,
168
+ exc_info=exc,
169
+ )
158
170
  return _status_row(
159
171
  "Project config", "warn", f"Could not read .ripperdoc/config.json: {exc}"
160
172
  )
@@ -179,7 +191,8 @@ def _handle(ui: Any, _: str) -> bool:
179
191
  project_row = _project_status(project_path)
180
192
  results.append(project_row)
181
193
 
182
- mcp_rows, mcp_errors = _mcp_status(project_path)
194
+ runner = getattr(ui, "run_async", None)
195
+ mcp_rows, mcp_errors = _mcp_status(project_path, runner=runner)
183
196
  results.extend(mcp_rows)
184
197
  results.append(_sandbox_status())
185
198
 
@@ -12,7 +12,7 @@ command = SlashCommand(
12
12
  name="exit",
13
13
  description="Exit Ripperdoc",
14
14
  handler=_handle,
15
- aliases=("quit",),
15
+ aliases=(),
16
16
  )
17
17
 
18
18
 
@@ -1,21 +1,26 @@
1
- import asyncio
2
-
3
1
  from rich.markup import escape
4
2
 
5
- from ripperdoc.utils.mcp import load_mcp_servers_async, shutdown_mcp_runtime
3
+ from ripperdoc.utils.mcp import load_mcp_servers_async
6
4
 
7
5
  from typing import Any
8
6
  from .base import SlashCommand
9
7
 
10
8
 
9
+ def _run_in_ui(ui: Any, coro: Any) -> Any:
10
+ runner = getattr(ui, "run_async", None)
11
+ if callable(runner):
12
+ return runner(coro)
13
+ # Fallback for non-UI contexts.
14
+ import asyncio
15
+
16
+ return asyncio.run(coro)
17
+
18
+
11
19
  def _handle(ui: Any, _: str) -> bool:
12
20
  async def _load() -> list:
13
- try:
14
- return await load_mcp_servers_async(ui.project_path)
15
- finally:
16
- await shutdown_mcp_runtime()
21
+ return await load_mcp_servers_async(ui.project_path)
17
22
 
18
- servers = asyncio.run(_load())
23
+ servers = _run_in_ui(ui, _load())
19
24
  if not servers:
20
25
  ui.console.print(
21
26
  "[yellow]No MCP servers configured. Add servers to ~/.ripperdoc/mcp.json, ~/.mcp.json, or a project .mcp.json file.[/yellow]"
@@ -26,14 +26,14 @@ def _shorten_path(path: Path, project_path: Path) -> str:
26
26
  """Return a short, user-friendly path."""
27
27
  try:
28
28
  return str(path.resolve().relative_to(project_path.resolve()))
29
- except Exception:
29
+ except (ValueError, OSError):
30
30
  pass
31
31
 
32
32
  home = Path.home()
33
33
  try:
34
34
  rel_home = path.resolve().relative_to(home)
35
35
  return f"~/{rel_home}"
36
- except Exception:
36
+ except (ValueError, OSError):
37
37
  return str(path)
38
38
 
39
39
 
@@ -77,7 +77,7 @@ def _ensure_gitignore_entry(project_path: Path, entry: str) -> bool:
77
77
  f.write("\n")
78
78
  f.write(f"{entry}\n")
79
79
  return True
80
- except Exception:
80
+ except (OSError, IOError):
81
81
  return False
82
82
 
83
83
 
@@ -116,7 +116,7 @@ def _open_in_editor(path: Path, console: Any) -> bool:
116
116
  except FileNotFoundError:
117
117
  console.print(f"[red]Editor command not found: {escape(editor_cmd[0])}[/red]")
118
118
  return False
119
- except Exception as exc: # pragma: no cover - best-effort logging
119
+ except (OSError, subprocess.SubprocessError) as exc: # pragma: no cover - best-effort logging
120
120
  console.print(f"[red]Failed to launch editor: {escape(str(exc))}[/red]")
121
121
  return False
122
122
 
@@ -195,7 +195,7 @@ command = SlashCommand(
195
195
  name="memory",
196
196
  description="List and edit AGENTS memory files",
197
197
  handler=_handle,
198
- aliases=("mem",),
198
+ aliases=(),
199
199
  )
200
200
 
201
201
 
@@ -35,6 +35,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
35
35
  console.print("[bold]/models edit <name>[/bold] — edit an existing model profile")
36
36
  console.print("[bold]/models delete <name>[/bold] — delete a model profile")
37
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)")
38
39
 
39
40
  def parse_int(prompt_text: str, default_value: Optional[int]) -> Optional[int]:
40
41
  raw = console.input(prompt_text).strip()
@@ -181,10 +182,11 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
181
182
  overwrite=overwrite,
182
183
  set_as_main=set_as_main,
183
184
  )
184
- except Exception as exc:
185
+ except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
185
186
  console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
186
- logger.exception(
187
- "[models_cmd] Failed to save model profile",
187
+ logger.warning(
188
+ "[models_cmd] Failed to save model profile: %s: %s",
189
+ type(exc).__name__, exc,
188
190
  extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
189
191
  )
190
192
  return True
@@ -289,10 +291,11 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
289
291
  overwrite=True,
290
292
  set_as_main=False,
291
293
  )
292
- except Exception as exc:
294
+ except (OSError, IOError, ValueError, TypeError, PermissionError) as exc:
293
295
  console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
294
- logger.exception(
295
- "[models_cmd] Failed to update model profile",
296
+ logger.warning(
297
+ "[models_cmd] Failed to update model profile: %s: %s",
298
+ type(exc).__name__, exc,
296
299
  extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
297
300
  )
298
301
  return True
@@ -311,30 +314,58 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
311
314
  console.print(f"[green]✓ Deleted model '{escape(target)}'[/green]")
312
315
  except KeyError as exc:
313
316
  console.print(f"[yellow]{escape(str(exc))}[/yellow]")
314
- except Exception as exc:
317
+ except (OSError, IOError, PermissionError) as exc:
315
318
  console.print(f"[red]Failed to delete model: {escape(str(exc))}[/red]")
316
319
  print_models_usage()
317
- logger.exception(
318
- "[models_cmd] Failed to delete model profile",
320
+ logger.warning(
321
+ "[models_cmd] Failed to delete model profile: %s: %s",
322
+ type(exc).__name__, exc,
319
323
  extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
320
324
  )
321
325
  return True
322
326
 
323
327
  if subcmd in ("use", "main", "set-main"):
324
- target = tokens[1] if len(tokens) > 1 else console.input("Model to use as main: ").strip()
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
+
325
355
  if not target:
326
356
  console.print("[red]Model name is required.[/red]")
327
357
  print_models_usage()
328
358
  return True
329
359
  try:
330
- set_model_pointer("main", target)
331
- console.print(f"[green]✓ Main model set to '{escape(target)}'[/green]")
332
- except Exception as exc:
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:
333
363
  console.print(f"[red]{escape(str(exc))}[/red]")
334
364
  print_models_usage()
335
- logger.exception(
336
- "[models_cmd] Failed to set main model pointer",
337
- extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
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)},
338
369
  )
339
370
  return True
340
371