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
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import fnmatch
5
6
  from dataclasses import dataclass
6
7
  from typing import Callable, Iterable, List, Optional, Set
7
8
 
@@ -27,8 +28,25 @@ class PermissionDecision:
27
28
  rule_suggestions: Optional[List[ToolRule] | List[str]] = None
28
29
 
29
30
 
30
- def create_wildcard_rule(rule_name: str) -> str:
31
- """Create a wildcard/prefix rule string."""
31
+ def create_wildcard_rule(rule_name: str, use_glob_style: bool = False) -> str:
32
+ """Create a wildcard/prefix rule string.
33
+
34
+ Args:
35
+ rule_name: The command prefix (e.g., "git", "npm")
36
+ use_glob_style: If True, creates "rule_name *" format;
37
+ if False, creates "rule_name:*" format (default for compatibility)
38
+
39
+ Returns:
40
+ Wildcard rule string in requested format
41
+
42
+ Examples:
43
+ >>> create_wildcard_rule("git")
44
+ "git:*"
45
+ >>> create_wildcard_rule("git", use_glob_style=True)
46
+ "git *"
47
+ """
48
+ if use_glob_style:
49
+ return f"{rule_name} *"
32
50
  return f"{rule_name}:*"
33
51
 
34
52
 
@@ -36,22 +54,124 @@ def create_tool_rule(rule_content: str) -> List[ToolRule]:
36
54
  return [ToolRule(tool_name="Bash", rule_content=rule_content)]
37
55
 
38
56
 
39
- def create_wildcard_tool_rule(rule_name: str) -> List[ToolRule]:
40
- return [ToolRule(tool_name="Bash", rule_content=create_wildcard_rule(rule_name))]
57
+ def create_wildcard_tool_rule(rule_name: str, use_glob_style: bool = False) -> List[ToolRule]:
58
+ """Create a wildcard tool rule.
59
+
60
+ Args:
61
+ rule_name: The command prefix
62
+ use_glob_style: Whether to use glob-style format
63
+
64
+ Returns:
65
+ List containing a single ToolRule with wildcard pattern
66
+ """
67
+ return [
68
+ ToolRule(tool_name="Bash", rule_content=create_wildcard_rule(rule_name, use_glob_style))
69
+ ]
41
70
 
42
71
 
43
72
  def extract_rule_prefix(rule_string: str) -> Optional[str]:
44
73
  return rule_string[:-2] if rule_string.endswith(":*") else None
45
74
 
46
75
 
76
+ def _has_unquoted_shell_operators(command: str) -> bool:
77
+ """Check if command contains shell operators outside of quotes.
78
+
79
+ This prevents wildcard rules from matching commands with shell operators
80
+ like &&, ||, ;, | which could be used to chain dangerous commands.
81
+
82
+ Args:
83
+ command: The command to check
84
+
85
+ Returns:
86
+ True if command contains unquoted shell operators, False otherwise
87
+
88
+ Examples:
89
+ >>> _has_unquoted_shell_operators("git status && rm -rf /")
90
+ True
91
+ >>> _has_unquoted_shell_operators("echo 'foo && bar'")
92
+ False
93
+ >>> _has_unquoted_shell_operators("ls | grep foo")
94
+ True
95
+ >>> _has_unquoted_shell_operators("echo hi; rm -rf /")
96
+ True
97
+ """
98
+ # Parse the command into tokens, which handles quotes correctly
99
+ tokens = parse_shell_tokens(command)
100
+
101
+ # Check for shell operators in the tokens
102
+ # Note: parse_shell_tokens may attach semicolon to adjacent tokens
103
+ # (e.g., "echo hi; rm" -> ["echo", "hi;", "rm"] or ";echo" -> [";echo"])
104
+ for token in tokens:
105
+ # Check for separate operator tokens
106
+ if token in {"&&", "||", "|"}:
107
+ return True
108
+ # Check for semicolon (may be attached to previous or next token)
109
+ if token.endswith(";") or token.startswith(";"):
110
+ return True
111
+
112
+ return False
113
+
114
+
47
115
  def match_rule(command: str, rule: str) -> bool:
