ripperdoc 0.2.7__py3-none-any.whl → 0.2.9__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 +33 -115
- ripperdoc/cli/commands/__init__.py +70 -6
- ripperdoc/cli/commands/agents_cmd.py +6 -3
- ripperdoc/cli/commands/clear_cmd.py +1 -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/help_cmd.py +11 -1
- ripperdoc/cli/commands/hooks_cmd.py +610 -0
- ripperdoc/cli/commands/models_cmd.py +26 -9
- ripperdoc/cli/commands/permissions_cmd.py +57 -37
- ripperdoc/cli/commands/resume_cmd.py +6 -4
- ripperdoc/cli/commands/status_cmd.py +4 -4
- ripperdoc/cli/commands/tasks_cmd.py +8 -4
- ripperdoc/cli/ui/file_mention_completer.py +64 -8
- ripperdoc/cli/ui/interrupt_handler.py +3 -4
- ripperdoc/cli/ui/message_display.py +5 -3
- ripperdoc/cli/ui/panels.py +13 -10
- ripperdoc/cli/ui/provider_options.py +247 -0
- ripperdoc/cli/ui/rich_ui.py +196 -77
- ripperdoc/cli/ui/spinner.py +25 -1
- ripperdoc/cli/ui/tool_renderers.py +8 -2
- ripperdoc/cli/ui/wizard.py +215 -0
- ripperdoc/core/agents.py +9 -3
- ripperdoc/core/config.py +49 -12
- ripperdoc/core/custom_commands.py +412 -0
- ripperdoc/core/default_tools.py +11 -2
- ripperdoc/core/hooks/__init__.py +99 -0
- ripperdoc/core/hooks/config.py +301 -0
- ripperdoc/core/hooks/events.py +535 -0
- ripperdoc/core/hooks/executor.py +496 -0
- ripperdoc/core/hooks/integration.py +344 -0
- ripperdoc/core/hooks/manager.py +745 -0
- ripperdoc/core/permissions.py +40 -8
- ripperdoc/core/providers/anthropic.py +548 -68
- ripperdoc/core/providers/gemini.py +70 -5
- ripperdoc/core/providers/openai.py +60 -5
- ripperdoc/core/query.py +140 -39
- ripperdoc/core/query_utils.py +2 -0
- ripperdoc/core/skills.py +9 -3
- ripperdoc/core/system_prompt.py +4 -2
- ripperdoc/core/tool.py +9 -5
- ripperdoc/sdk/client.py +2 -2
- ripperdoc/tools/ask_user_question_tool.py +5 -3
- ripperdoc/tools/background_shell.py +2 -1
- ripperdoc/tools/bash_output_tool.py +1 -1
- ripperdoc/tools/bash_tool.py +30 -20
- 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 +8 -4
- ripperdoc/tools/file_read_tool.py +9 -5
- ripperdoc/tools/file_write_tool.py +9 -5
- 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/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 +5 -4
- ripperdoc/tools/todo_tool.py +2 -2
- ripperdoc/tools/tool_search_tool.py +3 -2
- ripperdoc/utils/conversation_compaction.py +11 -7
- ripperdoc/utils/file_watch.py +8 -2
- ripperdoc/utils/json_utils.py +2 -1
- ripperdoc/utils/mcp.py +11 -3
- ripperdoc/utils/memory.py +4 -2
- ripperdoc/utils/message_compaction.py +21 -7
- ripperdoc/utils/message_formatting.py +11 -7
- ripperdoc/utils/messages.py +105 -66
- ripperdoc/utils/path_ignore.py +38 -12
- ripperdoc/utils/permissions/path_validation_utils.py +2 -1
- ripperdoc/utils/permissions/shell_command_validation.py +427 -91
- ripperdoc/utils/safe_get_cwd.py +2 -1
- ripperdoc/utils/session_history.py +13 -6
- ripperdoc/utils/todo.py +2 -1
- ripperdoc/utils/token_estimation.py +6 -1
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/METADATA +24 -3
- ripperdoc-0.2.9.dist-info/RECORD +123 -0
- ripperdoc-0.2.7.dist-info/RECORD +0 -113
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.7.dist-info → ripperdoc-0.2.9.dist-info}/top_level.txt +0 -0
ripperdoc/utils/messages.py
CHANGED
|
@@ -4,7 +4,6 @@ This module provides utilities for creating and normalizing messages
|
|
|
4
4
|
for communication with AI models.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
from typing import Any, Dict, List, Optional, Union
|
|
9
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
9
|
from uuid import uuid4
|
|
@@ -93,7 +92,8 @@ def _content_block_to_openai(block: MessageContent) -> Dict[str, Any]:
|
|
|
93
92
|
except (TypeError, ValueError) as exc:
|
|
94
93
|
logger.warning(
|
|
95
94
|
"[_content_block_to_openai] Failed to serialize tool arguments: %s: %s",
|
|
96
|
-
type(exc).__name__,
|
|
95
|
+
type(exc).__name__,
|
|
96
|
+
exc,
|
|
97
97
|
)
|
|
98
98
|
args_str = "{}"
|
|
99
99
|
tool_call_id = (
|
|
@@ -211,7 +211,8 @@ def create_user_message(
|
|
|
211
211
|
# Fallback: keep as-is if conversion fails
|
|
212
212
|
logger.warning(
|
|
213
213
|
"[create_user_message] Failed to normalize tool_use_result: %s: %s",
|
|
214
|
-
type(exc).__name__,
|
|
214
|
+
type(exc).__name__,
|
|
215
|
+
exc,
|
|
215
216
|
)
|
|
216
217
|
|
|
217
218
|
message = Message(role=MessageRole.USER, content=message_content)
|
|
@@ -268,14 +269,80 @@ def create_progress_message(
|
|
|
268
269
|
)
|
|
269
270
|
|
|
270
271
|
|
|
272
|
+
def _apply_deepseek_reasoning_content(
|
|
273
|
+
normalized: List[Dict[str, Any]],
|
|
274
|
+
is_new_turn: bool = False,
|
|
275
|
+
) -> List[Dict[str, Any]]:
|
|
276
|
+
"""Apply DeepSeek reasoning_content handling to normalized messages.
|
|
277
|
+
|
|
278
|
+
DeepSeek thinking mode requires special handling for tool calls:
|
|
279
|
+
1. During a tool call loop (same turn), reasoning_content MUST be preserved
|
|
280
|
+
in assistant messages that contain tool_calls
|
|
281
|
+
2. When a new user turn starts, we can optionally clear previous reasoning_content
|
|
282
|
+
to save bandwidth (the API will ignore them anyway)
|
|
283
|
+
|
|
284
|
+
According to DeepSeek docs, an assistant message with tool_calls should look like:
|
|
285
|
+
{
|
|
286
|
+
'role': 'assistant',
|
|
287
|
+
'content': response.choices[0].message.content,
|
|
288
|
+
'reasoning_content': response.choices[0].message.reasoning_content,
|
|
289
|
+
'tool_calls': response.choices[0].message.tool_calls,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
normalized: The normalized messages list
|
|
294
|
+
is_new_turn: If True, clear reasoning_content from historical messages
|
|
295
|
+
to save network bandwidth
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
The processed messages list
|
|
299
|
+
"""
|
|
300
|
+
if not normalized:
|
|
301
|
+
return normalized
|
|
302
|
+
|
|
303
|
+
# Find the last user message index to determine the current turn boundary
|
|
304
|
+
last_user_idx = -1
|
|
305
|
+
for idx in range(len(normalized) - 1, -1, -1):
|
|
306
|
+
if normalized[idx].get("role") == "user":
|
|
307
|
+
last_user_idx = idx
|
|
308
|
+
break
|
|
309
|
+
|
|
310
|
+
if is_new_turn and last_user_idx > 0:
|
|
311
|
+
# Clear reasoning_content from messages before the last user message
|
|
312
|
+
# This is optional but recommended by DeepSeek to save bandwidth
|
|
313
|
+
for idx in range(last_user_idx):
|
|
314
|
+
msg = normalized[idx]
|
|
315
|
+
if msg.get("role") == "assistant" and "reasoning_content" in msg:
|
|
316
|
+
# Set to None instead of deleting to match DeepSeek's example
|
|
317
|
+
msg["reasoning_content"] = None
|
|
318
|
+
|
|
319
|
+
# Validate: ensure all assistant messages with tool_calls have reasoning_content
|
|
320
|
+
# within the current turn (after last_user_idx)
|
|
321
|
+
for idx in range(max(0, last_user_idx), len(normalized)):
|
|
322
|
+
msg = normalized[idx]
|
|
323
|
+
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
324
|
+
if "reasoning_content" not in msg:
|
|
325
|
+
# This is a problem - DeepSeek requires reasoning_content for tool_calls
|
|
326
|
+
logger.warning(
|
|
327
|
+
f"[deepseek] Assistant message at index {idx} has tool_calls "
|
|
328
|
+
f"but missing reasoning_content - this may cause API errors"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return normalized
|
|
332
|
+
|
|
333
|
+
|
|
271
334
|
def normalize_messages_for_api(
|
|
272
335
|
messages: List[Union[UserMessage, AssistantMessage, ProgressMessage]],
|
|
273
336
|
protocol: str = "anthropic",
|
|
274
337
|
tool_mode: str = "native",
|
|
338
|
+
thinking_mode: Optional[str] = None,
|
|
275
339
|
) -> List[Dict[str, Any]]:
|
|
276
340
|
"""Normalize messages for API submission.
|
|
277
341
|
|
|
278
342
|
Progress messages are filtered out as they are not sent to the API.
|
|
343
|
+
|
|
344
|
+
For DeepSeek thinking mode, this function ensures reasoning_content is properly
|
|
345
|
+
included in assistant messages that contain tool_calls, as required by the API.
|
|
279
346
|
"""
|
|
280
347
|
|
|
281
348
|
def _msg_type(msg: Any) -> Optional[str]:
|
|
@@ -318,58 +385,6 @@ def normalize_messages_for_api(
|
|
|
318
385
|
return meta_dict
|
|
319
386
|
return {}
|
|
320
387
|
|
|
321
|
-
def _block_type(block: Any) -> Optional[str]:
|
|
322
|
-
if hasattr(block, "type"):
|
|
323
|
-
return getattr(block, "type", None)
|
|
324
|
-
if isinstance(block, dict):
|
|
325
|
-
return block.get("type")
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
def _block_attr(block: Any, attr: str, default: Any = None) -> Any:
|
|
329
|
-
if hasattr(block, attr):
|
|
330
|
-
return getattr(block, attr, default)
|
|
331
|
-
if isinstance(block, dict):
|
|
332
|
-
return block.get(attr, default)
|
|
333
|
-
return default
|
|
334
|
-
|
|
335
|
-
def _flatten_blocks_to_text(blocks: List[Any]) -> str:
|
|
336
|
-
parts: List[str] = []
|
|
337
|
-
for blk in blocks:
|
|
338
|
-
btype = _block_type(blk)
|
|
339
|
-
if btype == "text":
|
|
340
|
-
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
341
|
-
if text:
|
|
342
|
-
parts.append(str(text))
|
|
343
|
-
elif btype == "tool_result":
|
|
344
|
-
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
345
|
-
tool_id = _block_attr(blk, "tool_use_id") or _block_attr(blk, "id")
|
|
346
|
-
prefix = "Tool error" if _block_attr(blk, "is_error") else "Tool result"
|
|
347
|
-
label = f"{prefix}{f' ({tool_id})' if tool_id else ''}"
|
|
348
|
-
parts.append(f"{label}: {text}" if text else label)
|
|
349
|
-
elif btype == "tool_use":
|
|
350
|
-
name = _block_attr(blk, "name") or ""
|
|
351
|
-
input_data = _block_attr(blk, "input")
|
|
352
|
-
input_preview = ""
|
|
353
|
-
if input_data not in (None, {}):
|
|
354
|
-
try:
|
|
355
|
-
input_preview = json.dumps(input_data)
|
|
356
|
-
except (TypeError, ValueError):
|
|
357
|
-
input_preview = str(input_data)
|
|
358
|
-
tool_id = _block_attr(blk, "tool_use_id") or _block_attr(blk, "id")
|
|
359
|
-
desc = "Tool call"
|
|
360
|
-
if name:
|
|
361
|
-
desc += f" {name}"
|
|
362
|
-
if tool_id:
|
|
363
|
-
desc += f" ({tool_id})"
|
|
364
|
-
if input_preview:
|
|
365
|
-
desc += f": {input_preview}"
|
|
366
|
-
parts.append(desc)
|
|
367
|
-
else:
|
|
368
|
-
text = _block_attr(blk, "text") or _block_attr(blk, "content") or ""
|
|
369
|
-
if text:
|
|
370
|
-
parts.append(str(text))
|
|
371
|
-
return "\n".join(p for p in parts if p)
|
|
372
|
-
|
|
373
388
|
effective_tool_mode = (tool_mode or "native").lower()
|
|
374
389
|
if effective_tool_mode not in {"native", "text"}:
|
|
375
390
|
effective_tool_mode = "native"
|
|
@@ -426,7 +441,9 @@ def normalize_messages_for_api(
|
|
|
426
441
|
if block_type == "tool_result":
|
|
427
442
|
tool_results_seen += 1
|
|
428
443
|
# Skip tool_result blocks that lack a preceding tool_use
|
|
429
|
-
tool_id = getattr(block, "tool_use_id", None) or getattr(
|
|
444
|
+
tool_id = getattr(block, "tool_use_id", None) or getattr(
|
|
445
|
+
block, "id", None
|
|
446
|
+
)
|
|
430
447
|
if not tool_id:
|
|
431
448
|
skipped_tool_results_no_call += 1
|
|
432
449
|
continue
|
|
@@ -486,19 +503,35 @@ def normalize_messages_for_api(
|
|
|
486
503
|
mapped = _content_block_to_openai(block)
|
|
487
504
|
if mapped:
|
|
488
505
|
assistant_openai_msgs.append(mapped)
|
|
489
|
-
if text_parts:
|
|
490
|
-
assistant_openai_msgs.append(
|
|
491
|
-
{"role": "assistant", "content": "\n".join(text_parts)}
|
|
492
|
-
)
|
|
493
506
|
if tool_calls:
|
|
507
|
+
# For DeepSeek thinking mode, we must include reasoning_content
|
|
508
|
+
# in the assistant message that contains tool_calls
|
|
509
|
+
tool_call_msg: Dict[str, Any] = {
|
|
510
|
+
"role": "assistant",
|
|
511
|
+
"content": "\n".join(text_parts) if text_parts else None,
|
|
512
|
+
"tool_calls": tool_calls,
|
|
513
|
+
}
|
|
514
|
+
# Add reasoning_content if present (required for DeepSeek thinking mode)
|
|
515
|
+
reasoning_content = meta.get("reasoning_content") if meta else None
|
|
516
|
+
if reasoning_content is not None:
|
|
517
|
+
tool_call_msg["reasoning_content"] = reasoning_content
|
|
518
|
+
logger.debug(
|
|
519
|
+
f"[normalize_messages_for_api] Added reasoning_content to "
|
|
520
|
+
f"tool_call message (len={len(str(reasoning_content))})"
|
|
521
|
+
)
|
|
522
|
+
elif thinking_mode == "deepseek":
|
|
523
|
+
logger.warning(
|
|
524
|
+
f"[normalize_messages_for_api] DeepSeek mode: assistant "
|
|
525
|
+
f"message with tool_calls but no reasoning_content in metadata. "
|
|
526
|
+
f"meta_keys={list(meta.keys()) if meta else []}"
|
|
527
|
+
)
|
|
528
|
+
assistant_openai_msgs.append(tool_call_msg)
|
|
529
|
+
elif text_parts:
|
|
494
530
|
assistant_openai_msgs.append(
|
|
495
|
-
{
|
|
496
|
-
"role": "assistant",
|
|
497
|
-
"content": None,
|
|
498
|
-
"tool_calls": tool_calls,
|
|
499
|
-
}
|
|
531
|
+
{"role": "assistant", "content": "\n".join(text_parts)}
|
|
500
532
|
)
|
|
501
|
-
|
|
533
|
+
# For non-tool-call messages, add reasoning metadata to the last message
|
|
534
|
+
if meta and assistant_openai_msgs and not tool_calls:
|
|
502
535
|
for key in ("reasoning_content", "reasoning_details", "reasoning"):
|
|
503
536
|
if key in meta and meta[key] is not None:
|
|
504
537
|
assistant_openai_msgs[-1][key] = meta[key]
|
|
@@ -515,6 +548,7 @@ def normalize_messages_for_api(
|
|
|
515
548
|
|
|
516
549
|
logger.debug(
|
|
517
550
|
f"[normalize_messages_for_api] protocol={protocol} tool_mode={effective_tool_mode} "
|
|
551
|
+
f"thinking_mode={thinking_mode} "
|
|
518
552
|
f"input_msgs={len(messages)} normalized={len(normalized)} "
|
|
519
553
|
f"tool_results_seen={tool_results_seen} tool_uses_seen={tool_uses_seen} "
|
|
520
554
|
f"tool_result_positions={len(tool_result_positions)} "
|
|
@@ -523,6 +557,11 @@ def normalize_messages_for_api(
|
|
|
523
557
|
f"skipped_tool_uses_no_id={skipped_tool_uses_no_id} "
|
|
524
558
|
f"skipped_tool_results_no_call={skipped_tool_results_no_call}"
|
|
525
559
|
)
|
|
560
|
+
|
|
561
|
+
# Apply DeepSeek-specific reasoning_content handling
|
|
562
|
+
if thinking_mode == "deepseek":
|
|
563
|
+
normalized = _apply_deepseek_reasoning_content(normalized, is_new_turn=False)
|
|
564
|
+
|
|
526
565
|
return normalized
|
|
527
566
|
|
|
528
567
|
|
ripperdoc/utils/path_ignore.py
CHANGED
|
@@ -11,8 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import re
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Dict, List, Optional, Set, Tuple
|
|
15
|
-
from functools import lru_cache
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
16
15
|
|
|
17
16
|
from ripperdoc.utils.git_utils import (
|
|
18
17
|
get_git_root,
|
|
@@ -287,7 +286,7 @@ def _compile_pattern(pattern: str) -> re.Pattern[str]:
|
|
|
287
286
|
while j < len(pattern) and pattern[j] != "]":
|
|
288
287
|
j += 1
|
|
289
288
|
if j < len(pattern):
|
|
290
|
-
regex += pattern[i:j + 1]
|
|
289
|
+
regex += pattern[i : j + 1]
|
|
291
290
|
i = j
|
|
292
291
|
else:
|
|
293
292
|
regex += re.escape(c)
|
|
@@ -361,7 +360,7 @@ class IgnoreFilter:
|
|
|
361
360
|
|
|
362
361
|
return result
|
|
363
362
|
|
|
364
|
-
def test(self, path: str) -> Dict[str,
|
|
363
|
+
def test(self, path: str) -> Dict[str, Any]:
|
|
365
364
|
"""Check if a path should be ignored and return details.
|
|
366
365
|
|
|
367
366
|
Returns:
|
|
@@ -369,7 +368,7 @@ class IgnoreFilter:
|
|
|
369
368
|
"""
|
|
370
369
|
path = path.replace("\\", "/").strip("/")
|
|
371
370
|
|
|
372
|
-
result = {"ignored": False, "rule": None}
|
|
371
|
+
result: Dict[str, Any] = {"ignored": False, "rule": None}
|
|
373
372
|
|
|
374
373
|
for pattern, is_negation in self._patterns:
|
|
375
374
|
if pattern.search(path):
|
|
@@ -384,7 +383,9 @@ class IgnoreFilter:
|
|
|
384
383
|
# =============================================================================
|
|
385
384
|
|
|
386
385
|
|
|
387
|
-
def parse_ignore_pattern(
|
|
386
|
+
def parse_ignore_pattern(
|
|
387
|
+
pattern: str, settings_path: Optional[Path] = None
|
|
388
|
+
) -> Tuple[str, Optional[Path]]:
|
|
388
389
|
"""Parse an ignore pattern and return (relative_pattern, root_path).
|
|
389
390
|
|
|
390
391
|
Supports prefixes:
|
|
@@ -504,6 +505,7 @@ def is_path_ignored(
|
|
|
504
505
|
file_path = Path(file_path)
|
|
505
506
|
if not file_path.is_absolute():
|
|
506
507
|
from ripperdoc.utils.safe_get_cwd import safe_get_cwd
|
|
508
|
+
|
|
507
509
|
file_path = Path(safe_get_cwd()) / file_path
|
|
508
510
|
|
|
509
511
|
file_path = file_path.resolve()
|
|
@@ -513,6 +515,7 @@ def is_path_ignored(
|
|
|
513
515
|
root_path = get_git_root(file_path.parent)
|
|
514
516
|
if root_path is None:
|
|
515
517
|
from ripperdoc.utils.safe_get_cwd import safe_get_cwd
|
|
518
|
+
|
|
516
519
|
root_path = Path(safe_get_cwd())
|
|
517
520
|
|
|
518
521
|
root_path = root_path.resolve()
|
|
@@ -629,12 +632,35 @@ def check_path_for_tool(
|
|
|
629
632
|
# Check if it's a binary/media file
|
|
630
633
|
suffix = file_path.suffix.lower()
|
|
631
634
|
binary_extensions = {
|
|
632
|
-
".png",
|
|
633
|
-
".
|
|
634
|
-
".
|
|
635
|
-
".
|
|
636
|
-
".
|
|
637
|
-
".
|
|
635
|
+
".png",
|
|
636
|
+
".jpg",
|
|
637
|
+
".jpeg",
|
|
638
|
+
".gif",
|
|
639
|
+
".bmp",
|
|
640
|
+
".ico",
|
|
641
|
+
".webp",
|
|
642
|
+
".mp4",
|
|
643
|
+
".avi",
|
|
644
|
+
".mkv",
|
|
645
|
+
".mov",
|
|
646
|
+
".mp3",
|
|
647
|
+
".wav",
|
|
648
|
+
".flac",
|
|
649
|
+
".zip",
|
|
650
|
+
".tar",
|
|
651
|
+
".gz",
|
|
652
|
+
".7z",
|
|
653
|
+
".rar",
|
|
654
|
+
".exe",
|
|
655
|
+
".dll",
|
|
656
|
+
".so",
|
|
657
|
+
".dylib",
|
|
658
|
+
".db",
|
|
659
|
+
".sqlite",
|
|
660
|
+
".parquet",
|
|
661
|
+
".ttf",
|
|
662
|
+
".otf",
|
|
663
|
+
".woff",
|
|
638
664
|
}
|
|
639
665
|
if suffix in binary_extensions:
|
|
640
666
|
reasons.append("binary/media file")
|
|
@@ -51,7 +51,8 @@ def _resolve_path(raw_path: str, cwd: str) -> Path:
|
|
|
51
51
|
except (OSError, ValueError) as exc:
|
|
52
52
|
logger.warning(
|
|
53
53
|
"[path_validation] Failed to resolve path: %s: %s",
|
|
54
|
-
type(exc).__name__,
|
|
54
|
+
type(exc).__name__,
|
|
55
|
+
exc,
|
|
55
56
|
extra={"raw_path": raw_path, "cwd": cwd},
|
|
56
57
|
)
|
|
57
58
|
return candidate
|