janito 0.10.0__py3-none-any.whl → 0.11.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.
@@ -1,197 +1,227 @@
1
- import os
2
- import fnmatch
3
- import re
4
- import pathlib
5
- from typing import List, Dict, Any, Tuple
6
- from janito.tools.decorators import tool_meta
7
-
8
-
9
- @tool_meta(label="Searching for '{text_pattern}' in files matching '{file_pattern}'")
10
- def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True, respect_gitignore: bool = True) -> Tuple[str, bool]:
11
- """
12
- Search for text patterns within files matching a filename pattern.
13
-
14
- Args:
15
- text_pattern: Text pattern to search for within files
16
- file_pattern: Pattern to match file names against (default: "*" - all files)
17
- root_dir: Root directory to start search from (default: current directory)
18
- recursive: Whether to search recursively in subdirectories (default: True)
19
- respect_gitignore: Whether to respect .gitignore files (default: True)
20
-
21
- Returns:
22
- A tuple containing (message, is_error)
23
- """
24
- try:
25
- # Convert to absolute path if relative
26
- abs_root = os.path.abspath(root_dir)
27
-
28
- if not os.path.isdir(abs_root):
29
- return f"Error: Directory '{root_dir}' does not exist", True
30
-
31
- # Compile the regex pattern for better performance
32
- try:
33
- regex = re.compile(text_pattern)
34
- except re.error as e:
35
- return f"Error: Invalid regex pattern '{text_pattern}': {str(e)}", True
36
-
37
- matching_files = []
38
- match_count = 0
39
- results = []
40
-
41
- # Get gitignore patterns if needed
42
- ignored_patterns = []
43
- if respect_gitignore:
44
- ignored_patterns = _get_gitignore_patterns(abs_root)
45
-
46
- # Use os.walk for recursive behavior
47
- if recursive:
48
- for dirpath, dirnames, filenames in os.walk(abs_root):
49
- # Skip ignored directories
50
- if respect_gitignore:
51
- dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
52
-
53
- for filename in fnmatch.filter(filenames, file_pattern):
54
- file_path = os.path.join(dirpath, filename)
55
-
56
- # Skip ignored files
57
- if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
58
- continue
59
-
60
- file_matches = _search_file(file_path, regex, abs_root)
61
- if file_matches:
62
- matching_files.append(file_path)
63
- match_count += len(file_matches)
64
- results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
65
- results.extend(file_matches)
66
- else:
67
- # Non-recursive mode - only search in the specified directory
68
- for filename in fnmatch.filter(os.listdir(abs_root), file_pattern):
69
- file_path = os.path.join(abs_root, filename)
70
-
71
- # Skip ignored files
72
- if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
73
- continue
74
-
75
- if os.path.isfile(file_path):
76
- file_matches = _search_file(file_path, regex, abs_root)
77
- if file_matches:
78
- matching_files.append(file_path)
79
- match_count += len(file_matches)
80
- results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
81
- results.extend(file_matches)
82
-
83
- if matching_files:
84
- result_text = "\n".join(results)
85
- summary = f"\n{match_count} matches in {len(matching_files)} files"
86
- return f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}", False
87
- else:
88
- return f"No matches found for '{text_pattern}' in files matching '{file_pattern}' in '{root_dir}'", False
89
-
90
- except Exception as e:
91
- return f"Error searching text: {str(e)}", True
92
-
93
-
94
- def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
95
- """
96
- Search for regex pattern in a file and return matching lines with line numbers.
97
-
98
- Args:
99
- file_path: Path to the file to search
100
- pattern: Compiled regex pattern to search for
101
- root_dir: Root directory (for path display)
102
-
103
- Returns:
104
- List of formatted matches with line numbers and content
105
- """
106
- matches = []
107
- try:
108
- with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
109
- for i, line in enumerate(f, 1):
110
- if pattern.search(line):
111
- # Truncate long lines for display
112
- display_line = line.strip()
113
- if len(display_line) > 100:
114
- display_line = display_line[:97] + "..."
115
- matches.append(f" Line {i}: {display_line}")
116
- except (UnicodeDecodeError, IOError) as e:
117
- # Skip binary files or files with encoding issues
118
- pass
119
- return matches
120
-
121
-
122
- def _get_gitignore_patterns(root_dir: str) -> List[str]:
123
- """
124
- Get patterns from .gitignore files.
125
-
126
- Args:
127
- root_dir: Root directory to start from
128
-
129
- Returns:
130
- List of gitignore patterns
131
- """
132
- patterns = []
133
-
134
- # Check for .gitignore in the root directory
135
- gitignore_path = os.path.join(root_dir, '.gitignore')
136
- if os.path.isfile(gitignore_path):
137
- try:
138
- with open(gitignore_path, 'r', encoding='utf-8') as f:
139
- for line in f:
140
- line = line.strip()
141
- # Skip empty lines and comments
142
- if line and not line.startswith('#'):
143
- patterns.append(line)
144
- except Exception:
145
- pass
146
-
147
- # Add common patterns that are always ignored
148
- common_patterns = [
149
- '.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
150
- '*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
151
- ]
152
- patterns.extend(common_patterns)
153
-
154
- return patterns
155
-
156
-
157
- def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
158
- """
159
- Check if a path should be ignored based on gitignore patterns.
160
-
161
- Args:
162
- path: Path to check
163
- patterns: List of gitignore patterns
164
- root_dir: Root directory for relative paths
165
-
166
- Returns:
167
- True if the path should be ignored, False otherwise
168
- """
169
- # Get the relative path from the root directory
170
- rel_path = os.path.relpath(path, root_dir)
171
-
172
- # Convert to forward slashes for consistency with gitignore patterns
173
- rel_path = rel_path.replace(os.sep, '/')
174
-
175
- # Add trailing slash for directories
176
- if os.path.isdir(path) and not rel_path.endswith('/'):
177
- rel_path += '/'
178
-
179
- for pattern in patterns:
180
- # Handle negation patterns (those starting with !)
181
- if pattern.startswith('!'):
182
- continue # Skip negation patterns for simplicity
183
-
184
- # Handle directory-specific patterns (those ending with /)
185
- if pattern.endswith('/'):
186
- if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
187
- return True
188
-
189
- # Handle file patterns
190
- if fnmatch.fnmatch(rel_path, pattern):
191
- return True
192
-
193
- # Handle patterns without wildcards as path prefixes
194
- if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
195
- return True
196
-
1
+ import os
2
+ import fnmatch
3
+ import re
4
+ import pathlib
5
+ from typing import List, Dict, Any, Tuple
6
+ from janito.tools.decorators import tool_meta
7
+
8
+
9
+ @tool_meta(label="Searching for '{text_pattern}' in files matching '{file_pattern}'")
10
+ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True, respect_gitignore: bool = True) -> Tuple[str, bool]:
11
+ """
12
+ Search for text patterns within files matching a filename pattern.
13
+
14
+ Args:
15
+ text_pattern: Text pattern to search for within files
16
+ file_pattern: Pattern to match file names against (default: "*")
17
+ Multiple patterns can be specified using semicolons or spaces as separators
18
+ Examples: "*.py *.toml *.sh *.md test*"
19
+ root_dir: Root directory to start search from (default: current directory)
20
+ recursive: Whether to search recursively in subdirectories (default: True)
21
+ respect_gitignore: Whether to respect .gitignore files (default: True)
22
+
23
+ Returns:
24
+ A tuple containing (message, is_error)
25
+ """
26
+ try:
27
+ # Convert to absolute path if relative
28
+ abs_root = os.path.abspath(root_dir)
29
+
30
+ if not os.path.isdir(abs_root):
31
+ return f"Error: Directory '{root_dir}' does not exist", True
32
+
33
+ # Compile the regex pattern for better performance
34
+ try:
35
+ regex = re.compile(text_pattern)
36
+ except re.error as e:
37
+ return f"Error: Invalid regex pattern '{text_pattern}': {str(e)}", True
38
+
39
+ matching_files = []
40
+ match_count = 0
41
+ results = []
42
+
43
+ # Get gitignore patterns if needed
44
+ ignored_patterns = []
45
+ if respect_gitignore:
46
+ ignored_patterns = _get_gitignore_patterns(abs_root)
47
+
48
+ # Use os.walk for recursive behavior
49
+ if recursive:
50
+ for dirpath, dirnames, filenames in os.walk(abs_root):
51
+ # Skip ignored directories
52
+ if respect_gitignore:
53
+ dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
54
+
55
+ # Handle multiple patterns separated by semicolons or spaces
56
+ patterns = []
57
+ if ';' in file_pattern:
58
+ patterns = file_pattern.split(';')
59
+ elif ' ' in file_pattern:
60
+ patterns = file_pattern.split()
61
+ else:
62
+ patterns = [file_pattern]
63
+
64
+ for pattern in patterns:
65
+ for filename in fnmatch.filter(filenames, pattern):
66
+ file_path = os.path.join(dirpath, filename)
67
+
68
+ # Skip ignored files
69
+ if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
70
+ continue
71
+
72
+ # Skip if already processed this file
73
+ if file_path in matching_files:
74
+ continue
75
+
76
+ file_matches = _search_file(file_path, regex, abs_root)
77
+ if file_matches:
78
+ matching_files.append(file_path)
79
+ match_count += len(file_matches)
80
+ results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
81
+ results.extend(file_matches)
82
+ else:
83
+ # Non-recursive mode - only search in the specified directory
84
+ # Handle multiple patterns separated by semicolons or spaces
85
+ patterns = []
86
+ if ';' in file_pattern:
87
+ patterns = file_pattern.split(';')
88
+ elif ' ' in file_pattern:
89
+ patterns = file_pattern.split()
90
+ else:
91
+ patterns = [file_pattern]
92
+
93
+ for pattern in patterns:
94
+ for filename in fnmatch.filter(os.listdir(abs_root), pattern):
95
+ file_path = os.path.join(abs_root, filename)
96
+
97
+ # Skip ignored files
98
+ if respect_gitignore and _is_ignored(file_path, ignored_patterns, abs_root):
99
+ continue
100
+
101
+ # Skip if already processed this file
102
+ if file_path in matching_files:
103
+ continue
104
+
105
+ if os.path.isfile(file_path):
106
+ file_matches = _search_file(file_path, regex, abs_root)
107
+ if file_matches:
108
+ matching_files.append(file_path)
109
+ match_count += len(file_matches)
110
+ results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
111
+ results.extend(file_matches)
112
+
113
+ if matching_files:
114
+ result_text = "\n".join(results)
115
+ summary = f"\n{match_count} matches in {len(matching_files)} files"
116
+ return f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}", False
117
+ else:
118
+ return f"No matches found for '{text_pattern}' in files matching '{file_pattern}' in '{root_dir}'", False
119
+
120
+ except Exception as e:
121
+ return f"Error searching text: {str(e)}", True
122
+
123
+
124
+ def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
125
+ """
126
+ Search for regex pattern in a file and return matching lines with line numbers.
127
+
128
+ Args:
129
+ file_path: Path to the file to search
130
+ pattern: Compiled regex pattern to search for
131
+ root_dir: Root directory (for path display)
132
+
133
+ Returns:
134
+ List of formatted matches with line numbers and content
135
+ """
136
+ matches = []
137
+ try:
138
+ with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
139
+ for i, line in enumerate(f, 1):
140
+ if pattern.search(line):
141
+ # Truncate long lines for display
142
+ display_line = line.strip()
143
+ if len(display_line) > 100:
144
+ display_line = display_line[:97] + "..."
145
+ matches.append(f" Line {i}: {display_line}")
146
+ except (UnicodeDecodeError, IOError) as e:
147
+ # Skip binary files or files with encoding issues
148
+ pass
149
+ return matches
150
+
151
+
152
+ def _get_gitignore_patterns(root_dir: str) -> List[str]:
153
+ """
154
+ Get patterns from .gitignore files.
155
+
156
+ Args:
157
+ root_dir: Root directory to start from
158
+
159
+ Returns:
160
+ List of gitignore patterns
161
+ """
162
+ patterns = []
163
+
164
+ # Check for .gitignore in the root directory
165
+ gitignore_path = os.path.join(root_dir, '.gitignore')
166
+ if os.path.isfile(gitignore_path):
167
+ try:
168
+ with open(gitignore_path, 'r', encoding='utf-8') as f:
169
+ for line in f:
170
+ line = line.strip()
171
+ # Skip empty lines and comments
172
+ if line and not line.startswith('#'):
173
+ patterns.append(line)
174
+ except Exception:
175
+ pass
176
+
177
+ # Add common patterns that are always ignored
178
+ common_patterns = [
179
+ '.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
180
+ '*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
181
+ ]
182
+ patterns.extend(common_patterns)
183
+
184
+ return patterns
185
+
186
+
187
+ def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
188
+ """
189
+ Check if a path should be ignored based on gitignore patterns.
190
+
191
+ Args:
192
+ path: Path to check
193
+ patterns: List of gitignore patterns
194
+ root_dir: Root directory for relative paths
195
+
196
+ Returns:
197
+ True if the path should be ignored, False otherwise
198
+ """
199
+ # Get the relative path from the root directory
200
+ rel_path = os.path.relpath(path, root_dir)
201
+
202
+ # Convert to forward slashes for consistency with gitignore patterns
203
+ rel_path = rel_path.replace(os.sep, '/')
204
+
205
+ # Add trailing slash for directories
206
+ if os.path.isdir(path) and not rel_path.endswith('/'):
207
+ rel_path += '/'
208
+
209
+ for pattern in patterns:
210
+ # Handle negation patterns (those starting with !)
211
+ if pattern.startswith('!'):
212
+ continue # Skip negation patterns for simplicity
213
+
214
+ # Handle directory-specific patterns (those ending with /)
215
+ if pattern.endswith('/'):
216
+ if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
217
+ return True
218
+
219
+ # Handle file patterns
220
+ if fnmatch.fnmatch(rel_path, pattern):
221
+ return True
222
+
223
+ # Handle patterns without wildcards as path prefixes
224
+ if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
225
+ return True
226
+
197
227
  return False
