ripperdoc 0.2.9__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.
Files changed (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/cli/cli.py +379 -51
  3. ripperdoc/cli/commands/__init__.py +6 -0
  4. ripperdoc/cli/commands/agents_cmd.py +128 -5
  5. ripperdoc/cli/commands/clear_cmd.py +8 -0
  6. ripperdoc/cli/commands/doctor_cmd.py +29 -0
  7. ripperdoc/cli/commands/exit_cmd.py +1 -0
  8. ripperdoc/cli/commands/memory_cmd.py +2 -1
  9. ripperdoc/cli/commands/models_cmd.py +63 -7
  10. ripperdoc/cli/commands/resume_cmd.py +5 -0
  11. ripperdoc/cli/commands/skills_cmd.py +103 -0
  12. ripperdoc/cli/commands/stats_cmd.py +244 -0
  13. ripperdoc/cli/commands/status_cmd.py +10 -0
  14. ripperdoc/cli/commands/tasks_cmd.py +6 -3
  15. ripperdoc/cli/commands/themes_cmd.py +139 -0
  16. ripperdoc/cli/ui/file_mention_completer.py +63 -13
  17. ripperdoc/cli/ui/helpers.py +6 -3
  18. ripperdoc/cli/ui/interrupt_handler.py +34 -0
  19. ripperdoc/cli/ui/panels.py +14 -8
  20. ripperdoc/cli/ui/rich_ui.py +737 -47
  21. ripperdoc/cli/ui/spinner.py +93 -18
  22. ripperdoc/cli/ui/thinking_spinner.py +1 -2
  23. ripperdoc/cli/ui/tool_renderers.py +10 -9
  24. ripperdoc/cli/ui/wizard.py +24 -19
  25. ripperdoc/core/agents.py +14 -3
  26. ripperdoc/core/config.py +238 -6
  27. ripperdoc/core/default_tools.py +91 -10
  28. ripperdoc/core/hooks/events.py +4 -0
  29. ripperdoc/core/hooks/llm_callback.py +58 -0
  30. ripperdoc/core/hooks/manager.py +6 -0
  31. ripperdoc/core/permissions.py +160 -9
  32. ripperdoc/core/providers/openai.py +84 -28
  33. ripperdoc/core/query.py +489 -87
  34. ripperdoc/core/query_utils.py +17 -14
  35. ripperdoc/core/skills.py +1 -0
  36. ripperdoc/core/theme.py +298 -0
  37. ripperdoc/core/tool.py +15 -5
  38. ripperdoc/protocol/__init__.py +14 -0
  39. ripperdoc/protocol/models.py +300 -0
  40. ripperdoc/protocol/stdio.py +1453 -0
  41. ripperdoc/tools/background_shell.py +354 -139
  42. ripperdoc/tools/bash_tool.py +117 -22
  43. ripperdoc/tools/file_edit_tool.py +228 -50
  44. ripperdoc/tools/file_read_tool.py +154 -3
  45. ripperdoc/tools/file_write_tool.py +53 -11
  46. ripperdoc/tools/grep_tool.py +98 -8
  47. ripperdoc/tools/lsp_tool.py +609 -0
  48. ripperdoc/tools/multi_edit_tool.py +26 -3
  49. ripperdoc/tools/skill_tool.py +52 -1
  50. ripperdoc/tools/task_tool.py +539 -65
  51. ripperdoc/utils/conversation_compaction.py +1 -1
  52. ripperdoc/utils/file_watch.py +216 -7
  53. ripperdoc/utils/image_utils.py +125 -0
  54. ripperdoc/utils/log.py +30 -3
  55. ripperdoc/utils/lsp.py +812 -0
  56. ripperdoc/utils/mcp.py +80 -18
  57. ripperdoc/utils/message_formatting.py +7 -4
  58. ripperdoc/utils/messages.py +198 -33
  59. ripperdoc/utils/pending_messages.py +50 -0
  60. ripperdoc/utils/permissions/shell_command_validation.py +3 -3
  61. ripperdoc/utils/permissions/tool_permission_utils.py +180 -15
  62. ripperdoc/utils/platform.py +198 -0
  63. ripperdoc/utils/session_heatmap.py +242 -0
  64. ripperdoc/utils/session_history.py +2 -2
  65. ripperdoc/utils/session_stats.py +294 -0
  66. ripperdoc/utils/shell_utils.py +8 -5
  67. ripperdoc/utils/todo.py +0 -6
  68. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +55 -17
  69. ripperdoc-0.3.0.dist-info/RECORD +136 -0
  70. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
  71. ripperdoc/sdk/__init__.py +0 -9
  72. ripperdoc/sdk/client.py +0 -333
  73. ripperdoc-0.2.9.dist-info/RECORD +0 -123
  74. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
  76. {ripperdoc-0.2.9.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
@@ -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
- config = get_global_config()
130
- profile_name = getattr(config.model_pointers, model, None) or model
131
- model_profile = config.model_profiles.get(profile_name)
132
- if model_profile is None:
133
- fallback_profile = getattr(config.model_pointers, "main", "default")
134
- model_profile = config.model_profiles.get(fallback_profile)
135
- if not model_profile:
136
- logger.warning(
137
- "[config] No model profile found; using built-in default profile",
138
- extra={"model_pointer": model},
139
- )
140
- return ModelProfile(provider=ProviderType.OPENAI_COMPATIBLE, model="gpt-4o-mini")
141
- return model_profile
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
  ]
@@ -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
@@ -8,8 +8,9 @@ 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 FileSnapshot
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):
@@ -41,10 +44,13 @@ class ToolUseContext(BaseModel):
41
44
  verbose: bool = False
42
45
  permission_checker: Optional[Any] = None
43
46
  read_file_timestamps: Dict[str, float] = Field(default_factory=dict)
44
- # SkipValidation prevents Pydantic from copying the dict during validation,
45
- # ensuring Read and Edit tools share the same cache instance
46
- file_state_cache: Annotated[Dict[str, FileSnapshot], SkipValidation] = Field(
47
- default_factory=dict
47
+ # SkipValidation prevents Pydantic from copying the cache during validation,
48
+ # ensuring Read and Edit tools share the same cache instance.
49
+ # FileCacheType supports both Dict[str, FileSnapshot] and BoundedFileCache.
50
+ file_state_cache: Annotated[FileCacheType, SkipValidation] = Field(default_factory=dict)
51
+ conversation_messages: Annotated[Optional[List[Any]], SkipValidation] = Field(
52
+ default=None,
53
+ description="Full conversation history for tools that need parent context.",
48
54
  )
49
55
  tool_registry: Optional[Any] = None
50
56
  abort_signal: Optional[Any] = None
@@ -55,6 +61,10 @@ class ToolUseContext(BaseModel):
55
61
  on_exit_plan_mode: Optional[Any] = Field(
56
62
  default=None, description="Callback invoked when exiting plan mode"
57
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
+ )
58
68
  model_config = ConfigDict(arbitrary_types_allowed=True)
59
69
 
60
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
+ ]