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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +257 -123
- ripperdoc/cli/commands/__init__.py +2 -1
- ripperdoc/cli/commands/agents_cmd.py +138 -8
- ripperdoc/cli/commands/clear_cmd.py +9 -4
- ripperdoc/cli/commands/config_cmd.py +1 -1
- ripperdoc/cli/commands/context_cmd.py +3 -2
- ripperdoc/cli/commands/doctor_cmd.py +18 -4
- ripperdoc/cli/commands/exit_cmd.py +1 -0
- ripperdoc/cli/commands/hooks_cmd.py +27 -53
- ripperdoc/cli/commands/models_cmd.py +27 -10
- ripperdoc/cli/commands/permissions_cmd.py +27 -9
- ripperdoc/cli/commands/resume_cmd.py +9 -3
- ripperdoc/cli/commands/stats_cmd.py +244 -0
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +2 -1
- ripperdoc/cli/ui/interrupt_handler.py +2 -3
- ripperdoc/cli/ui/message_display.py +4 -2
- ripperdoc/cli/ui/panels.py +1 -0
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +403 -81
- ripperdoc/cli/ui/spinner.py +54 -18
- ripperdoc/cli/ui/thinking_spinner.py +1 -2
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +213 -0
- ripperdoc/core/agents.py +19 -6
- ripperdoc/core/config.py +51 -17
- ripperdoc/core/custom_commands.py +7 -6
- ripperdoc/core/default_tools.py +101 -12
- ripperdoc/core/hooks/config.py +1 -3
- ripperdoc/core/hooks/events.py +27 -28
- ripperdoc/core/hooks/executor.py +4 -6
- ripperdoc/core/hooks/integration.py +12 -21
- ripperdoc/core/hooks/llm_callback.py +59 -0
- ripperdoc/core/hooks/manager.py +40 -15
- ripperdoc/core/permissions.py +118 -12
- ripperdoc/core/providers/anthropic.py +109 -36
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +89 -24
- ripperdoc/core/query.py +273 -68
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +17 -8
- ripperdoc/sdk/client.py +79 -4
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +307 -135
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +63 -24
- ripperdoc/tools/dynamic_mcp_tool.py +29 -8
- ripperdoc/tools/enter_plan_mode_tool.py +1 -1
- ripperdoc/tools/exit_plan_mode_tool.py +1 -1
- ripperdoc/tools/file_edit_tool.py +167 -54
- ripperdoc/tools/file_read_tool.py +28 -4
- ripperdoc/tools/file_write_tool.py +13 -10
- ripperdoc/tools/glob_tool.py +3 -2
- ripperdoc/tools/grep_tool.py +3 -2
- ripperdoc/tools/kill_bash_tool.py +1 -1
- ripperdoc/tools/ls_tool.py +1 -1
- ripperdoc/tools/lsp_tool.py +615 -0
- ripperdoc/tools/mcp_tools.py +13 -10
- ripperdoc/tools/multi_edit_tool.py +8 -7
- ripperdoc/tools/notebook_edit_tool.py +7 -4
- ripperdoc/tools/skill_tool.py +1 -1
- ripperdoc/tools/task_tool.py +519 -69
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +9 -5
- ripperdoc/utils/file_watch.py +214 -5
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/lsp.py +806 -0
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +14 -7
- ripperdoc/utils/messages.py +126 -67
- ripperdoc/utils/path_ignore.py +35 -8
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/permissions/tool_permission_utils.py +174 -15
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_heatmap.py +244 -0
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/session_stats.py +293 -0
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/METADATA +8 -2
- ripperdoc-0.2.10.dist-info/RECORD +129 -0
- ripperdoc-0.2.8.dist-info/RECORD +0 -121
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.8.dist-info → ripperdoc-0.2.10.dist-info}/top_level.txt +0 -0
ripperdoc/core/skills.py
CHANGED
|
@@ -84,10 +84,15 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
|
|
|
84
84
|
body = "\n".join(lines[idx + 1 :])
|
|
85
85
|
try:
|
|
86
86
|
frontmatter = yaml.safe_load(frontmatter_text) or {}
|
|
87
|
-
except (
|
|
87
|
+
except (
|
|
88
|
+
yaml.YAMLError,
|
|
89
|
+
ValueError,
|
|
90
|
+
TypeError,
|
|
91
|
+
) as exc: # pragma: no cover - defensive
|
|
88
92
|
logger.warning(
|
|
89
93
|
"[skills] Invalid frontmatter in SKILL.md: %s: %s",
|
|
90
|
-
type(exc).__name__,
|
|
94
|
+
type(exc).__name__,
|
|
95
|
+
exc,
|
|
91
96
|
)
|
|
92
97
|
return {"__error__": f"Invalid frontmatter: {exc}"}, body
|
|
93
98
|
return frontmatter, body
|
|
@@ -118,7 +123,8 @@ def _load_skill_file(
|
|
|
118
123
|
except (OSError, IOError, UnicodeDecodeError) as exc:
|
|
119
124
|
logger.warning(
|
|
120
125
|
"[skills] Failed to read skill file: %s: %s",
|
|
121
|
-
type(exc).__name__,
|
|
126
|
+
type(exc).__name__,
|
|
127
|
+
exc,
|
|
122
128
|
extra={"path": str(path)},
|
|
123
129
|
)
|
|
124
130
|
return None, SkillLoadError(path=path, reason=f"Failed to read file: {exc}")
|
ripperdoc/core/system_prompt.py
CHANGED
|
@@ -49,7 +49,8 @@ def _detect_git_repo(cwd: Path) -> bool:
|
|
|
49
49
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
50
50
|
logger.warning(
|
|
51
51
|
"[system_prompt] Failed to detect git repository: %s: %s",
|
|
52
|
-
type(exc).__name__,
|
|
52
|
+
type(exc).__name__,
|
|
53
|
+
exc,
|
|
53
54
|
extra={"cwd": str(cwd)},
|
|
54
55
|
)
|
|
55
56
|
return False
|
|
@@ -393,7 +394,8 @@ def build_system_prompt(
|
|
|
393
394
|
except (OSError, ValueError, RuntimeError) as exc:
|
|
394
395
|
logger.warning(
|
|
395
396
|
"Failed to load agent definitions: %s: %s",
|
|
396
|
-
type(exc).__name__,
|
|
397
|
+
type(exc).__name__,
|
|
398
|
+
exc,
|
|
397
399
|
)
|
|
398
400
|
agent_section = (
|
|
399
401
|
"# Subagents\nTask tool available, but agent definitions could not be loaded."
|
ripperdoc/core/tool.py
CHANGED
|
@@ -8,7 +8,7 @@ import json
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, TypeVar, Generic, Union
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field, SkipValidation
|
|
11
|
-
from ripperdoc.utils.file_watch import
|
|
11
|
+
from ripperdoc.utils.file_watch import FileCacheType
|
|
12
12
|
from ripperdoc.utils.log import get_logger
|
|
13
13
|
|
|
14
14
|
|
|
@@ -37,13 +37,20 @@ class ToolUseContext(BaseModel):
|
|
|
37
37
|
|
|
38
38
|
message_id: Optional[str] = None
|
|
39
39
|
agent_id: Optional[str] = None
|
|
40
|
-
|
|
40
|
+
yolo_mode: bool = False
|
|
41
41
|
verbose: bool = False
|
|
42
42
|
permission_checker: Optional[Any] = None
|
|
43
43
|
read_file_timestamps: Dict[str, float] = Field(default_factory=dict)
|
|
44
|
-
# SkipValidation prevents Pydantic from copying the
|
|
45
|
-
# ensuring Read and Edit tools share the same cache instance
|
|
46
|
-
|
|
44
|
+
# SkipValidation prevents Pydantic from copying the cache during validation,
|
|
45
|
+
# ensuring Read and Edit tools share the same cache instance.
|
|
46
|
+
# FileCacheType supports both Dict[str, FileSnapshot] and BoundedFileCache.
|
|
47
|
+
file_state_cache: Annotated[FileCacheType, SkipValidation] = Field(
|
|
48
|
+
default_factory=dict
|
|
49
|
+
)
|
|
50
|
+
conversation_messages: Annotated[Optional[List[Any]], SkipValidation] = Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="Full conversation history for tools that need parent context.",
|
|
53
|
+
)
|
|
47
54
|
tool_registry: Optional[Any] = None
|
|
48
55
|
abort_signal: Optional[Any] = None
|
|
49
56
|
# UI control callbacks for tools that need user interaction
|
|
@@ -110,7 +117,7 @@ class Tool(ABC, Generic[TInput, TOutput]):
|
|
|
110
117
|
pass
|
|
111
118
|
|
|
112
119
|
@abstractmethod
|
|
113
|
-
async def prompt(self,
|
|
120
|
+
async def prompt(self, yolo_mode: bool = False) -> str:
|
|
114
121
|
"""Get the system prompt for this tool."""
|
|
115
122
|
pass
|
|
116
123
|
|
|
@@ -213,7 +220,8 @@ async def build_tool_description(
|
|
|
213
220
|
except (TypeError, ValueError, AttributeError, KeyError) as exc:
|
|
214
221
|
logger.warning(
|
|
215
222
|
"[tool] Failed to build input example section: %s: %s",
|
|
216
|
-
type(exc).__name__,
|
|
223
|
+
type(exc).__name__,
|
|
224
|
+
exc,
|
|
217
225
|
extra={"tool": getattr(tool, "name", None)},
|
|
218
226
|
)
|
|
219
227
|
return description_text
|
|
@@ -233,7 +241,8 @@ def tool_input_examples(tool: Tool[Any, Any], limit: int = 5) -> List[Dict[str,
|
|
|
233
241
|
except (TypeError, ValueError, AttributeError) as exc:
|
|
234
242
|
logger.warning(
|
|
235
243
|
"[tool] Failed to format tool input example: %s: %s",
|
|
236
|
-
type(exc).__name__,
|
|
244
|
+
type(exc).__name__,
|
|
245
|
+
exc,
|
|
237
246
|
extra={"tool": getattr(tool, "name", None)},
|
|
238
247
|
)
|
|
239
248
|
continue
|
ripperdoc/sdk/client.py
CHANGED
|
@@ -8,6 +8,8 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import os
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
11
13
|
from dataclasses import dataclass, field
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import (
|
|
@@ -24,6 +26,8 @@ from typing import (
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
from ripperdoc.core.default_tools import get_default_tools
|
|
29
|
+
from ripperdoc.core.hooks.llm_callback import build_hook_llm_callback
|
|
30
|
+
from ripperdoc.core.hooks.manager import hook_manager
|
|
27
31
|
from ripperdoc.core.query import QueryContext, query as _core_query
|
|
28
32
|
from ripperdoc.core.permissions import PermissionResult
|
|
29
33
|
from ripperdoc.core.system_prompt import build_system_prompt
|
|
@@ -36,6 +40,7 @@ from ripperdoc.utils.messages import (
|
|
|
36
40
|
AssistantMessage,
|
|
37
41
|
ProgressMessage,
|
|
38
42
|
UserMessage,
|
|
43
|
+
create_assistant_message,
|
|
39
44
|
create_user_message,
|
|
40
45
|
)
|
|
41
46
|
from ripperdoc.utils.mcp import (
|
|
@@ -83,7 +88,7 @@ class RipperdocOptions:
|
|
|
83
88
|
tools: Optional[Sequence[Tool[Any, Any]]] = None
|
|
84
89
|
allowed_tools: Optional[Sequence[str]] = None
|
|
85
90
|
disallowed_tools: Optional[Sequence[str]] = None
|
|
86
|
-
|
|
91
|
+
yolo_mode: bool = False
|
|
87
92
|
verbose: bool = False
|
|
88
93
|
model: str = "main"
|
|
89
94
|
max_thinking_tokens: int = 0
|
|
@@ -159,6 +164,10 @@ class RipperdocClient:
|
|
|
159
164
|
self._current_context: Optional[QueryContext] = None
|
|
160
165
|
self._connected = False
|
|
161
166
|
self._previous_cwd: Optional[Path] = None
|
|
167
|
+
self._session_hook_contexts: List[str] = []
|
|
168
|
+
self._session_id: Optional[str] = None
|
|
169
|
+
self._session_start_time: Optional[float] = None
|
|
170
|
+
self._session_end_sent: bool = False
|
|
162
171
|
|
|
163
172
|
@property
|
|
164
173
|
def tools(self) -> List[Tool[Any, Any]]:
|
|
@@ -182,6 +191,22 @@ class RipperdocClient:
|
|
|
182
191
|
self._previous_cwd = Path.cwd()
|
|
183
192
|
os.chdir(_coerce_to_path(self.options.cwd))
|
|
184
193
|
self._connected = True
|
|
194
|
+
project_path = _coerce_to_path(self.options.cwd or Path.cwd())
|
|
195
|
+
hook_manager.set_project_dir(project_path)
|
|
196
|
+
self._session_id = self._session_id or str(uuid.uuid4())
|
|
197
|
+
hook_manager.set_session_id(self._session_id)
|
|
198
|
+
hook_manager.set_llm_callback(build_hook_llm_callback())
|
|
199
|
+
try:
|
|
200
|
+
result = await hook_manager.run_session_start_async("startup")
|
|
201
|
+
self._session_hook_contexts = self._collect_hook_contexts(result)
|
|
202
|
+
self._session_start_time = time.time()
|
|
203
|
+
self._session_end_sent = False
|
|
204
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
205
|
+
logger.warning(
|
|
206
|
+
"[sdk] SessionStart hook failed: %s: %s",
|
|
207
|
+
type(exc).__name__,
|
|
208
|
+
exc,
|
|
209
|
+
)
|
|
185
210
|
|
|
186
211
|
if prompt:
|
|
187
212
|
await self.query(prompt)
|
|
@@ -203,6 +228,25 @@ class RipperdocClient:
|
|
|
203
228
|
self._previous_cwd = None
|
|
204
229
|
|
|
205
230
|
self._connected = False
|
|
231
|
+
if not self._session_end_sent:
|
|
232
|
+
duration = (
|
|
233
|
+
max(time.time() - self._session_start_time, 0.0)
|
|
234
|
+
if self._session_start_time is not None
|
|
235
|
+
else None
|
|
236
|
+
)
|
|
237
|
+
try:
|
|
238
|
+
await hook_manager.run_session_end_async(
|
|
239
|
+
"other",
|
|
240
|
+
duration_seconds=duration,
|
|
241
|
+
message_count=len(self._history),
|
|
242
|
+
)
|
|
243
|
+
except (OSError, RuntimeError, ConnectionError, ValueError, TypeError) as exc:
|
|
244
|
+
logger.warning(
|
|
245
|
+
"[sdk] SessionEnd hook failed: %s: %s",
|
|
246
|
+
type(exc).__name__,
|
|
247
|
+
exc,
|
|
248
|
+
)
|
|
249
|
+
self._session_end_sent = True
|
|
206
250
|
await shutdown_mcp_runtime()
|
|
207
251
|
|
|
208
252
|
async def query(self, prompt: str) -> None:
|
|
@@ -217,17 +261,32 @@ class RipperdocClient:
|
|
|
217
261
|
|
|
218
262
|
self._queue = asyncio.Queue()
|
|
219
263
|
|
|
264
|
+
hook_result = await hook_manager.run_user_prompt_submit_async(prompt)
|
|
265
|
+
if hook_result.should_block or not hook_result.should_continue:
|
|
266
|
+
reason = (
|
|
267
|
+
hook_result.block_reason
|
|
268
|
+
or hook_result.stop_reason
|
|
269
|
+
or "Prompt blocked by hook."
|
|
270
|
+
)
|
|
271
|
+
blocked_message = create_assistant_message(str(reason))
|
|
272
|
+
self._history.append(blocked_message)
|
|
273
|
+
await self._queue.put(blocked_message)
|
|
274
|
+
await self._queue.put(_END_OF_STREAM)
|
|
275
|
+
self._current_task = asyncio.create_task(asyncio.sleep(0))
|
|
276
|
+
return
|
|
277
|
+
hook_instructions = self._collect_hook_contexts(hook_result)
|
|
278
|
+
|
|
220
279
|
user_message = create_user_message(prompt)
|
|
221
280
|
history = list(self._history) + [user_message]
|
|
222
281
|
self._history.append(user_message)
|
|
223
282
|
|
|
224
|
-
system_prompt = await self._build_system_prompt(prompt)
|
|
283
|
+
system_prompt = await self._build_system_prompt(prompt, hook_instructions)
|
|
225
284
|
context = dict(self.options.context)
|
|
226
285
|
|
|
227
286
|
query_context = QueryContext(
|
|
228
287
|
tools=self._tools,
|
|
229
288
|
max_thinking_tokens=self.options.max_thinking_tokens,
|
|
230
|
-
|
|
289
|
+
yolo_mode=self.options.yolo_mode,
|
|
231
290
|
model=self.options.model,
|
|
232
291
|
verbose=self.options.verbose,
|
|
233
292
|
)
|
|
@@ -280,7 +339,9 @@ class RipperdocClient:
|
|
|
280
339
|
|
|
281
340
|
await self._queue.put(_END_OF_STREAM)
|
|
282
341
|
|
|
283
|
-
async def _build_system_prompt(
|
|
342
|
+
async def _build_system_prompt(
|
|
343
|
+
self, user_prompt: str, hook_instructions: Optional[List[str]] = None
|
|
344
|
+
) -> str:
|
|
284
345
|
if self.options.system_prompt:
|
|
285
346
|
return self.options.system_prompt
|
|
286
347
|
|
|
@@ -299,6 +360,10 @@ class RipperdocClient:
|
|
|
299
360
|
memory = build_memory_instructions()
|
|
300
361
|
if memory:
|
|
301
362
|
instructions.append(memory)
|
|
363
|
+
if self._session_hook_contexts:
|
|
364
|
+
instructions.extend(self._session_hook_contexts)
|
|
365
|
+
if hook_instructions:
|
|
366
|
+
instructions.extend([text for text in hook_instructions if text])
|
|
302
367
|
|
|
303
368
|
dynamic_tools = await load_dynamic_mcp_tools_async(project_path)
|
|
304
369
|
if dynamic_tools:
|
|
@@ -315,6 +380,16 @@ class RipperdocClient:
|
|
|
315
380
|
mcp_instructions=mcp_instructions,
|
|
316
381
|
)
|
|
317
382
|
|
|
383
|
+
def _collect_hook_contexts(self, hook_result: Any) -> List[str]:
|
|
384
|
+
contexts: List[str] = []
|
|
385
|
+
system_message = getattr(hook_result, "system_message", None)
|
|
386
|
+
additional_context = getattr(hook_result, "additional_context", None)
|
|
387
|
+
if system_message:
|
|
388
|
+
contexts.append(str(system_message))
|
|
389
|
+
if additional_context:
|
|
390
|
+
contexts.append(str(additional_context))
|
|
391
|
+
return contexts
|
|
392
|
+
|
|
318
393
|
|
|
319
394
|
async def query(
|
|
320
395
|
prompt: str,
|
|
@@ -228,7 +228,8 @@ async def prompt_user_for_answer(
|
|
|
228
228
|
except (OSError, RuntimeError, ValueError) as e:
|
|
229
229
|
logger.warning(
|
|
230
230
|
"[ask_user_question_tool] Error during prompt: %s: %s",
|
|
231
|
-
type(e).__name__,
|
|
231
|
+
type(e).__name__,
|
|
232
|
+
e,
|
|
232
233
|
)
|
|
233
234
|
return None
|
|
234
235
|
|
|
@@ -275,7 +276,7 @@ class AskUserQuestionTool(Tool[AskUserQuestionToolInput, AskUserQuestionToolOutp
|
|
|
275
276
|
def input_schema(self) -> type[AskUserQuestionToolInput]:
|
|
276
277
|
return AskUserQuestionToolInput
|
|
277
278
|
|
|
278
|
-
async def prompt(self,
|
|
279
|
+
async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
|
|
279
280
|
return ASK_USER_QUESTION_PROMPT
|
|
280
281
|
|
|
281
282
|
def user_facing_name(self) -> str:
|
|
@@ -410,7 +411,8 @@ class AskUserQuestionTool(Tool[AskUserQuestionToolInput, AskUserQuestionToolOutp
|
|
|
410
411
|
except (OSError, RuntimeError, ValueError, KeyError) as exc:
|
|
411
412
|
logger.warning(
|
|
412
413
|
"[ask_user_question_tool] Error collecting answers: %s: %s",
|
|
413
|
-
type(exc).__name__,
|
|
414
|
+
type(exc).__name__,
|
|
415
|
+
exc,
|
|
414
416
|
)
|
|
415
417
|
output = AskUserQuestionToolOutput(
|
|
416
418
|
questions=questions,
|