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/tools/todo_tool.py
CHANGED
|
@@ -309,7 +309,7 @@ class TodoWriteTool(Tool[TodoWriteToolInput, TodoToolOutput]):
|
|
|
309
309
|
),
|
|
310
310
|
]
|
|
311
311
|
|
|
312
|
-
async def prompt(self,
|
|
312
|
+
async def prompt(self, _yolo_mode: bool = False) -> str:
|
|
313
313
|
return TODO_WRITE_PROMPT
|
|
314
314
|
|
|
315
315
|
def is_read_only(self) -> bool:
|
|
@@ -403,7 +403,7 @@ class TodoReadTool(Tool[TodoReadToolInput, TodoToolOutput]):
|
|
|
403
403
|
),
|
|
404
404
|
]
|
|
405
405
|
|
|
406
|
-
async def prompt(self,
|
|
406
|
+
async def prompt(self, _yolo_mode: bool = False) -> str:
|
|
407
407
|
return (
|
|
408
408
|
"Use TodoRead to fetch the current todo list before making progress or when you need "
|
|
409
409
|
"to confirm the next action. You can request only the next actionable item or filter "
|
|
@@ -106,7 +106,7 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
|
|
|
106
106
|
),
|
|
107
107
|
]
|
|
108
108
|
|
|
109
|
-
async def prompt(self,
|
|
109
|
+
async def prompt(self, yolo_mode: bool = False) -> str: # noqa: ARG002
|
|
110
110
|
return (
|
|
111
111
|
"Search for a tool by providing a short description (e.g., 'query database', 'render notebook'). "
|
|
112
112
|
"Use names to activate tools you've already discovered. "
|
|
@@ -193,7 +193,8 @@ class ToolSearchTool(Tool[ToolSearchInput, ToolSearchOutput]):
|
|
|
193
193
|
description = ""
|
|
194
194
|
logger.warning(
|
|
195
195
|
"[tool_search] Failed to build tool description: %s: %s",
|
|
196
|
-
type(exc).__name__,
|
|
196
|
+
type(exc).__name__,
|
|
197
|
+
exc,
|
|
197
198
|
extra={"tool_name": getattr(tool, "name", None)},
|
|
198
199
|
)
|
|
199
200
|
doc_text = " ".join([name, tool.user_facing_name(), description])
|
|
@@ -34,6 +34,7 @@ RECENT_MESSAGES_AFTER_COMPACT = 8
|
|
|
34
34
|
# Summary Prompt Generation
|
|
35
35
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
36
36
|
|
|
37
|
+
|
|
37
38
|
def generate_summary_prompt(additional_instructions: Optional[str] = None) -> str:
|
|
38
39
|
"""Generate the system prompt for conversation summarization.
|
|
39
40
|
|
|
@@ -203,9 +204,11 @@ Please continue the conversation from where we left it off without asking the us
|
|
|
203
204
|
# Data Classes
|
|
204
205
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
205
206
|
|
|
207
|
+
|
|
206
208
|
@dataclass
|
|
207
209
|
class CompactionResult:
|
|
208
210
|
"""Result of a conversation compaction operation."""
|
|
211
|
+
|
|
209
212
|
messages: List[ConversationMessage]
|
|
210
213
|
summary_text: str
|
|
211
214
|
continuation_prompt: str
|
|
@@ -219,6 +222,7 @@ class CompactionResult:
|
|
|
219
222
|
@dataclass
|
|
220
223
|
class CompactionError:
|
|
221
224
|
"""Error during compaction."""
|
|
225
|
+
|
|
222
226
|
error_type: str # "not_enough_messages", "empty_summary", "exception"
|
|
223
227
|
message: str
|
|
224
228
|
exception: Optional[Exception] = None
|
|
@@ -329,7 +333,7 @@ async def summarize_conversation(
|
|
|
329
333
|
system_prompt=system_prompt,
|
|
330
334
|
tools=[],
|
|
331
335
|
max_thinking_tokens=0,
|
|
332
|
-
model="
|
|
336
|
+
model="quick",
|
|
333
337
|
)
|
|
334
338
|
|
|
335
339
|
result = extract_assistant_text(assistant_response)
|
|
@@ -373,16 +377,15 @@ async def compact_conversation(
|
|
|
373
377
|
messages_for_summary = micro.messages
|
|
374
378
|
|
|
375
379
|
# Summarize the conversation
|
|
376
|
-
|
|
380
|
+
|
|
377
381
|
non_progress_messages = [
|
|
378
382
|
m for m in messages_for_summary if getattr(m, "type", "") != "progress"
|
|
379
383
|
]
|
|
380
384
|
try:
|
|
381
|
-
summary_text = await summarize_conversation(
|
|
382
|
-
non_progress_messages, custom_instructions
|
|
383
|
-
)
|
|
385
|
+
summary_text = await summarize_conversation(non_progress_messages, custom_instructions)
|
|
384
386
|
except Exception as exc:
|
|
385
387
|
import traceback
|
|
388
|
+
|
|
386
389
|
logger.warning(
|
|
387
390
|
"[compaction] Error during compaction: %s: %s\n%s",
|
|
388
391
|
type(exc).__name__,
|
|
@@ -443,6 +446,7 @@ class ConversationCompactor:
|
|
|
443
446
|
Deprecated: Use compact_conversation() function directly instead.
|
|
444
447
|
This class is kept for backward compatibility.
|
|
445
448
|
"""
|
|
449
|
+
|
|
446
450
|
# Keep CompactionResult as a nested class for backward compatibility
|
|
447
451
|
CompactionResult = CompactionResult
|
|
448
452
|
|
ripperdoc/utils/file_watch.py
CHANGED
|
@@ -4,13 +4,20 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import difflib
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from collections import OrderedDict
|
|
7
10
|
from dataclasses import dataclass
|
|
8
|
-
from typing import Dict, List, Optional
|
|
11
|
+
from typing import Dict, Iterator, List, Optional, Tuple
|
|
9
12
|
|
|
10
13
|
from ripperdoc.utils.log import get_logger
|
|
11
14
|
|
|
12
15
|
logger = get_logger()
|
|
13
16
|
|
|
17
|
+
# Default limits for BoundedFileCache
|
|
18
|
+
DEFAULT_MAX_ENTRIES = int(os.getenv("RIPPERDOC_FILE_CACHE_MAX_ENTRIES", "500"))
|
|
19
|
+
DEFAULT_MAX_MEMORY_MB = float(os.getenv("RIPPERDOC_FILE_CACHE_MAX_MEMORY_MB", "50"))
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
@dataclass
|
|
16
23
|
class FileSnapshot:
|
|
@@ -21,6 +28,198 @@ class FileSnapshot:
|
|
|
21
28
|
offset: int = 0
|
|
22
29
|
limit: Optional[int] = None
|
|
23
30
|
|
|
31
|
+
def memory_size(self) -> int:
|
|
32
|
+
"""Estimate memory usage of this snapshot in bytes."""
|
|
33
|
+
# String memory: roughly 1 byte per char for ASCII, more for unicode
|
|
34
|
+
# Plus object overhead (~50 bytes for dataclass)
|
|
35
|
+
return sys.getsizeof(self.content) + 50
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BoundedFileCache:
|
|
39
|
+
"""Thread-safe LRU cache for FileSnapshots with memory and entry limits.
|
|
40
|
+
|
|
41
|
+
This cache prevents unbounded memory growth in long sessions by:
|
|
42
|
+
1. Limiting the maximum number of entries (LRU eviction)
|
|
43
|
+
2. Limiting total memory usage
|
|
44
|
+
3. Providing thread-safe access
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
cache = BoundedFileCache(max_entries=500, max_memory_mb=50)
|
|
48
|
+
cache["/path/to/file"] = FileSnapshot(content="...", timestamp=123.0)
|
|
49
|
+
snapshot = cache.get("/path/to/file")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
max_entries: int = DEFAULT_MAX_ENTRIES,
|
|
55
|
+
max_memory_mb: float = DEFAULT_MAX_MEMORY_MB,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize the bounded cache.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
max_entries: Maximum number of file snapshots to keep
|
|
61
|
+
max_memory_mb: Maximum total memory usage in megabytes
|
|
62
|
+
"""
|
|
63
|
+
self._max_entries = max(1, max_entries)
|
|
64
|
+
self._max_memory_bytes = int(max_memory_mb * 1024 * 1024)
|
|
65
|
+
self._cache: OrderedDict[str, FileSnapshot] = OrderedDict()
|
|
66
|
+
self._current_memory = 0
|
|
67
|
+
self._lock = threading.RLock()
|
|
68
|
+
self._eviction_count = 0
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def max_entries(self) -> int:
|
|
72
|
+
"""Maximum number of entries allowed."""
|
|
73
|
+
return self._max_entries
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def max_memory_bytes(self) -> int:
|
|
77
|
+
"""Maximum memory in bytes."""
|
|
78
|
+
return self._max_memory_bytes
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def current_memory(self) -> int:
|
|
82
|
+
"""Current estimated memory usage in bytes."""
|
|
83
|
+
with self._lock:
|
|
84
|
+
return self._current_memory
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def eviction_count(self) -> int:
|
|
88
|
+
"""Number of entries evicted due to limits."""
|
|
89
|
+
with self._lock:
|
|
90
|
+
return self._eviction_count
|
|
91
|
+
|
|
92
|
+
def __len__(self) -> int:
|
|
93
|
+
with self._lock:
|
|
94
|
+
return len(self._cache)
|
|
95
|
+
|
|
96
|
+
def __contains__(self, key: str) -> bool:
|
|
97
|
+
with self._lock:
|
|
98
|
+
return key in self._cache
|
|
99
|
+
|
|
100
|
+
def __getitem__(self, key: str) -> FileSnapshot:
|
|
101
|
+
with self._lock:
|
|
102
|
+
if key not in self._cache:
|
|
103
|
+
raise KeyError(key)
|
|
104
|
+
# Move to end (most recently used)
|
|
105
|
+
self._cache.move_to_end(key)
|
|
106
|
+
return self._cache[key]
|
|
107
|
+
|
|
108
|
+
def __setitem__(self, key: str, value: FileSnapshot) -> None:
|
|
109
|
+
with self._lock:
|
|
110
|
+
new_size = value.memory_size()
|
|
111
|
+
|
|
112
|
+
# If key exists, remove old entry first (atomic pop to avoid TOCTOU)
|
|
113
|
+
old_value = self._cache.pop(key, None)
|
|
114
|
+
if old_value is not None:
|
|
115
|
+
self._current_memory = max(0, self._current_memory - old_value.memory_size())
|
|
116
|
+
|
|
117
|
+
# Evict entries if needed (memory limit)
|
|
118
|
+
while self._current_memory + new_size > self._max_memory_bytes and self._cache:
|
|
119
|
+
self._evict_oldest()
|
|
120
|
+
|
|
121
|
+
# Evict entries if needed (entry limit)
|
|
122
|
+
while len(self._cache) >= self._max_entries:
|
|
123
|
+
self._evict_oldest()
|
|
124
|
+
|
|
125
|
+
# Add new entry
|
|
126
|
+
self._cache[key] = value
|
|
127
|
+
self._current_memory += new_size
|
|
128
|
+
|
|
129
|
+
def __delitem__(self, key: str) -> None:
|
|
130
|
+
with self._lock:
|
|
131
|
+
# Use atomic pop to avoid TOCTOU between check and delete
|
|
132
|
+
old_value = self._cache.pop(key, None)
|
|
133
|
+
if old_value is not None:
|
|
134
|
+
self._current_memory = max(0, self._current_memory - old_value.memory_size())
|
|
135
|
+
|
|
136
|
+
def _evict_oldest(self) -> None:
|
|
137
|
+
"""Evict the least recently used entry. Must be called with lock held."""
|
|
138
|
+
if self._cache:
|
|
139
|
+
oldest_key, oldest_value = self._cache.popitem(last=False)
|
|
140
|
+
self._current_memory = max(0, self._current_memory - oldest_value.memory_size())
|
|
141
|
+
self._eviction_count += 1
|
|
142
|
+
logger.debug(
|
|
143
|
+
"[file_cache] Evicted entry due to cache limits",
|
|
144
|
+
extra={"evicted_path": oldest_key, "total_evictions": self._eviction_count},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def get(self, key: str, default: Optional[FileSnapshot] = None) -> Optional[FileSnapshot]:
|
|
148
|
+
"""Get a snapshot, returning default if not found."""
|
|
149
|
+
with self._lock:
|
|
150
|
+
if key not in self._cache:
|
|
151
|
+
return default
|
|
152
|
+
self._cache.move_to_end(key)
|
|
153
|
+
return self._cache[key]
|
|
154
|
+
|
|
155
|
+
def pop(self, key: str, default: Optional[FileSnapshot] = None) -> Optional[FileSnapshot]:
|
|
156
|
+
"""Remove and return a snapshot."""
|
|
157
|
+
with self._lock:
|
|
158
|
+
if key not in self._cache:
|
|
159
|
+
return default
|
|
160
|
+
value = self._cache.pop(key)
|
|
161
|
+
self._current_memory = max(0, self._current_memory - value.memory_size())
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
def setdefault(self, key: str, default: FileSnapshot) -> FileSnapshot:
|
|
165
|
+
"""Atomically get or set a snapshot.
|
|
166
|
+
|
|
167
|
+
If key exists, return its value (and mark as recently used).
|
|
168
|
+
If key doesn't exist, set it to default and return default.
|
|
169
|
+
This provides a thread-safe get-or-create operation.
|
|
170
|
+
"""
|
|
171
|
+
with self._lock:
|
|
172
|
+
if key in self._cache:
|
|
173
|
+
self._cache.move_to_end(key)
|
|
174
|
+
return self._cache[key]
|
|
175
|
+
# Key doesn't exist - add it
|
|
176
|
+
new_size = default.memory_size()
|
|
177
|
+
# Evict if needed
|
|
178
|
+
while self._current_memory + new_size > self._max_memory_bytes and self._cache:
|
|
179
|
+
self._evict_oldest()
|
|
180
|
+
while len(self._cache) >= self._max_entries:
|
|
181
|
+
self._evict_oldest()
|
|
182
|
+
self._cache[key] = default
|
|
183
|
+
self._current_memory += new_size
|
|
184
|
+
return default
|
|
185
|
+
|
|
186
|
+
def clear(self) -> None:
|
|
187
|
+
"""Remove all entries from the cache."""
|
|
188
|
+
with self._lock:
|
|
189
|
+
self._cache.clear()
|
|
190
|
+
self._current_memory = 0
|
|
191
|
+
|
|
192
|
+
def keys(self) -> List[str]:
|
|
193
|
+
"""Return list of cached file paths."""
|
|
194
|
+
with self._lock:
|
|
195
|
+
return list(self._cache.keys())
|
|
196
|
+
|
|
197
|
+
def values(self) -> List[FileSnapshot]:
|
|
198
|
+
"""Return list of cached snapshots."""
|
|
199
|
+
with self._lock:
|
|
200
|
+
return list(self._cache.values())
|
|
201
|
+
|
|
202
|
+
def items(self) -> List[Tuple[str, FileSnapshot]]:
|
|
203
|
+
"""Return list of (path, snapshot) pairs."""
|
|
204
|
+
with self._lock:
|
|
205
|
+
return list(self._cache.items())
|
|
206
|
+
|
|
207
|
+
def __iter__(self) -> Iterator[str]:
|
|
208
|
+
"""Iterate over keys (not thread-safe for modifications during iteration)."""
|
|
209
|
+
with self._lock:
|
|
210
|
+
return iter(list(self._cache.keys()))
|
|
211
|
+
|
|
212
|
+
def stats(self) -> Dict[str, int]:
|
|
213
|
+
"""Return cache statistics."""
|
|
214
|
+
with self._lock:
|
|
215
|
+
return {
|
|
216
|
+
"entries": len(self._cache),
|
|
217
|
+
"max_entries": self._max_entries,
|
|
218
|
+
"memory_bytes": self._current_memory,
|
|
219
|
+
"max_memory_bytes": self._max_memory_bytes,
|
|
220
|
+
"eviction_count": self._eviction_count,
|
|
221
|
+
}
|
|
222
|
+
|
|
24
223
|
|
|
25
224
|
@dataclass
|
|
26
225
|
class ChangedFileNotice:
|
|
@@ -30,10 +229,14 @@ class ChangedFileNotice:
|
|
|
30
229
|
summary: str
|
|
31
230
|
|
|
32
231
|
|
|
232
|
+
# Type alias for cache - supports both Dict and BoundedFileCache
|
|
233
|
+
FileCacheType = Dict[str, FileSnapshot] | BoundedFileCache
|
|
234
|
+
|
|
235
|
+
|
|
33
236
|
def record_snapshot(
|
|
34
237
|
file_path: str,
|
|
35
238
|
content: str,
|
|
36
|
-
cache:
|
|
239
|
+
cache: FileCacheType,
|
|
37
240
|
*,
|
|
38
241
|
offset: int = 0,
|
|
39
242
|
limit: Optional[int] = None,
|
|
@@ -79,7 +282,7 @@ def _build_diff_summary(old_content: str, new_content: str, file_path: str, max_
|
|
|
79
282
|
|
|
80
283
|
|
|
81
284
|
def detect_changed_files(
|
|
82
|
-
cache:
|
|
285
|
+
cache: FileCacheType, *, max_diff_lines: int = 80
|
|
83
286
|
) -> List[ChangedFileNotice]:
|
|
84
287
|
"""Return notices for files whose mtime increased since they were read."""
|
|
85
288
|
notices: List[ChangedFileNotice] = []
|
|
@@ -102,10 +305,16 @@ def detect_changed_files(
|
|
|
102
305
|
|
|
103
306
|
try:
|
|
104
307
|
new_content = _read_portion(file_path, snapshot.offset, snapshot.limit)
|
|
105
|
-
except (
|
|
308
|
+
except (
|
|
309
|
+
OSError,
|
|
310
|
+
IOError,
|
|
311
|
+
UnicodeDecodeError,
|
|
312
|
+
ValueError,
|
|
313
|
+
) as exc: # pragma: no cover - best-effort telemetry
|
|
106
314
|
logger.warning(
|
|
107
315
|
"[file_watch] Failed reading changed file: %s: %s",
|
|
108
|
-
type(exc).__name__,
|
|
316
|
+
type(exc).__name__,
|
|
317
|
+
exc,
|
|
109
318
|
extra={"file_path": file_path},
|
|
110
319
|
)
|
|
111
320
|
notices.append(
|
ripperdoc/utils/json_utils.py
CHANGED
|
@@ -21,7 +21,8 @@ def safe_parse_json(json_text: Optional[str], log_error: bool = True) -> Optiona
|
|
|
21
21
|
if log_error:
|
|
22
22
|
logger.debug(
|
|
23
23
|
"[json_utils] Failed to parse JSON: %s: %s",
|
|
24
|
-
type(exc).__name__,
|
|
24
|
+
type(exc).__name__,
|
|
25
|
+
exc,
|
|
25
26
|
extra={"length": len(json_text)},
|
|
26
27
|
)
|
|
27
28
|
return None
|