ripperdoc 0.2.4__py3-none-any.whl → 0.2.5__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 (75) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/__main__.py +0 -5
  3. ripperdoc/cli/cli.py +37 -16
  4. ripperdoc/cli/commands/__init__.py +2 -0
  5. ripperdoc/cli/commands/agents_cmd.py +12 -9
  6. ripperdoc/cli/commands/compact_cmd.py +7 -3
  7. ripperdoc/cli/commands/context_cmd.py +33 -13
  8. ripperdoc/cli/commands/doctor_cmd.py +27 -14
  9. ripperdoc/cli/commands/exit_cmd.py +1 -1
  10. ripperdoc/cli/commands/mcp_cmd.py +13 -8
  11. ripperdoc/cli/commands/memory_cmd.py +5 -5
  12. ripperdoc/cli/commands/models_cmd.py +47 -16
  13. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  14. ripperdoc/cli/commands/resume_cmd.py +1 -2
  15. ripperdoc/cli/commands/tasks_cmd.py +24 -13
  16. ripperdoc/cli/ui/rich_ui.py +500 -406
  17. ripperdoc/cli/ui/tool_renderers.py +298 -0
  18. ripperdoc/core/agents.py +17 -9
  19. ripperdoc/core/config.py +130 -6
  20. ripperdoc/core/default_tools.py +7 -2
  21. ripperdoc/core/permissions.py +20 -14
  22. ripperdoc/core/providers/anthropic.py +107 -4
  23. ripperdoc/core/providers/base.py +33 -4
  24. ripperdoc/core/providers/gemini.py +169 -50
  25. ripperdoc/core/providers/openai.py +257 -23
  26. ripperdoc/core/query.py +294 -61
  27. ripperdoc/core/query_utils.py +50 -6
  28. ripperdoc/core/skills.py +295 -0
  29. ripperdoc/core/system_prompt.py +13 -7
  30. ripperdoc/core/tool.py +8 -6
  31. ripperdoc/sdk/client.py +14 -1
  32. ripperdoc/tools/ask_user_question_tool.py +20 -22
  33. ripperdoc/tools/background_shell.py +19 -13
  34. ripperdoc/tools/bash_tool.py +356 -209
  35. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  36. ripperdoc/tools/enter_plan_mode_tool.py +5 -2
  37. ripperdoc/tools/exit_plan_mode_tool.py +6 -3
  38. ripperdoc/tools/file_edit_tool.py +53 -10
  39. ripperdoc/tools/file_read_tool.py +17 -7
  40. ripperdoc/tools/file_write_tool.py +49 -13
  41. ripperdoc/tools/glob_tool.py +10 -9
  42. ripperdoc/tools/grep_tool.py +182 -51
  43. ripperdoc/tools/ls_tool.py +6 -6
  44. ripperdoc/tools/mcp_tools.py +106 -456
  45. ripperdoc/tools/multi_edit_tool.py +49 -9
  46. ripperdoc/tools/notebook_edit_tool.py +57 -13
  47. ripperdoc/tools/skill_tool.py +205 -0
  48. ripperdoc/tools/task_tool.py +7 -8
  49. ripperdoc/tools/todo_tool.py +12 -12
  50. ripperdoc/tools/tool_search_tool.py +5 -6
  51. ripperdoc/utils/coerce.py +34 -0
  52. ripperdoc/utils/context_length_errors.py +252 -0
  53. ripperdoc/utils/file_watch.py +5 -4
  54. ripperdoc/utils/json_utils.py +4 -4
  55. ripperdoc/utils/log.py +3 -3
  56. ripperdoc/utils/mcp.py +36 -15
  57. ripperdoc/utils/memory.py +9 -6
  58. ripperdoc/utils/message_compaction.py +16 -11
  59. ripperdoc/utils/messages.py +73 -8
  60. ripperdoc/utils/path_ignore.py +677 -0
  61. ripperdoc/utils/permissions/__init__.py +7 -1
  62. ripperdoc/utils/permissions/path_validation_utils.py +5 -3
  63. ripperdoc/utils/permissions/shell_command_validation.py +496 -18
  64. ripperdoc/utils/prompt.py +1 -1
  65. ripperdoc/utils/safe_get_cwd.py +5 -2
  66. ripperdoc/utils/session_history.py +38 -19
  67. ripperdoc/utils/todo.py +6 -2
  68. ripperdoc/utils/token_estimation.py +4 -3
  69. {ripperdoc-0.2.4.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +12 -1
  70. ripperdoc-0.2.5.dist-info/RECORD +107 -0
  71. ripperdoc-0.2.4.dist-info/RECORD +0 -99
  72. {ripperdoc-0.2.4.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
  73. {ripperdoc-0.2.4.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
  74. {ripperdoc-0.2.4.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
  75. {ripperdoc-0.2.4.dist-info → ripperdoc-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,677 @@
1
+ """Path ignore utilities for Ripperdoc.
2
+
3
+ This module implements comprehensive path ignore checking based on:
4
+ 1. Default ignore patterns (binary files, build outputs, etc.)
5
+ 2. .gitignore patterns
6
+ 3. Project configuration ignore patterns
7
+ 4. User-defined ignore patterns
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Set, Tuple
15
+ from functools import lru_cache
16
+
17
+ from ripperdoc.utils.git_utils import (
18
+ get_git_root,
19
+ is_git_repository,
20
+ read_gitignore_patterns,
21
+ )
22
+
23
+
24
+ # =============================================================================
25
+ # Default Ignore Patterns (System-level)
26
+ # =============================================================================
27
+
28
+ # These patterns are always ignored for safety and performance
29
+ DEFAULT_IGNORE_PATTERNS: List[str] = [
30
+ # Version control
31
+ ".git/",
32
+ ".svn/",
33
+ ".hg/",
34
+ # IDE and editor
35
+ ".idea/",
36
+ ".vscode/",
37
+ "*.swp",
38
+ "*.swo",
39
+ "*~",
40
+ # Ripperdoc config
41
+ ".ripperdoc/",
42
+ ".claude/",
43
+ # Build and cache directories
44
+ ".parcel-cache/",
45
+ ".pytest_cache/",
46
+ ".nuxt/",
47
+ ".next/",
48
+ ".sass-cache/",
49
+ "__pycache__/",
50
+ "*.pyc",
51
+ "*.pyo",
52
+ # Node.js
53
+ "node_modules/",
54
+ # Python environments
55
+ "venv/",
56
+ ".venv/",
57
+ "env/",
58
+ ".env/",
59
+ ".tox/",
60
+ # Java/Gradle
61
+ ".gradle/",
62
+ "build/",
63
+ "target/",
64
+ # .NET
65
+ "bin/",
66
+ "obj/",
67
+ # Rust
68
+ "target/",
69
+ # Go
70
+ "vendor/",
71
+ # Ruby
72
+ "vendor/bundle/",
73
+ # Dart/Flutter
74
+ ".dart_tool/",
75
+ ".pub-cache/",
76
+ # Elixir
77
+ "_build/",
78
+ "deps/",
79
+ # Haskell
80
+ "dist-newstyle/",
81
+ # JavaScript/TypeScript build outputs
82
+ "dist/",
83
+ # Deno
84
+ ".deno/",
85
+ # Bower (legacy)
86
+ "bower_components/",
87
+ # Image files
88
+ "*.png",
89
+ "*.jpg",
90
+ "*.jpeg",
91
+ "*.gif",
92
+ "*.bmp",
93
+ "*.ico",
94
+ "*.webp",
95
+ "*.svg",
96
+ "*.psd",
97
+ "*.ai",
98
+ "*.eps",
99
+ "*.tiff",
100
+ "*.tif",
101
+ "*.avif",
102
+ "*.heic",
103
+ "*.heif",
104
+ # Video files
105
+ "*.mp4",
106
+ "*.avi",
107
+ "*.mkv",
108
+ "*.mov",
109
+ "*.wmv",
110
+ "*.flv",
111
+ "*.webm",
112
+ "*.m4v",
113
+ "*.mpeg",
114
+ "*.mpg",
115
+ "*.3gp",
116
+ "*.3g2",
117
+ # Audio files
118
+ "*.mp3",
119
+ "*.wav",
120
+ "*.flac",
121
+ "*.aac",
122
+ "*.ogg",
123
+ "*.wma",
124
+ "*.m4a",
125
+ "*.aiff",
126
+ "*.asf",
127
+ # Compressed archives
128
+ "*.zip",
129
+ "*.tar",
130
+ "*.gz",
131
+ "*.bz2",
132
+ "*.xz",
133
+ "*.7z",
134
+ "*.rar",
135
+ "*.tgz",
136
+ # Executable and binary files
137
+ "*.exe",
138
+ "*.dll",
139
+ "*.so",
140
+ "*.dylib",
141
+ "*.bin",
142
+ "*.app",
143
+ "*.dmg",
144
+ "*.msi",
145
+ "*.deb",
146
+ "*.rpm",
147
+ # Database files
148
+ "*.db",
149
+ "*.sqlite",
150
+ "*.sqlite3",
151
+ "*.parquet",
152
+ "*.orc",
153
+ "*.arrow",
154
+ # GIS data
155
+ "*.shp",
156
+ "*.kmz",
157
+ "*.kml",
158
+ "*.dem",
159
+ "*.las",
160
+ "*.laz",
161
+ # CAD/Design files
162
+ "*.dwg",
163
+ "*.dxf",
164
+ # PDF (can be read but often large)
165
+ # "*.pdf", # Keeping PDF readable since it's often documentation
166
+ # Font files
167
+ "*.ttf",
168
+ "*.otf",
169
+ "*.woff",
170
+ "*.woff2",
171
+ "*.eot",
172
+ # Lock files (usually large and auto-generated)
173
+ "package-lock.json",
174
+ "yarn.lock",
175
+ "pnpm-lock.yaml",
176
+ "poetry.lock",
177
+ "Cargo.lock",
178
+ "Gemfile.lock",
179
+ "composer.lock",
180
+ # Large data files
181
+ "*.csv", # Can be very large
182
+ "*.jsonl",
183
+ "*.ndjson",
184
+ # ML model files
185
+ "*.pt",
186
+ "*.pth",
187
+ "*.onnx",
188
+ "*.h5",
189
+ "*.hdf5",
190
+ "*.safetensors",
191
+ "*.ckpt",
192
+ "*.pkl",
193
+ "*.pickle",
194
+ ]
195
+
196
+ # Directories that are always skipped during traversal (fast path)
197
+ IGNORED_DIRECTORIES: Set[str] = {
198
+ "node_modules",
199
+ "vendor/bundle",
200
+ "vendor",
201
+ "venv",
202
+ "env",
203
+ ".venv",
204
+ ".env",
205
+ ".tox",
206
+ "target",
207
+ "build",
208
+ ".gradle",
209
+ "packages",
210
+ "bin",
211
+ "obj",
212
+ ".build",
213
+ ".dart_tool",
214
+ ".pub-cache",
215
+ "_build",
216
+ "deps",
217
+ "dist",
218
+ "dist-newstyle",
219
+ ".deno",
220
+ "bower_components",
221
+ "__pycache__",
222
+ ".git",
223
+ ".svn",
224
+ ".hg",
225
+ ".idea",
226
+ ".vscode",
227
+ ".ripperdoc",
228
+ ".claude",
229
+ }
230
+
231
+
232
+ # =============================================================================
233
+ # Pattern Matching Implementation
234
+ # =============================================================================
235
+
236
+
237
+ def _compile_pattern(pattern: str) -> re.Pattern[str]:
238
+ """Compile a gitignore-style pattern to a regex.
239
+
240
+ This supports basic gitignore syntax:
241
+ - * matches anything except /
242
+ - ** matches anything including /
243
+ - ? matches any single character
244
+ - [abc] character classes
245
+ - ! at start negates the pattern
246
+ - / at end matches directories only
247
+ - / at start anchors to root
248
+ """
249
+ # Remove trailing slashes for matching (we handle directory-only patterns separately)
250
+ is_dir_only = pattern.endswith("/")
251
+ if is_dir_only:
252
+ pattern = pattern[:-1]
253
+
254
+ # Check if pattern is anchored to root
255
+ is_anchored = pattern.startswith("/")
256
+ if is_anchored:
257
+ pattern = pattern[1:]
258
+
259
+ # Escape special regex characters (except our wildcards)
260
+ regex = ""
261
+ i = 0
262
+ while i < len(pattern):
263
+ c = pattern[i]
264
+
265
+ if c == "*":
266
+ # Check for **
267
+ if i + 1 < len(pattern) and pattern[i + 1] == "*":
268
+ # ** matches anything including path separators
269
+ if i + 2 < len(pattern) and pattern[i + 2] == "/":
270
+ regex += "(?:.*/)?"
271
+ i += 3
272
+ continue
273
+ else:
274
+ regex += ".*"
275
+ i += 2
276
+ continue
277
+ else:
278
+ # * matches anything except /
279
+ regex += "[^/]*"
280
+ elif c == "?":
281
+ regex += "[^/]"
282
+ elif c == "[":
283
+ # Find closing bracket
284
+ j = i + 1
285
+ if j < len(pattern) and pattern[j] in "!^":
286
+ j += 1
287
+ while j < len(pattern) and pattern[j] != "]":
288
+ j += 1
289
+ if j < len(pattern):
290
+ regex += pattern[i:j + 1]
291
+ i = j
292
+ else:
293
+ regex += re.escape(c)
294
+ elif c in ".^$+{}|()":
295
+ regex += re.escape(c)
296
+ else:
297
+ regex += c
298
+
299
+ i += 1
300
+
301
+ # Build final regex
302
+ if is_anchored:
303
+ final_regex = f"^{regex}"
304
+ else:
305
+ # Non-anchored patterns can match anywhere in the path
306
+ final_regex = f"(?:^|/){regex}"
307
+
308
+ if is_dir_only:
309
+ final_regex += "(?:/|$)"
310
+ else:
311
+ final_regex += "(?:/.*)?$"
312
+
313
+ return re.compile(final_regex)
314
+
315
+
316
+ class IgnoreFilter:
317
+ """A filter for checking if paths should be ignored.
318
+
319
+ Uses gitignore-style pattern matching.
320
+ """
321
+
322
+ def __init__(self) -> None:
323
+ self._patterns: List[Tuple[re.Pattern[str], bool]] = [] # (pattern, is_negation)
324
+
325
+ def add(self, patterns: List[str]) -> "IgnoreFilter":
326
+ """Add patterns to the filter."""
327
+ for pattern in patterns:
328
+ pattern = pattern.strip()
329
+ if not pattern or pattern.startswith("#"):
330
+ continue
331
+
332
+ is_negation = pattern.startswith("!")
333
+ if is_negation:
334
+ pattern = pattern[1:]
335
+
336
+ try:
337
+ compiled = _compile_pattern(pattern)
338
+ self._patterns.append((compiled, is_negation))
339
+ except re.error:
340
+ # Skip invalid patterns
341
+ pass
342
+
343
+ return self
344
+
345
+ def ignores(self, path: str) -> bool:
346
+ """Check if a path should be ignored.
347
+
348
+ Args:
349
+ path: Relative path to check (using / as separator)
350
+
351
+ Returns:
352
+ True if the path should be ignored
353
+ """
354
+ # Normalize path
355
+ path = path.replace("\\", "/").strip("/")
356
+
357
+ result = False
358
+ for pattern, is_negation in self._patterns:
359
+ if pattern.search(path):
360
+ result = not is_negation
361
+
362
+ return result
363
+
364
+ def test(self, path: str) -> Dict[str, any]:
365
+ """Check if a path should be ignored and return details.
366
+
367
+ Returns:
368
+ Dict with 'ignored' bool and 'rule' if matched
369
+ """
370
+ path = path.replace("\\", "/").strip("/")
371
+
372
+ result = {"ignored": False, "rule": None}
373
+
374
+ for pattern, is_negation in self._patterns:
375
+ if pattern.search(path):
376
+ result["ignored"] = not is_negation
377
+ result["rule"] = {"pattern": pattern.pattern, "negation": is_negation}
378
+
379
+ return result
380
+
381
+
382
+ # =============================================================================
383
+ # Ignore Pattern Management
384
+ # =============================================================================
385
+
386
+
387
+ def parse_ignore_pattern(pattern: str, settings_path: Optional[Path] = None) -> Tuple[str, Optional[Path]]:
388
+ """Parse an ignore pattern and return (relative_pattern, root_path).
389
+
390
+ Supports prefixes:
391
+ - // - Global pattern (from filesystem root)
392
+ - ~/ - Pattern relative to home directory
393
+ - / - Pattern relative to settings file directory
394
+ - (no prefix) - Pattern applies to any directory
395
+
396
+ Args:
397
+ pattern: The ignore pattern to parse
398
+ settings_path: Path to the settings file (for / prefix patterns)
399
+
400
+ Returns:
401
+ Tuple of (relative_pattern, root_path or None)
402
+ """
403
+ pattern = pattern.strip()
404
+
405
+ # // - Global pattern from filesystem root
406
+ if pattern.startswith("//"):
407
+ return pattern[1:], Path("/")
408
+
409
+ # ~/ - Pattern relative to home directory
410
+ if pattern.startswith("~/"):
411
+ return pattern[2:], Path.home()
412
+
413
+ # / - Pattern relative to settings file directory
414
+ if pattern.startswith("/") and settings_path:
415
+ # Determine if settings_path is a file or directory based on suffix
416
+ # If it has a file-like suffix (e.g., .json), treat as file
417
+ if settings_path.suffix:
418
+ return pattern[1:], settings_path.parent
419
+ else:
420
+ return pattern[1:], settings_path
421
+
422
+ # No prefix - applies to any directory
423
+ return pattern, None
424
+
425
+
426
+ def build_ignore_filter(
427
+ root_path: Path,
428
+ user_patterns: Optional[List[str]] = None,
429
+ project_patterns: Optional[List[str]] = None,
430
+ include_defaults: bool = True,
431
+ include_gitignore: bool = True,
432
+ ) -> IgnoreFilter:
433
+ """Build an ignore filter with all applicable patterns.
434
+
435
+ Args:
436
+ root_path: The root path for pattern matching
437
+ user_patterns: User-provided patterns
438
+ project_patterns: Project configuration patterns
439
+ include_defaults: Whether to include default ignore patterns
440
+ include_gitignore: Whether to include .gitignore patterns
441
+
442
+ Returns:
443
+ Configured IgnoreFilter instance
444
+ """
445
+ ignore_filter = IgnoreFilter()
446
+ all_patterns: List[str] = []
447
+
448
+ # 1. Add default patterns
449
+ if include_defaults:
450
+ all_patterns.extend(DEFAULT_IGNORE_PATTERNS)
451
+
452
+ # 2. Add gitignore patterns
453
+ if include_gitignore and is_git_repository(root_path):
454
+ gitignore_patterns = read_gitignore_patterns(root_path)
455
+ all_patterns.extend(gitignore_patterns)
456
+
457
+ # 3. Add project patterns
458
+ if project_patterns:
459
+ all_patterns.extend(project_patterns)
460
+
461
+ # 4. Add user patterns
462
+ if user_patterns:
463
+ all_patterns.extend(user_patterns)
464
+
465
+ ignore_filter.add(all_patterns)
466
+ return ignore_filter
467
+
468
+
469
+ def get_project_ignore_patterns() -> List[str]:
470
+ """Get ignore patterns from project configuration.
471
+
472
+ Returns patterns from ProjectConfig.ignore_patterns if configured.
473
+ """
474
+ try:
475
+ from ripperdoc.core.config import config_manager
476
+
477
+ project_config = config_manager.get_project_config()
478
+ return getattr(project_config, "ignore_patterns", []) or []
479
+ except (ImportError, AttributeError):
480
+ return []
481
+
482
+
483
+ # =============================================================================
484
+ # Path Checking Functions
485
+ # =============================================================================
486
+
487
+
488
+ def is_path_ignored(
489
+ file_path: Path,
490
+ root_path: Optional[Path] = None,
491
+ ignore_filter: Optional[IgnoreFilter] = None,
492
+ ) -> bool:
493
+ """Check if a file path should be ignored.
494
+
495
+ Args:
496
+ file_path: The file path to check
497
+ root_path: The root path for relative matching (defaults to git root or cwd)
498
+ ignore_filter: Pre-built ignore filter (if None, builds a new one)
499
+
500
+ Returns:
501
+ True if the path should be ignored
502
+ """
503
+ # Resolve paths
504
+ file_path = Path(file_path)
505
+ if not file_path.is_absolute():
506
+ from ripperdoc.utils.safe_get_cwd import safe_get_cwd
507
+ file_path = Path(safe_get_cwd()) / file_path
508
+
509
+ file_path = file_path.resolve()
510
+
511
+ # Determine root path
512
+ if root_path is None:
513
+ root_path = get_git_root(file_path.parent)
514
+ if root_path is None:
515
+ from ripperdoc.utils.safe_get_cwd import safe_get_cwd
516
+ root_path = Path(safe_get_cwd())
517
+
518
+ root_path = root_path.resolve()
519
+
520
+ # Get relative path
521
+ try:
522
+ rel_path = file_path.relative_to(root_path).as_posix()
523
+ except ValueError:
524
+ # Path is not under root, not ignored
525
+ return False
526
+
527
+ # Build filter if not provided
528
+ if ignore_filter is None:
529
+ project_patterns = get_project_ignore_patterns()
530
+ ignore_filter = build_ignore_filter(
531
+ root_path,
532
+ project_patterns=project_patterns,
533
+ include_defaults=True,
534
+ include_gitignore=True,
535
+ )
536
+
537
+ return ignore_filter.ignores(rel_path)
538
+
539
+
540
+ def is_directory_ignored(dir_name: str) -> bool:
541
+ """Quick check if a directory name is in the always-ignored list.
542
+
543
+ This is a fast path for directory traversal.
544
+
545
+ Args:
546
+ dir_name: The directory name (not full path)
547
+
548
+ Returns:
549
+ True if the directory should always be skipped
550
+ """
551
+ return dir_name in IGNORED_DIRECTORIES
552
+
553
+
554
+ def should_skip_path(
555
+ path: Path,
556
+ root_path: Path,
557
+ ignore_filter: Optional[IgnoreFilter] = None,
558
+ skip_hidden: bool = True,
559
+ ) -> bool:
560
+ """Check if a path should be skipped during traversal.
561
+
562
+ Combines multiple checks:
563
+ - Hidden files (starting with .)
564
+ - Always-ignored directories
565
+ - Ignore filter patterns
566
+
567
+ Args:
568
+ path: The path to check
569
+ root_path: The root path for relative matching
570
+ ignore_filter: Pre-built ignore filter
571
+ skip_hidden: Whether to skip hidden files
572
+
573
+ Returns:
574
+ True if the path should be skipped
575
+ """
576
+ name = path.name
577
+
578
+ # Skip hidden files
579
+ if skip_hidden and name.startswith("."):
580
+ return True
581
+
582
+ # Quick check for always-ignored directories
583
+ if path.is_dir() and is_directory_ignored(name):
584
+ return True
585
+
586
+ # Check against ignore filter
587
+ if ignore_filter is not None:
588
+ try:
589
+ rel_path = path.relative_to(root_path).as_posix()
590
+ if ignore_filter.ignores(rel_path):
591
+ return True
592
+ except ValueError:
593
+ pass
594
+
595
+ return False
596
+
597
+
598
+ # =============================================================================
599
+ # Integration with File Tools
600
+ # =============================================================================
601
+
602
+
603
+ def check_path_for_tool(
604
+ file_path: Path,
605
+ tool_name: str = "unknown",
606
+ warn_only: bool = True,
607
+ ) -> Tuple[bool, Optional[str]]:
608
+ """Check if a path should be accessible for a tool.
609
+
610
+ This is designed to be called from file tools (Read, Edit, Write)
611
+ to warn or block access to ignored paths.
612
+
613
+ Args:
614
+ file_path: The file path to check
615
+ tool_name: Name of the calling tool (for messages)
616
+ warn_only: If True, return warning message; if False, block access
617
+
618
+ Returns:
619
+ Tuple of (should_proceed, warning_message)
620
+ - should_proceed: True if the operation should continue
621
+ - warning_message: Warning or error message if path is ignored
622
+ """
623
+ if is_path_ignored(file_path):
624
+ file_name = file_path.name
625
+
626
+ # Check why it's ignored
627
+ reasons = []
628
+
629
+ # Check if it's a binary/media file
630
+ suffix = file_path.suffix.lower()
631
+ binary_extensions = {
632
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp",
633
+ ".mp4", ".avi", ".mkv", ".mov", ".mp3", ".wav", ".flac",
634
+ ".zip", ".tar", ".gz", ".7z", ".rar",
635
+ ".exe", ".dll", ".so", ".dylib",
636
+ ".db", ".sqlite", ".parquet",
637
+ ".ttf", ".otf", ".woff",
638
+ }
639
+ if suffix in binary_extensions:
640
+ reasons.append("binary/media file")
641
+
642
+ # Check if it's in an ignored directory
643
+ for part in file_path.parts:
644
+ if is_directory_ignored(part):
645
+ reasons.append(f"inside '{part}' directory")
646
+ break
647
+
648
+ reason_str = ", ".join(reasons) if reasons else "matches ignore pattern"
649
+
650
+ if warn_only:
651
+ message = (
652
+ f"Warning: '{file_name}' is typically ignored ({reason_str}). "
653
+ f"Proceeding with {tool_name} operation."
654
+ )
655
+ return True, message
656
+ else:
657
+ message = (
658
+ f"Access denied: '{file_name}' is in the ignore list ({reason_str}). "
659
+ f"This file type is not meant to be accessed by {tool_name}."
660
+ )
661
+ return False, message
662
+
663
+ return True, None
664
+
665
+
666
+ __all__ = [
667
+ "DEFAULT_IGNORE_PATTERNS",
668
+ "IGNORED_DIRECTORIES",
669
+ "IgnoreFilter",
670
+ "build_ignore_filter",
671
+ "check_path_for_tool",
672
+ "get_project_ignore_patterns",
673
+ "is_directory_ignored",
674
+ "is_path_ignored",
675
+ "parse_ignore_pattern",
676
+ "should_skip_path",
677
+ ]
@@ -1,7 +1,11 @@
1
1
  """Permission utilities."""
2
2
 
3
3
  from .path_validation_utils import validate_shell_command_paths
4
- from .shell_command_validation import validate_shell_command
4
+ from .shell_command_validation import (
5
+ validate_shell_command,
6
+ is_complex_unsafe_shell_command,
7
+ ValidationResult,
8
+ )
5
9
  from .tool_permission_utils import (
6
10
  PermissionDecision,
7
11
  ToolRule,
@@ -13,8 +17,10 @@ from .tool_permission_utils import (
13
17
  __all__ = [
14
18
  "PermissionDecision",
15
19
  "ToolRule",
20
+ "ValidationResult",
16
21
  "evaluate_shell_command_permissions",
17
22
  "extract_rule_prefix",
23
+ "is_complex_unsafe_shell_command",
18
24
  "match_rule",
19
25
  "validate_shell_command_paths",
20
26
  "validate_shell_command",
@@ -48,9 +48,11 @@ def _resolve_path(raw_path: str, cwd: str) -> Path:
48
48
  candidate = Path(cwd) / candidate
49
49
  try:
50
50
  return candidate.resolve()
51
- except Exception:
52
- logger.exception(
53
- "[path_validation] Failed to resolve path", extra={"raw_path": raw_path, "cwd": cwd}
51
+ except (OSError, ValueError) as exc:
52
+ logger.warning(
53
+ "[path_validation] Failed to resolve path: %s: %s",
54
+ type(exc).__name__, exc,
55
+ extra={"raw_path": raw_path, "cwd": cwd},
54
56
  )
55
57
  return candidate
56
58