48
- """Return True if a command matches a rule (exact or wildcard)."""
116
+ """Return True if a command matches a rule (exact, prefix wildcard, or glob pattern).
117
+
118
+ Supports three formats:
119
+ - Exact match: "git status" matches "git status" only
120
+ - Legacy prefix: "git:*" matches any command starting with "git"
121
+ - Glob patterns: "git * main" matches "git push main", "git pull main", etc.
122
+
123
+ Glob patterns support:
124
+ - * (matches any characters)
125
+ - ? (matches single character)
126
+ - [seq] (matches any character in seq)
127
+ - [!seq] (matches any character NOT in seq)
128
+
129
+ Security: Wildcard rules (both legacy and glob) will NOT match commands
130
+ containing shell operators (&&, ||, ;, |) outside of quotes. This prevents
131
+ a rule like "git:*" from matching "git status && rm -rf /". Exact match
132
+ rules still work with operators (user explicitly approved the full command).
133
+
134
+ Args:
135
+ command: The command to check
136
+ rule: The rule pattern to match against
137
+
138
+ Returns:
139
+ True if command matches rule, False otherwise
140
+
141
+ Examples:
142
+ >>> match_rule("git status", "git:*")
143
+ True
144
+ >>> match_rule("npm install", "npm *")
145
+ True
146
+ >>> match_rule("pip install", "* install")
147
+ True
148
+ >>> match_rule("git push main", "git * main")
149
+ True
150
+ >>> match_rule("git status && rm -rf /", "git:*")
151
+ False
152
+ >>> match_rule("git status && git commit", "git status && git commit")
153
+ True
154
+ """
49
155
  command = command.strip()
50
156
  if not command:
51
157
  return False
158
+
159
+ # Legacy format: prefix:* (highest priority for backward compatibility)
52
160
  prefix = extract_rule_prefix(rule)
53
161
  if prefix is not None:
162
+ # For wildcard rules, reject commands with shell operators for security
163
+ if _has_unquoted_shell_operators(command):
164
+ return False
54
165
  return command.startswith(prefix)
166
+
167
+ # New format: glob-style patterns with wildcards
168
+ if "*" in rule or "?" in rule or "[" in rule:
169
+ # For wildcard rules, reject commands with shell operators for security
170
+ if _has_unquoted_shell_operators(command):
171
+ return False
172
+ return fnmatch.fnmatch(command, rule)
173
+
174
+ # Exact match - allow even with operators (user explicitly approved full command)
55
175
  return command == rule
56
176
 
57
177
 
