ripperdoc 0.2.10__py3-none-any.whl → 0.3.1__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_listener.py +233 -0
- ripperdoc/cli/ui/message_display.py +7 -0
- ripperdoc/cli/ui/panels.py +13 -8
- ripperdoc/cli/ui/rich_ui.py +513 -84
- 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 +123 -39
- 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.1.dist-info}/METADATA +49 -17
- ripperdoc-0.3.1.dist-info/RECORD +136 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.1.dist-info}/WHEEL +1 -1
- ripperdoc/cli/ui/interrupt_handler.py +0 -174
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -408
- ripperdoc-0.2.10.dist-info/RECORD +0 -129
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.1.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.1.dist-info}/top_level.txt +0 -0
ripperdoc/core/query_utils.py
CHANGED
|
@@ -125,20 +125,23 @@ def estimate_cost_usd(model_profile: ModelProfile, usage_tokens: Dict[str, int])
|
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def resolve_model_profile(model: str) -> ModelProfile:
|
|
128
|
-
"""Resolve a model pointer to a concrete profile, falling back to a safe default.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
"""Resolve a model pointer to a concrete profile, falling back to a safe default.
|
|
129
|
+
|
|
130
|
+
此函数现在尊重 RIPPERDOC_* 环境变量的覆盖。
|
|
131
|
+
"""
|
|
132
|
+
from ripperdoc.core.config import get_effective_model_profile
|
|
133
|
+
|
|
134
|
+
# 首先尝试使用 get_effective_model_profile 来应用环境变量覆盖
|
|
135
|
+
model_profile = get_effective_model_profile(model)
|
|
136
|
+
if model_profile:
|
|
137
|
+
return model_profile
|
|
138
|
+
|
|
139
|
+
# 如果仍然没有找到,使用默认配置
|
|
140
|
+
logger.warning(
|
|
141
|
+
"[config] No model profile found; using built-in default profile",
|
|
142
|
+
extra={"model_pointer": model},
|
|
143
|
+
)
|
|
144
|
+
return ModelProfile(provider=ProviderType.OPENAI_COMPATIBLE, model="gpt-4o-mini")
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
def determine_tool_mode(model_profile: ModelProfile) -> str:
|
ripperdoc/core/skills.py
CHANGED
|
@@ -278,6 +278,7 @@ def build_skill_summary(skills: Sequence[SkillDefinition]) -> str:
|
|
|
278
278
|
lines = [
|
|
279
279
|
"# Skills",
|
|
280
280
|
"Skills extend your capabilities with reusable instructions stored in SKILL.md files.",
|
|
281
|
+
"IMPORTANT: To use a skill, you MUST call the Skill tool. Do NOT read SKILL.md files directly with Read, Glob, Grep, or Bash commands.",
|
|
281
282
|
'Call the Skill tool with {"skill": "<name>"} to load a skill when it matches the user request.',
|
|
282
283
|
"Available skills:",
|
|
283
284
|
]
|
ripperdoc/core/theme.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Theme system for Ripperdoc CLI.
|
|
2
|
+
|
|
3
|
+
Provides color theming support with predefined themes and runtime switching.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Callable, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ThemeColors:
|
|
12
|
+
"""Theme color definitions - all semantic color slots."""
|
|
13
|
+
|
|
14
|
+
# === Brand/Primary colors ===
|
|
15
|
+
primary: str = "cyan" # Main accent (brand, borders, spinner)
|
|
16
|
+
secondary: str = "green" # Secondary accent (success, ready status)
|
|
17
|
+
|
|
18
|
+
# === Semantic colors ===
|
|
19
|
+
success: str = "green"
|
|
20
|
+
warning: str = "yellow"
|
|
21
|
+
error: str = "red"
|
|
22
|
+
info: str = "blue"
|
|
23
|
+
|
|
24
|
+
# === Text colors ===
|
|
25
|
+
text_primary: str = "white"
|
|
26
|
+
text_secondary: str = "dim" # dim/grey for secondary text
|
|
27
|
+
text_muted: str = "grey50"
|
|
28
|
+
|
|
29
|
+
# === UI elements ===
|
|
30
|
+
border: str = "cyan"
|
|
31
|
+
spinner: str = "cyan"
|
|
32
|
+
prompt: str = "bold green"
|
|
33
|
+
|
|
34
|
+
# === Tool output ===
|
|
35
|
+
tool_call: str = "dim cyan"
|
|
36
|
+
tool_result: str = "dim"
|
|
37
|
+
|
|
38
|
+
# === Message senders ===
|
|
39
|
+
sender_user: str = "bold green"
|
|
40
|
+
sender_assistant: str = "white"
|
|
41
|
+
sender_system: str = "dim cyan"
|
|
42
|
+
|
|
43
|
+
# === Context visualization ===
|
|
44
|
+
ctx_system_prompt: str = "grey58"
|
|
45
|
+
ctx_mcp: str = "cyan"
|
|
46
|
+
ctx_tools: str = "green3"
|
|
47
|
+
ctx_memory: str = "dark_orange3"
|
|
48
|
+
ctx_messages: str = "medium_purple"
|
|
49
|
+
ctx_free: str = "grey46"
|
|
50
|
+
ctx_reserved: str = "yellow3"
|
|
51
|
+
|
|
52
|
+
# === Thinking mode ===
|
|
53
|
+
thinking_on: str = "ansicyan bold"
|
|
54
|
+
thinking_off: str = "ansibrightblack"
|
|
55
|
+
thinking_content: str = "dim italic"
|
|
56
|
+
|
|
57
|
+
# === Headings and emphasis ===
|
|
58
|
+
heading: str = "bold"
|
|
59
|
+
emphasis: str = "bold cyan"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Theme:
|
|
64
|
+
"""Complete theme definition."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
display_name: str
|
|
68
|
+
description: str
|
|
69
|
+
colors: ThemeColors = field(default_factory=ThemeColors)
|
|
70
|
+
|
|
71
|
+
def get_color(self, slot: str) -> str:
|
|
72
|
+
"""Get color for a specific slot."""
|
|
73
|
+
return getattr(self.colors, slot, "white")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# === Predefined themes ===
|
|
77
|
+
|
|
78
|
+
THEME_DARK = Theme(
|
|
79
|
+
name="dark",
|
|
80
|
+
display_name="Dark",
|
|
81
|
+
description="Default dark theme with cyan accents",
|
|
82
|
+
colors=ThemeColors(),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
THEME_LIGHT = Theme(
|
|
86
|
+
name="light",
|
|
87
|
+
display_name="Light",
|
|
88
|
+
description="Light theme for bright terminals",
|
|
89
|
+
colors=ThemeColors(
|
|
90
|
+
primary="blue",
|
|
91
|
+
secondary="green",
|
|
92
|
+
text_primary="black",
|
|
93
|
+
text_secondary="grey37",
|
|
94
|
+
text_muted="grey50",
|
|
95
|
+
border="blue",
|
|
96
|
+
spinner="blue",
|
|
97
|
+
tool_call="dim blue",
|
|
98
|
+
sender_user="bold blue",
|
|
99
|
+
ctx_system_prompt="grey37",
|
|
100
|
+
ctx_free="grey62",
|
|
101
|
+
thinking_on="ansiblue bold",
|
|
102
|
+
thinking_off="grey50",
|
|
103
|
+
emphasis="bold blue",
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
THEME_MONOKAI = Theme(
|
|
108
|
+
name="monokai",
|
|
109
|
+
display_name="Monokai",
|
|
110
|
+
description="Monokai-inspired color scheme",
|
|
111
|
+
colors=ThemeColors(
|
|
112
|
+
primary="#f92672", # Monokai pink
|
|
113
|
+
secondary="#a6e22e", # Monokai green
|
|
114
|
+
warning="#e6db74", # Monokai yellow
|
|
115
|
+
error="#f92672", # Monokai pink
|
|
116
|
+
info="#66d9ef", # Monokai cyan
|
|
117
|
+
border="#f92672",
|
|
118
|
+
spinner="#66d9ef",
|
|
119
|
+
tool_call="#75715e", # Monokai comment
|
|
120
|
+
sender_user="#a6e22e",
|
|
121
|
+
emphasis="#f92672",
|
|
122
|
+
ctx_mcp="#66d9ef",
|
|
123
|
+
ctx_tools="#a6e22e",
|
|
124
|
+
ctx_memory="#fd971f", # Monokai orange
|
|
125
|
+
ctx_messages="#ae81ff", # Monokai purple
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
THEME_DRACULA = Theme(
|
|
130
|
+
name="dracula",
|
|
131
|
+
display_name="Dracula",
|
|
132
|
+
description="Dracula color scheme",
|
|
133
|
+
colors=ThemeColors(
|
|
134
|
+
primary="#bd93f9", # Dracula purple
|
|
135
|
+
secondary="#50fa7b", # Dracula green
|
|
136
|
+
warning="#f1fa8c", # Dracula yellow
|
|
137
|
+
error="#ff5555", # Dracula red
|
|
138
|
+
info="#8be9fd", # Dracula cyan
|
|
139
|
+
border="#bd93f9",
|
|
140
|
+
spinner="#bd93f9",
|
|
141
|
+
tool_call="#6272a4", # Dracula comment
|
|
142
|
+
sender_user="#50fa7b",
|
|
143
|
+
emphasis="#ff79c6", # Dracula pink
|
|
144
|
+
ctx_mcp="#8be9fd",
|
|
145
|
+
ctx_tools="#50fa7b",
|
|
146
|
+
ctx_memory="#ffb86c", # Dracula orange
|
|
147
|
+
ctx_messages="#bd93f9",
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
THEME_SOLARIZED_DARK = Theme(
|
|
152
|
+
name="solarized_dark",
|
|
153
|
+
display_name="Solarized Dark",
|
|
154
|
+
description="Solarized dark color scheme",
|
|
155
|
+
colors=ThemeColors(
|
|
156
|
+
primary="#268bd2", # Solarized blue
|
|
157
|
+
secondary="#859900", # Solarized green
|
|
158
|
+
warning="#b58900", # Solarized yellow
|
|
159
|
+
error="#dc322f", # Solarized red
|
|
160
|
+
info="#2aa198", # Solarized cyan
|
|
161
|
+
text_secondary="#586e75", # Solarized base01
|
|
162
|
+
border="#268bd2",
|
|
163
|
+
spinner="#2aa198",
|
|
164
|
+
emphasis="#268bd2",
|
|
165
|
+
ctx_mcp="#2aa198",
|
|
166
|
+
ctx_tools="#859900",
|
|
167
|
+
ctx_memory="#cb4b16", # Solarized orange
|
|
168
|
+
ctx_messages="#6c71c4", # Solarized violet
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
THEME_NORD = Theme(
|
|
173
|
+
name="nord",
|
|
174
|
+
display_name="Nord",
|
|
175
|
+
description="Arctic, bluish color scheme",
|
|
176
|
+
colors=ThemeColors(
|
|
177
|
+
primary="#88c0d0", # Nord frost
|
|
178
|
+
secondary="#a3be8c", # Nord green
|
|
179
|
+
warning="#ebcb8b", # Nord yellow
|
|
180
|
+
error="#bf616a", # Nord red
|
|
181
|
+
info="#81a1c1", # Nord blue
|
|
182
|
+
text_secondary="#4c566a", # Nord polar night
|
|
183
|
+
border="#88c0d0",
|
|
184
|
+
spinner="#88c0d0",
|
|
185
|
+
emphasis="#88c0d0",
|
|
186
|
+
ctx_mcp="#81a1c1",
|
|
187
|
+
ctx_tools="#a3be8c",
|
|
188
|
+
ctx_memory="#d08770", # Nord orange
|
|
189
|
+
ctx_messages="#b48ead", # Nord purple
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Theme registry
|
|
194
|
+
BUILTIN_THEMES: Dict[str, Theme] = {
|
|
195
|
+
"dark": THEME_DARK,
|
|
196
|
+
"light": THEME_LIGHT,
|
|
197
|
+
"monokai": THEME_MONOKAI,
|
|
198
|
+
"dracula": THEME_DRACULA,
|
|
199
|
+
"solarized_dark": THEME_SOLARIZED_DARK,
|
|
200
|
+
"nord": THEME_NORD,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ThemeManager:
|
|
205
|
+
"""Theme manager - singleton pattern."""
|
|
206
|
+
|
|
207
|
+
_instance: Optional["ThemeManager"] = None
|
|
208
|
+
_current_theme: Theme
|
|
209
|
+
_listeners: List[Callable[["Theme"], None]]
|
|
210
|
+
|
|
211
|
+
def __new__(cls) -> "ThemeManager":
|
|
212
|
+
if cls._instance is None:
|
|
213
|
+
cls._instance = super().__new__(cls)
|
|
214
|
+
cls._instance._current_theme = THEME_DARK
|
|
215
|
+
cls._instance._listeners = []
|
|
216
|
+
return cls._instance
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def current(self) -> Theme:
|
|
220
|
+
"""Get current theme."""
|
|
221
|
+
return self._current_theme
|
|
222
|
+
|
|
223
|
+
def set_theme(self, theme_name: str) -> bool:
|
|
224
|
+
"""Set theme by name (does not persist)."""
|
|
225
|
+
theme = BUILTIN_THEMES.get(theme_name)
|
|
226
|
+
if theme:
|
|
227
|
+
self._current_theme = theme
|
|
228
|
+
self._notify_listeners()
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def get_color(self, slot: str) -> str:
|
|
233
|
+
"""Get color for a slot from current theme."""
|
|
234
|
+
return self._current_theme.get_color(slot)
|
|
235
|
+
|
|
236
|
+
def add_listener(self, callback: Callable[["Theme"], None]) -> None:
|
|
237
|
+
"""Add a theme change listener."""
|
|
238
|
+
self._listeners.append(callback)
|
|
239
|
+
|
|
240
|
+
def remove_listener(self, callback: Callable[["Theme"], None]) -> None:
|
|
241
|
+
"""Remove a theme change listener."""
|
|
242
|
+
if callback in self._listeners:
|
|
243
|
+
self._listeners.remove(callback)
|
|
244
|
+
|
|
245
|
+
def _notify_listeners(self) -> None:
|
|
246
|
+
"""Notify all listeners of theme change."""
|
|
247
|
+
for listener in self._listeners:
|
|
248
|
+
try:
|
|
249
|
+
listener(self._current_theme)
|
|
250
|
+
except Exception:
|
|
251
|
+
pass # Don't let listener errors break theme switching
|
|
252
|
+
|
|
253
|
+
def list_themes(self) -> List[str]:
|
|
254
|
+
"""List all available theme names."""
|
|
255
|
+
return list(BUILTIN_THEMES.keys())
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# Global accessor functions
|
|
259
|
+
_theme_manager: Optional[ThemeManager] = None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def get_theme_manager() -> ThemeManager:
|
|
263
|
+
"""Get the global theme manager instance."""
|
|
264
|
+
global _theme_manager
|
|
265
|
+
if _theme_manager is None:
|
|
266
|
+
_theme_manager = ThemeManager()
|
|
267
|
+
return _theme_manager
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def theme_color(slot: str) -> str:
|
|
271
|
+
"""Convenience function: get color for a slot from current theme."""
|
|
272
|
+
return get_theme_manager().get_color(slot)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def styled(text: str, slot: str) -> str:
|
|
276
|
+
"""Convenience function: wrap text with theme color markup."""
|
|
277
|
+
color = theme_color(slot)
|
|
278
|
+
# Handle colors that already include style modifiers (like "bold cyan")
|
|
279
|
+
if " " in color:
|
|
280
|
+
return f"[{color}]{text}[/]"
|
|
281
|
+
return f"[{color}]{text}[/{color}]"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_current_theme() -> Theme:
|
|
285
|
+
"""Get the current active theme."""
|
|
286
|
+
return get_theme_manager().current
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
__all__ = [
|
|
290
|
+
"Theme",
|
|
291
|
+
"ThemeColors",
|
|
292
|
+
"ThemeManager",
|
|
293
|
+
"BUILTIN_THEMES",
|
|
294
|
+
"get_theme_manager",
|
|
295
|
+
"theme_color",
|
|
296
|
+
"styled",
|
|
297
|
+
"get_current_theme",
|
|
298
|
+
]
|
ripperdoc/core/tool.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Annotated, Any, AsyncGenerator, Dict, List, Optional, TypeVar
|
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field, SkipValidation
|
|
11
11
|
from ripperdoc.utils.file_watch import FileCacheType
|
|
12
12
|
from ripperdoc.utils.log import get_logger
|
|
13
|
+
from ripperdoc.utils.pending_messages import PendingMessageQueue
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
logger = get_logger()
|
|
@@ -30,6 +31,8 @@ class ToolProgress(BaseModel):
|
|
|
30
31
|
content: Any
|
|
31
32
|
normalized_messages: list = []
|
|
32
33
|
tools: list = []
|
|
34
|
+
# Flag to indicate if content is a subagent message that should be forwarded to SDK
|
|
35
|
+
is_subagent_message: bool = False
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class ToolUseContext(BaseModel):
|
|
@@ -44,9 +47,7 @@ class ToolUseContext(BaseModel):
|
|
|
44
47
|
# SkipValidation prevents Pydantic from copying the cache during validation,
|
|
45
48
|
# ensuring Read and Edit tools share the same cache instance.
|
|
46
49
|
# FileCacheType supports both Dict[str, FileSnapshot] and BoundedFileCache.
|
|
47
|
-
file_state_cache: Annotated[FileCacheType, SkipValidation] = Field(
|
|
48
|
-
default_factory=dict
|
|
49
|
-
)
|
|
50
|
+
file_state_cache: Annotated[FileCacheType, SkipValidation] = Field(default_factory=dict)
|
|
50
51
|
conversation_messages: Annotated[Optional[List[Any]], SkipValidation] = Field(
|
|
51
52
|
default=None,
|
|
52
53
|
description="Full conversation history for tools that need parent context.",
|
|
@@ -60,6 +61,10 @@ class ToolUseContext(BaseModel):
|
|
|
60
61
|
on_exit_plan_mode: Optional[Any] = Field(
|
|
61
62
|
default=None, description="Callback invoked when exiting plan mode"
|
|
62
63
|
)
|
|
64
|
+
pending_message_queue: Annotated[Optional[PendingMessageQueue], SkipValidation] = Field(
|
|
65
|
+
default=None,
|
|
66
|
+
description="Queue for pending conversation messages (background notices or user interjections).",
|
|
67
|
+
)
|
|
63
68
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
64
69
|
|
|
65
70
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SDK communication protocol modules.
|
|
2
|
+
|
|
3
|
+
This package contains protocol handlers for communication between
|
|
4
|
+
Ripperdoc CLI and external SDKs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ripperdoc.protocol.stdio import stdio_cmd, StdioProtocolHandler
|
|
8
|
+
from ripperdoc.protocol import models
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"stdio_cmd",
|
|
12
|
+
"StdioProtocolHandler",
|
|
13
|
+
"models",
|
|
14
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Pydantic models for stdio protocol messages.
|
|
2
|
+
|
|
3
|
+
This module defines type-safe models for all JSON messages exchanged
|
|
4
|
+
over the stdio protocol, replacing raw dictionary construction with
|
|
5
|
+
validated, self-documenting Pydantic models.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Content Block Models
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContentBlock(BaseModel):
|
|
20
|
+
"""Base class for content blocks in messages."""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(
|
|
23
|
+
extra="allow",
|
|
24
|
+
populate_by_name=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TextContentBlock(ContentBlock):
|
|
29
|
+
"""A text content block."""
|
|
30
|
+
|
|
31
|
+
type: Literal["text"] = "text"
|
|
32
|
+
text: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ThinkingContentBlock(ContentBlock):
|
|
36
|
+
"""A thinking/reasoning content block."""
|
|
37
|
+
|
|
38
|
+
type: str = Field(default="thinking")
|
|
39
|
+
thinking: str = Field(alias="text")
|
|
40
|
+
signature: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolUseContentBlock(ContentBlock):
|
|
44
|
+
"""A tool use content block."""
|
|
45
|
+
|
|
46
|
+
type: str = Field(default="tool_use")
|
|
47
|
+
id: str = Field(default="")
|
|
48
|
+
name: str
|
|
49
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolResultContentBlock(ContentBlock):
|
|
53
|
+
"""A tool result content block."""
|
|
54
|
+
|
|
55
|
+
type: str = Field(default="tool_result")
|
|
56
|
+
tool_use_id: str = Field(default="")
|
|
57
|
+
content: str = Field(default="")
|
|
58
|
+
is_error: bool | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ImageSource(BaseModel):
|
|
62
|
+
"""Image source data."""
|
|
63
|
+
|
|
64
|
+
type: str = Field(default="base64")
|
|
65
|
+
media_type: str = Field(default="image/jpeg")
|
|
66
|
+
data: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ImageContentBlock(ContentBlock):
|
|
70
|
+
"""An image content block."""
|
|
71
|
+
|
|
72
|
+
type: str = Field(default="image")
|
|
73
|
+
source: ImageSource
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Union type for all content blocks
|
|
77
|
+
ContentBlockType = (
|
|
78
|
+
TextContentBlock
|
|
79
|
+
| ThinkingContentBlock
|
|
80
|
+
| ToolUseContentBlock
|
|
81
|
+
| ToolResultContentBlock
|
|
82
|
+
| ImageContentBlock
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# Message Models
|
|
88
|
+
# ============================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class MessageData(BaseModel):
|
|
92
|
+
"""Base message data."""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(
|
|
95
|
+
extra="allow",
|
|
96
|
+
populate_by_name=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AssistantMessageData(MessageData):
|
|
101
|
+
"""Assistant message data."""
|
|
102
|
+
|
|
103
|
+
content: list[dict[str, Any]] | str
|
|
104
|
+
model: str = "main"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UserMessageData(MessageData):
|
|
108
|
+
"""User message data."""
|
|
109
|
+
|
|
110
|
+
content: str = ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AssistantStreamMessage(BaseModel):
|
|
114
|
+
"""An assistant message sent to the SDK."""
|
|
115
|
+
|
|
116
|
+
type: str = Field(default="assistant")
|
|
117
|
+
message: AssistantMessageData
|
|
118
|
+
parent_tool_use_id: str | None = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class UserStreamMessage(BaseModel):
|
|
122
|
+
"""A user message sent to the SDK."""
|
|
123
|
+
|
|
124
|
+
type: str = Field(default="user")
|
|
125
|
+
message: UserMessageData
|
|
126
|
+
uuid: str | None = None
|
|
127
|
+
parent_tool_use_id: str | None = None
|
|
128
|
+
tool_use_result: Any = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Union type for stream messages
|
|
132
|
+
StreamMessage = AssistantStreamMessage | UserStreamMessage
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# Control Protocol Models
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ControlResponseData(BaseModel):
|
|
141
|
+
"""Base class for control response data."""
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(
|
|
144
|
+
extra="allow",
|
|
145
|
+
populate_by_name=True,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ControlResponseSuccess(ControlResponseData):
|
|
150
|
+
"""A successful control response."""
|
|
151
|
+
|
|
152
|
+
subtype: str = Field(default="success")
|
|
153
|
+
request_id: str
|
|
154
|
+
response: dict[str, Any] | None = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ControlResponseError(ControlResponseData):
|
|
158
|
+
"""An error control response."""
|
|
159
|
+
|
|
160
|
+
subtype: str = Field(default="error")
|
|
161
|
+
request_id: str
|
|
162
|
+
error: str
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ControlResponseMessage(BaseModel):
|
|
166
|
+
"""A control response message wrapper."""
|
|
167
|
+
|
|
168
|
+
type: str = Field(default="control_response")
|
|
169
|
+
response: ControlResponseSuccess | ControlResponseError
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ============================================================================
|
|
173
|
+
# Result/Usage Models
|
|
174
|
+
# ============================================================================
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class UsageInfo(BaseModel):
|
|
178
|
+
"""Token usage information."""
|
|
179
|
+
|
|
180
|
+
input_tokens: int = 0
|
|
181
|
+
cache_creation_input_tokens: int = 0
|
|
182
|
+
cache_read_input_tokens: int = 0
|
|
183
|
+
output_tokens: int = 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class MCPServerInfo(BaseModel):
|
|
187
|
+
"""MCP server information."""
|
|
188
|
+
|
|
189
|
+
name: str
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class InitializeResponseData(BaseModel):
|
|
193
|
+
"""Response data for initialize request."""
|
|
194
|
+
|
|
195
|
+
session_id: str
|
|
196
|
+
system_prompt: str
|
|
197
|
+
tools: list[str]
|
|
198
|
+
mcp_servers: list[MCPServerInfo] = Field(default_factory=list)
|
|
199
|
+
slash_commands: list[Any] = Field(default_factory=list)
|
|
200
|
+
apiKeySource: str = "none"
|
|
201
|
+
claude_code_version: str = "0.1.0"
|
|
202
|
+
output_style: str = "default"
|
|
203
|
+
agents: list[str] = Field(default_factory=list)
|
|
204
|
+
skills: list[Any] = Field(default_factory=list)
|
|
205
|
+
plugins: list[Any] = Field(default_factory=list)
|
|
206
|
+
|
|
207
|
+
model_config = ConfigDict(
|
|
208
|
+
extra="allow",
|
|
209
|
+
populate_by_name=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ResultMessage(BaseModel):
|
|
214
|
+
"""A result message sent at the end of a query."""
|
|
215
|
+
|
|
216
|
+
type: str = Field(default="result")
|
|
217
|
+
subtype: str = Field(default="result")
|
|
218
|
+
duration_ms: int
|
|
219
|
+
duration_api_ms: int
|
|
220
|
+
is_error: bool
|
|
221
|
+
num_turns: int
|
|
222
|
+
session_id: str
|
|
223
|
+
total_cost_usd: float | None = None
|
|
224
|
+
usage: UsageInfo | None = None
|
|
225
|
+
result: str | None = None
|
|
226
|
+
structured_output: Any = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ============================================================================
|
|
230
|
+
# Permission Response Models
|
|
231
|
+
# ============================================================================
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class PermissionResponseAllow(BaseModel):
|
|
235
|
+
"""A permission allow response."""
|
|
236
|
+
|
|
237
|
+
decision: str = Field(default="allow")
|
|
238
|
+
updatedInput: dict[str, Any] | None = None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class PermissionResponseDeny(BaseModel):
|
|
242
|
+
"""A permission deny response."""
|
|
243
|
+
|
|
244
|
+
decision: str = Field(default="deny")
|
|
245
|
+
message: str = ""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ============================================================================
|
|
249
|
+
# Helper Functions
|
|
250
|
+
# ============================================================================
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def model_to_dict(model: BaseModel) -> dict[str, Any]:
|
|
254
|
+
"""Convert a Pydantic model to a JSON-serializable dictionary.
|
|
255
|
+
|
|
256
|
+
This handles exclude_none=True and ensures proper serialization,
|
|
257
|
+
while always including type/subtype fields for protocol messages.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
model: The Pydantic model to convert.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
A JSON-serializable dictionary.
|
|
264
|
+
"""
|
|
265
|
+
return model.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
__all__ = [
|
|
269
|
+
# Content Blocks
|
|
270
|
+
"ContentBlock",
|
|
271
|
+
"TextContentBlock",
|
|
272
|
+
"ThinkingContentBlock",
|
|
273
|
+
"ToolUseContentBlock",
|
|
274
|
+
"ToolResultContentBlock",
|
|
275
|
+
"ImageContentBlock",
|
|
276
|
+
"ImageSource",
|
|
277
|
+
"ContentBlockType",
|
|
278
|
+
# Messages
|
|
279
|
+
"MessageData",
|
|
280
|
+
"AssistantMessageData",
|
|
281
|
+
"UserMessageData",
|
|
282
|
+
"AssistantStreamMessage",
|
|
283
|
+
"UserStreamMessage",
|
|
284
|
+
"StreamMessage",
|
|
285
|
+
# Control Protocol
|
|
286
|
+
"ControlResponseData",
|
|
287
|
+
"ControlResponseSuccess",
|
|
288
|
+
"ControlResponseError",
|
|
289
|
+
"ControlResponseMessage",
|
|
290
|
+
# Result/Usage
|
|
291
|
+
"UsageInfo",
|
|
292
|
+
"MCPServerInfo",
|
|
293
|
+
"InitializeResponseData",
|
|
294
|
+
"ResultMessage",
|
|
295
|
+
# Permissions
|
|
296
|
+
"PermissionResponseAllow",
|
|
297
|
+
"PermissionResponseDeny",
|
|
298
|
+
# Helpers
|
|
299
|
+
"model_to_dict",
|
|
300
|
+
]
|