ripperdoc 0.2.10__py3-none-any.whl → 0.3.0__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 +164 -57
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +3 -7
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +61 -5
- ripperdoc/cli/commands/resume_cmd.py +1 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +4 -4
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +13 -8
- ripperdoc/cli/ui/rich_ui.py +451 -32
- ripperdoc/cli/ui/spinner.py +68 -5
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +18 -11
- ripperdoc/core/agents.py +4 -0
- ripperdoc/core/config.py +235 -0
- ripperdoc/core/default_tools.py +1 -0
- ripperdoc/core/hooks/llm_callback.py +0 -1
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +82 -5
- ripperdoc/core/providers/openai.py +55 -9
- ripperdoc/core/query.py +349 -108
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +8 -3
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +49 -5
- ripperdoc/tools/bash_tool.py +75 -9
- ripperdoc/tools/file_edit_tool.py +98 -29
- ripperdoc/tools/file_read_tool.py +139 -8
- ripperdoc/tools/file_write_tool.py +46 -3
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +9 -15
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +33 -8
- ripperdoc/utils/file_watch.py +12 -6
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +9 -3
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +2 -2
- ripperdoc/utils/messages.py +177 -32
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +9 -3
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +1 -3
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +1 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +49 -17
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/RECORD +68 -61
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -408
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/core/permissions.py
CHANGED
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Awaitable, Callable, Optional, Set
|
|
9
|
+
from typing import Any, Awaitable, Callable, Optional, Set, TYPE_CHECKING, TYPE_CHECKING as TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from ripperdoc.core.config import config_manager
|
|
12
12
|
from ripperdoc.core.hooks.manager import hook_manager
|
|
@@ -14,6 +14,10 @@ from ripperdoc.core.tool import Tool
|
|
|
14
14
|
from ripperdoc.utils.permissions import PermissionDecision, ToolRule
|
|
15
15
|
from ripperdoc.utils.log import get_logger
|
|
16
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from prompt_toolkit import PromptSession
|
|
20
|
+
|
|
17
21
|
logger = get_logger()
|
|
18
22
|
|
|
19
23
|
|
|
@@ -89,7 +93,7 @@ def permission_key(tool: Tool[Any, Any], parsed_input: Any) -> str:
|
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def _render_options_prompt(prompt: str, options: list[tuple[str, str]]) -> str:
|
|
92
|
-
"""Render a simple numbered prompt."""
|
|
96
|
+
"""Render a simple numbered prompt (fallback for non-Rich environments)."""
|
|
93
97
|
border = "─" * 120
|
|
94
98
|
lines = [border, prompt, ""]
|
|
95
99
|
for idx, (_, label) in enumerate(options, start=1):
|
|
@@ -101,6 +105,42 @@ def _render_options_prompt(prompt: str, options: list[tuple[str, str]]) -> str:
|
|
|
101
105
|
return "\n".join(lines)
|
|
102
106
|
|
|
103
107
|
|
|
108
|
+
def _render_options_prompt_rich(
|
|
109
|
+
console: "Console",
|
|
110
|
+
prompt: str,
|
|
111
|
+
options: list[tuple[str, str]]
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Render permission dialog using Rich Panel for better visual consistency."""
|
|
114
|
+
from rich.panel import Panel
|
|
115
|
+
from rich.text import Text
|
|
116
|
+
|
|
117
|
+
# Build option lines with markup
|
|
118
|
+
option_lines = []
|
|
119
|
+
for idx, (_, label) in enumerate(options, start=1):
|
|
120
|
+
prefix = "[cyan]❯[/cyan]" if idx == 1 else " "
|
|
121
|
+
option_lines.append(f"{prefix} {idx}. {label}")
|
|
122
|
+
|
|
123
|
+
numeric_choices = "/".join(str(i) for i in range(1, len(options) + 1))
|
|
124
|
+
shortcut_choices = "/".join(opt[0] for opt in options)
|
|
125
|
+
|
|
126
|
+
# Build the prompt content as a markup string
|
|
127
|
+
markup_content = f"{prompt}\n\n" + "\n".join(option_lines) + "\n"
|
|
128
|
+
markup_content += f"Choice ([cyan]{numeric_choices}[/cyan] or [cyan]{shortcut_choices}[/cyan]): "
|
|
129
|
+
|
|
130
|
+
# Parse markup to create a Text object
|
|
131
|
+
content = Text.from_markup(markup_content)
|
|
132
|
+
|
|
133
|
+
# Render the panel
|
|
134
|
+
panel = Panel(
|
|
135
|
+
content,
|
|
136
|
+
title=Text.from_markup("[yellow]Permission Required[/yellow]"),
|
|
137
|
+
title_align="left",
|
|
138
|
+
border_style="yellow",
|
|
139
|
+
padding=(0, 1),
|
|
140
|
+
)
|
|
141
|
+
console.print(panel)
|
|
142
|
+
|
|
143
|
+
|
|
104
144
|
def _rule_strings(rule_suggestions: Optional[Any]) -> list[str]:
|
|
105
145
|
"""Normalize rule suggestions to simple strings."""
|
|
106
146
|
if not rule_suggestions:
|
|
@@ -118,9 +158,18 @@ def make_permission_checker(
|
|
|
118
158
|
project_path: Path,
|
|
119
159
|
yolo_mode: bool,
|
|
120
160
|
prompt_fn: Optional[Callable[[str], str]] = None,
|
|
161
|
+
console: Optional["Console"] = None,
|
|
162
|
+
prompt_session: Optional["PromptSession"] = None,
|
|
121
163
|
) -> Callable[[Tool[Any, Any], Any], Awaitable[PermissionResult]]:
|
|
122
164
|
"""Create a permission checking function for the current project.
|
|
123
165
|
|
|
166
|
+
Args:
|
|
167
|
+
project_path: Path to the project directory
|
|
168
|
+
yolo_mode: If True, all tool calls are allowed without prompting
|
|
169
|
+
prompt_fn: Optional function to use for prompting (defaults to input())
|
|
170
|
+
console: Optional Rich console for rich permission dialogs
|
|
171
|
+
prompt_session: Optional PromptSession for better interrupt handling
|
|
172
|
+
|
|
124
173
|
In yolo mode, all tool calls are allowed without prompting.
|
|
125
174
|
"""
|
|
126
175
|
|
|
@@ -131,13 +180,41 @@ def make_permission_checker(
|
|
|
131
180
|
session_tool_rules: dict[str, Set[str]] = defaultdict(set)
|
|
132
181
|
|
|
133
182
|
async def _prompt_user(prompt: str, options: list[tuple[str, str]]) -> str:
|
|
134
|
-
"""Prompt the user
|
|
183
|
+
"""Prompt the user with proper interrupt handling."""
|
|
184
|
+
# Build the prompt message
|
|
185
|
+
if console is not None:
|
|
186
|
+
# Use Rich Panel for the dialog
|
|
187
|
+
_render_options_prompt_rich(console, prompt, options)
|
|
188
|
+
# Build simple prompt for the input line
|
|
189
|
+
numeric_choices = "/".join(str(i) for i in range(1, len(options) + 1))
|
|
190
|
+
shortcut_choices = "/".join(opt[0] for opt in options)
|
|
191
|
+
input_prompt = f"Choice ({numeric_choices} or {shortcut_choices}): "
|
|
192
|
+
else:
|
|
193
|
+
# Use plain text rendering
|
|
194
|
+
rendered = _render_options_prompt(prompt, options)
|
|
195
|
+
input_prompt = rendered
|
|
196
|
+
|
|
197
|
+
# Try to use PromptSession if available (better interrupt handling)
|
|
198
|
+
if prompt_session is not None:
|
|
199
|
+
try:
|
|
200
|
+
# PromptSession.prompt() can handle Ctrl+C gracefully
|
|
201
|
+
return await prompt_session.prompt_async(input_prompt)
|
|
202
|
+
except KeyboardInterrupt:
|
|
203
|
+
logger.debug("[permissions] KeyboardInterrupt in prompt_session")
|
|
204
|
+
return "n"
|
|
205
|
+
except EOFError:
|
|
206
|
+
logger.debug("[permissions] EOFError in prompt_session")
|
|
207
|
+
return "n"
|
|
208
|
+
|
|
209
|
+
# Fallback to simple input() via executor
|
|
135
210
|
loop = asyncio.get_running_loop()
|
|
136
211
|
responder = prompt_fn or input
|
|
137
212
|
|
|
138
213
|
def _ask() -> str:
|
|
139
|
-
|
|
140
|
-
|
|
214
|
+
try:
|
|
215
|
+
return responder(input_prompt)
|
|
216
|
+
except (KeyboardInterrupt, EOFError):
|
|
217
|
+
return "n"
|
|
141
218
|
|
|
142
219
|
return await loop.run_in_executor(None, _ask)
|
|
143
220
|
|
|
@@ -80,10 +80,18 @@ def _effort_from_tokens(max_thinking_tokens: int) -> Optional[str]:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def _detect_openai_vendor(model_profile: ModelProfile) -> str:
|
|
83
|
-
"""Best-effort vendor hint for OpenAI-compatible endpoints.
|
|
83
|
+
"""Best-effort vendor hint for OpenAI-compatible endpoints.
|
|
84
|
+
|
|
85
|
+
If thinking_mode is explicitly set to "none" or "disabled", returns "none"
|
|
86
|
+
to skip all thinking protocol handling.
|
|
87
|
+
"""
|
|
84
88
|
override = getattr(model_profile, "thinking_mode", None)
|
|
85
89
|
if isinstance(override, str) and override.strip():
|
|
86
|
-
|
|
90
|
+
mode = override.strip().lower()
|
|
91
|
+
# Allow explicit disable of thinking protocol
|
|
92
|
+
if mode in ("disabled", "off"):
|
|
93
|
+
return "none"
|
|
94
|
+
return mode
|
|
87
95
|
base = (model_profile.api_base or "").lower()
|
|
88
96
|
name = (model_profile.model or "").lower()
|
|
89
97
|
if "openrouter.ai" in base:
|
|
@@ -106,21 +114,25 @@ def _build_thinking_kwargs(
|
|
|
106
114
|
extra_body: Dict[str, Any] = {}
|
|
107
115
|
top_level: Dict[str, Any] = {}
|
|
108
116
|
vendor = _detect_openai_vendor(model_profile)
|
|
117
|
+
|
|
118
|
+
# Skip thinking protocol if explicitly disabled
|
|
119
|
+
if vendor == "none":
|
|
120
|
+
return extra_body, top_level
|
|
121
|
+
|
|
109
122
|
effort = _effort_from_tokens(max_thinking_tokens)
|
|
110
123
|
|
|
111
124
|
if vendor == "deepseek":
|
|
112
125
|
if max_thinking_tokens != 0:
|
|
113
126
|
extra_body["thinking"] = {"type": "enabled"}
|
|
114
127
|
elif vendor == "qwen":
|
|
128
|
+
# Only send enable_thinking when explicitly enabling thinking mode
|
|
129
|
+
# Some qwen-compatible APIs don't support this parameter
|
|
115
130
|
if max_thinking_tokens > 0:
|
|
116
131
|
extra_body["enable_thinking"] = True
|
|
117
|
-
elif max_thinking_tokens == 0:
|
|
118
|
-
extra_body["enable_thinking"] = False
|
|
119
132
|
elif vendor == "openrouter":
|
|
133
|
+
# Only send reasoning when explicitly enabling thinking mode
|
|
120
134
|
if max_thinking_tokens > 0:
|
|
121
135
|
extra_body["reasoning"] = {"max_tokens": max_thinking_tokens}
|
|
122
|
-
elif max_thinking_tokens == 0:
|
|
123
|
-
extra_body["reasoning"] = {"effort": "none"}
|
|
124
136
|
elif vendor == "gemini_openai":
|
|
125
137
|
google_cfg: Dict[str, Any] = {}
|
|
126
138
|
if max_thinking_tokens > 0:
|
|
@@ -250,6 +262,15 @@ class OpenAIClient(ProviderClient):
|
|
|
250
262
|
model_profile, max_thinking_tokens
|
|
251
263
|
)
|
|
252
264
|
|
|
265
|
+
logger.debug(
|
|
266
|
+
"[openai_client] Starting API request",
|
|
267
|
+
extra={
|
|
268
|
+
"model": model_profile.model,
|
|
269
|
+
"api_base": model_profile.api_base,
|
|
270
|
+
"request_timeout": request_timeout,
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
253
274
|
logger.debug(
|
|
254
275
|
"[openai_client] Request parameters",
|
|
255
276
|
extra={
|
|
@@ -420,12 +441,13 @@ class OpenAIClient(ProviderClient):
|
|
|
420
441
|
)
|
|
421
442
|
|
|
422
443
|
if (
|
|
423
|
-
|
|
444
|
+
can_stream
|
|
424
445
|
and not collected_text
|
|
425
446
|
and not streamed_tool_calls
|
|
426
447
|
and not streamed_tool_text
|
|
448
|
+
and not stream_reasoning_text
|
|
427
449
|
):
|
|
428
|
-
logger.
|
|
450
|
+
logger.warning(
|
|
429
451
|
"[openai_client] Streaming returned no content; retrying without stream",
|
|
430
452
|
extra={"model": model_profile.model},
|
|
431
453
|
)
|
|
@@ -450,6 +472,30 @@ class OpenAIClient(ProviderClient):
|
|
|
450
472
|
if not can_stream and (
|
|
451
473
|
not openai_response or not getattr(openai_response, "choices", None)
|
|
452
474
|
):
|
|
475
|
+
# Check for non-standard error response (e.g., iflow returns HTTP 200 with error JSON)
|
|
476
|
+
error_msg = (
|
|
477
|
+
getattr(openai_response, "msg", None)
|
|
478
|
+
or getattr(openai_response, "message", None)
|
|
479
|
+
or getattr(openai_response, "error", None)
|
|
480
|
+
)
|
|
481
|
+
error_status = getattr(openai_response, "status", None)
|
|
482
|
+
if error_msg or error_status:
|
|
483
|
+
error_text = f"API Error: {error_msg or 'Unknown error'}"
|
|
484
|
+
if error_status:
|
|
485
|
+
error_text = f"API Error ({error_status}): {error_msg or 'Unknown error'}"
|
|
486
|
+
logger.error(
|
|
487
|
+
"[openai_client] Non-standard error response from API",
|
|
488
|
+
extra={
|
|
489
|
+
"model": model_profile.model,
|
|
490
|
+
"error_status": error_status,
|
|
491
|
+
"error_msg": error_msg,
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
return ProviderResponse.create_error(
|
|
495
|
+
error_code="api_error",
|
|
496
|
+
error_message=error_text,
|
|
497
|
+
duration_ms=duration_ms,
|
|
498
|
+
)
|
|
453
499
|
logger.warning(
|
|
454
500
|
"[openai_client] No choices returned from OpenAI response",
|
|
455
501
|
extra={"model": model_profile.model},
|
|
@@ -532,7 +578,7 @@ class OpenAIClient(ProviderClient):
|
|
|
532
578
|
},
|
|
533
579
|
)
|
|
534
580
|
|
|
535
|
-
logger.
|
|
581
|
+
logger.debug(
|
|
536
582
|
"[openai_client] Response received",
|
|
537
583
|
extra={
|
|
538
584
|
"model": model_profile.model,
|