@@ -131,10 +251,41 @@ def _is_command_read_only(
131
251
 
132
252
 
133
253
  def _collect_rule_suggestions(command: str) -> List[ToolRule]:
134
- suggestions: list[ToolRule] = [ToolRule(tool_name="Bash", rule_content=command)]
254
+ """Collect rule suggestions for a command.
255
+
256
+ Suggests three options:
257
+ 1. Exact command match
258
+ 2. Legacy prefix wildcard (git:*)
259
+ 3. Glob-style wildcard (git *)
260
+
261
+ This gives users choice between formats while maintaining backward compatibility.
262
+
263
+ Args:
264
+ command: The command to generate suggestions for
265
+
266
+ Returns:
267
+ List of suggested ToolRule objects
268
+ """
269
+ suggestions: list[ToolRule] = [
270
+ # Exact match
271
+ ToolRule(tool_name="Bash", rule_content=command)
272
+ ]
273
+
135
274
  tokens = parse_and_clean_shell_tokens(command)
136
275
  if tokens:
137
- suggestions.append(ToolRule(tool_name="Bash", rule_content=create_wildcard_rule(tokens[0])))
276
+ # Legacy prefix format
277
+ suggestions.append(
278
+ ToolRule(
279
+ tool_name="Bash", rule_content=create_wildcard_rule(tokens[0], use_glob_style=False)
280
+ )
281
+ )
282
+ # New glob-style format
283
+ suggestions.append(
284
+ ToolRule(
285
+ tool_name="Bash", rule_content=create_wildcard_rule(tokens[0], use_glob_style=True)
286
+ )
287
+ )
288
+
138
289
  return suggestions
139
290
 
140
291
 
@@ -144,19 +295,34 @@ def evaluate_shell_command_permissions(
144
295
  denied_rules: Iterable[str],
145
296
  allowed_working_dirs: Set[str] | None = None,
146
297
  *,
147
- command_injection_detected: bool = False,
148
- injection_detector: Callable[[str], bool] | None = None,
298
+ danger_detector: Callable[[str], bool] | None = None,
149
299
  read_only_detector: Callable[[str, Callable[[str], bool]], bool] | None = None,
150
300
  ) -> PermissionDecision:
151
- """Evaluate whether a bash command should be allowed."""
301
+ """Evaluate whether a bash command should be allowed.
302
+
303
+ Args:
304
+ tool_request: The tool request containing the command.
305
+ allowed_rules: Rules that allow commands.
306
+ denied_rules: Rules that deny commands.
307
+ allowed_working_dirs: Allowed working directories.
308
+ danger_detector: Callable that returns True if command contains dangerous
309
+ shell patterns (injection, destructive commands, etc.).
310
+ read_only_detector: Callable that returns True if command is read-only.
311
+
312
+ Returns:
313
+ PermissionDecision indicating whether the command should be allowed.
314
+ """
152
315
  command = tool_request.command if hasattr(tool_request, "command") else str(tool_request)
153
316
  trimmed_command = command.strip()
154
317
  allowed_working_dirs = allowed_working_dirs or {safe_get_cwd()}
155
- injection_detector = injection_detector or (
318
+ danger_detector = danger_detector or (
156
319
  lambda cmd: validate_shell_command(cmd).behavior != "passthrough"
157
320
  )
158
321
  read_only_detector = read_only_detector or _is_command_read_only
159
322
 
323
+ # Detect dangerous patterns once, reuse the result
324
+ has_dangerous_patterns = danger_detector(trimmed_command)
325
+
160
326
  merged_denied = _merge_rules(denied_rules)
161
327
  merged_allowed = _merge_rules(allowed_rules)
162
328
 
@@ -217,11 +383,10 @@ def evaluate_shell_command_permissions(
217
383
  merged_allowed,
218
384
  merged_denied,
219
385
  allowed_working_dirs,
220
- command_injection_detected=command_injection_detected,
221
- injection_detector=injection_detector,
386
+ danger_detector=danger_detector,
222
387
  read_only_detector=read_only_detector,
223
388
  )
224
- right_read_only = read_only_detector(right_command, injection_detector)
389
+ right_read_only = read_only_detector(right_command, danger_detector)
225
390
 
226
391
  if left_result.behavior == "deny":
227
392
  return left_result
@@ -245,7 +410,7 @@ def evaluate_shell_command_permissions(
245
410
  rule_suggestions=_collect_rule_suggestions(trimmed_command),
246
411
  )
247
412
 
248
- if read_only_detector(trimmed_command, injection_detector) and not command_injection_detected:
413
+ if read_only_detector(trimmed_command, danger_detector) and not has_dangerous_patterns:
249
414
  return PermissionDecision(
250
415
  behavior="allow",
251
416
  updated_input=tool_request,
@@ -0,0 +1,198 @@
1
+ """Platform detection utilities.
2
+
3
+ This module provides a unified interface for detecting the current operating system
4
+ and platform-specific capabilities. It should be used instead of direct checks
5
+ like `sys.platform == "win32"` or `os.name == "nt"`.
6
+
7
+ Usage:
8
+ from ripperdoc.utils.platform import (
9
+ is_windows,
10
+ is_linux,
11
+ is_macos,
12
+ is_unix,
13
+ Platform,
14
+ )
15
+
16
+ if is_windows():
17
+ # Windows-specific code
18
+ elif is_macos():
19
+ # macOS-specific code
20
+ else:
21
+ # Linux or other Unix-specific code
22
+ """
23
+
24
+ import os
25
+ import sys
26
+ from typing import Final, Literal
27
+
28
+
29
+ # Platform type definitions
30
+ PlatformType = Literal["windows", "linux", "macos", "unknown"]
31
+
32
+
33
+ class Platform:
34
+ """Platform detection constants and utilities.
35
+
36
+ This class provides platform detection methods and constants that should
37
+ be used throughout the codebase instead of direct checks.
38
+ """
39
+
40
+ # Platform constants (using sys.platform for consistency)
41
+ WINDOWS: Final = "win32"
42
+ LINUX: Final = "linux"
43
+ MACOS: Final = "darwin"
44
+ FREEBSD: Final = "freebsd"
45
+ OPENBSD: Final = "openbsd"
46
+ NETBSD: Final = "netbsd"
47
+
48
+ # os.name constants
49
+ NAME_NT: Final = "nt" # Windows
50
+ NAME_POSIX: Final = "posix" # Unix-like systems
51
+
52
+ @staticmethod
53
+ def get_system() -> PlatformType:
54
+ """Get the current operating system name.
55
+
56
+ Returns:
57
+ 'windows', 'linux', 'macos', or 'unknown'
58
+ """
59
+ platform = sys.platform.lower()
60
+
61
+ if platform.startswith("win"):
62
+ return "windows"
63
+ elif platform.startswith("darwin"):
64
+ return "macos"
65
+ elif platform.startswith("linux"):
66
+ return "linux"
67
+ elif platform in {"freebsd", "openbsd", "netbsd"}:
68
+ return "linux" # Treat BSD as Linux for most purposes
69
+ else:
70
+ return "unknown"
71
+
72
+ @staticmethod
73
+ def is_windows() -> bool:
74
+ """Check if running on Windows."""
75
+ return sys.platform == Platform.WINDOWS
76
+
77
+ @staticmethod
78
+ def is_linux() -> bool:
79
+ """Check if running on Linux."""
80
+ return sys.platform.startswith("linux")
81
+
82
+ @staticmethod
83
+ def is_macos() -> bool:
84
+ """Check if running on macOS."""
85
+ return sys.platform == Platform.MACOS
86
+
87
+ @staticmethod
88
+ def is_bsd() -> bool:
89
+ """Check if running on any BSD variant."""
90
+ return sys.platform in {Platform.FREEBSD, Platform.OPENBSD, Platform.NETBSD}
91
+
92
+ @staticmethod
93
+ def is_unix() -> bool:
94
+ """Check if running on any Unix-like system (Linux, macOS, BSD)."""
95
+ return os.name == Platform.NAME_POSIX
96
+
97
+ @staticmethod
98
+ def is_posix() -> bool:
99
+ """Check if running on a POSIX-compliant system.
100
+
101
+ This is equivalent to is_unix() but uses os.name for the check.
102
+ """
103
+ return os.name == Platform.NAME_POSIX
104
+
105
+ @staticmethod
106
+ def get_raw_name() -> str:
107
+ """Get the raw sys.platform value.
108
+
109
+ Returns:
110
+ The raw sys.platform string (e.g., 'win32', 'linux', 'darwin').
111
+ """
112
+ return sys.platform
113
+
114
+ @staticmethod
115
+ def get_os_name() -> str:
116
+ """Get the os.name value.
117
+
118
+ Returns:
119
+ 'nt' for Windows, 'posix' for Unix-like systems.
120
+ """
121
+ return os.name
122
+
123
+
124
+ # Convenience functions for direct import
125
+ def is_windows() -> bool:
126
+ """Check if running on Windows."""
127
+ return Platform.is_windows()
128
+
129
+
130
+ def is_linux() -> bool:
131
+ """Check if running on Linux."""
132
+ return Platform.is_linux()
133
+
134
+
135
+ def is_macos() -> bool:
136
+ """Check if running on macOS."""
137
+ return Platform.is_macos()
138
+
139
+
140
+ def is_bsd() -> bool:
141
+ """Check if running on any BSD variant."""
142
+ return Platform.is_bsd()
143
+
144
+
145
+ def is_unix() -> bool:
146
+ """Check if running on any Unix-like system (Linux, macOS, BSD)."""
147
+ return Platform.is_unix()
148
+
149
+
150
+ def is_posix() -> bool:
151
+ """Check if running on a POSIX-compliant system."""
152
+ return Platform.is_posix()
153
+
154
+
155
+ # Module-level constants for backward compatibility
156
+ IS_WINDOWS: Final = is_windows()
157
+ IS_LINUX: Final = is_linux()
158
+ IS_MACOS: Final = is_macos()
159
+ IS_BSD: Final = is_bsd()
160
+ IS_UNIX: Final = is_unix()
161
+ IS_POSIX: Final = is_posix()
162
+
163
+
164
+ # Platform-specific module availability
165
+ def has_termios() -> bool:
166
+ """Check if the termios module is available (Unix-like systems only)."""
167
+ try:
168
+ import termios # noqa: F401
169
+
170
+ return True
171
+ except ImportError:
172
+ return False
173
+
174
+
175
+ def has_fcntl() -> bool:
176
+ """Check if the fcntl module is available (Unix-like systems only)."""
177
+ try:
178
+ import fcntl # noqa: F401
179
+
180
+ return True
181
+ except ImportError:
182
+ return False
183
+
184
+
185
+ def has_tty() -> bool:
186
+ """Check if the tty module is available (Unix-like systems only)."""
187
+ try:
188
+ import tty # noqa: F401
189
+
190
+ return True
191
+ except ImportError:
192
+ return False
193
+
194
+
195
+ # Module-level constants for module availability
196
+ HAS_TERMIOS: Final = has_termios()
197
+ HAS_FCNTL: Final = has_fcntl()
198
+ HAS_TTY: Final = has_tty()