@@ -1,43 +1,52 @@
1
- """
2
- Main module for implementing the Claude text editor functionality.
3
- """
4
- from typing import Dict, Any, Tuple
5
- from .handlers import (
6
- handle_create,
7
- handle_view,
8
- handle_str_replace,
9
- handle_insert,
10
- handle_undo_edit
11
- )
12
- from .utils import normalize_path
13
- from janito.tools.decorators import tool_meta
14
-
15
- @tool_meta(label="Editing file: {file_path} ({command})")
16
- def str_replace_editor(**kwargs) -> Tuple[str, bool]:
17
- """
18
- Handle text editor tool requests from Claude.
19
- Implements the Claude text editor tool specification.
20
-
21
- Args:
22
- **kwargs: All arguments passed to the tool, including:
23
- - command: The command to execute (view, create, str_replace, insert, undo_edit)
24
- - path: Path to the file
25
- - Additional command-specific arguments
26
-
27
- Returns:
28
- A tuple containing (message, is_error)
29
- """
30
- command = kwargs.get("command")
31
-
32
- if command == "create":
33
- return handle_create(kwargs)
34
- elif command == "view":
35
- return handle_view(kwargs)
36
- elif command == "str_replace":
37
- return handle_str_replace(kwargs)
38
- elif command == "insert":
39
- return handle_insert(kwargs)
40
- elif command == "undo_edit":
41
- return handle_undo_edit(kwargs)
42
- else:
43
- return (f"Command '{command}' not implemented yet", True)
1
+ """
2
+ Main module for implementing the Claude text editor functionality.
3
+ """
4
+ from typing import Dict, Any, Tuple
5
+ from .handlers import (
6
+ handle_create,
7
+ handle_view,
8
+ handle_str_replace,
9
+ handle_insert,
10
+ handle_undo_edit
11
+ )
12
+ from .utils import normalize_path
13
+ from janito.tools.decorators import tool_meta
14
+
15
+ @tool_meta(label="File Command: ({command})")
16
+ def str_replace_editor(**kwargs) -> Tuple[str, bool]:
17
+ """
18
+ Custom editing tool for viewing, creating and editing files
19
+ * State is persistent across command calls and discussions with the user
20
+ * If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
21
+ * The `create` command cannot be used if the specified `path` already exists as a file
22
+ * If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
23
+ * The `undo_edit` command will revert the last edit made to the file at `path`
24
+
25
+ Notes for using the `str_replace` command:
26
+ * The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
27
+ * If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
28
+ * The `new_str` parameter should contain the edited lines that should replace the `old_str`
29
+
30
+ Args:
31
+ **kwargs: All arguments passed to the tool, including:
32
+ - command: The command to execute (view, create, str_replace, insert, undo_edit)
33
+ - path: Path to the file
34
+ - Additional command-specific arguments
35
+
36
+ Returns:
37
+ A tuple containing (message, is_error)
38
+ """
39
+ command = kwargs.get("command")
40
+
41
+ if command == "create":
42
+ return handle_create(kwargs)
43
+ elif command == "view":
44
+ return handle_view(kwargs)
45
+ elif command == "str_replace":
46
+ return handle_str_replace(kwargs)
47
+ elif command == "insert":
48
+ return handle_insert(kwargs)
49
+ elif command == "undo_edit":
50
+ return handle_undo_edit(kwargs)
51
+ else:
52
+ return (f"Command '{command}' not implemented yet", True)