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.
- janito/__init__.py +1 -1
- janito/__main__.py +205 -151
- janito/callbacks.py +132 -130
- janito/chat_history.py +117 -0
- janito/config.py +64 -6
- janito/data/instructions.txt +4 -6
- janito/token_report.py +145 -73
- janito/tools/__init__.py +21 -10
- janito/tools/bash.py +22 -0
- janito/tools/decorators.py +101 -84
- janito/tools/delete_file.py +47 -44
- janito/tools/find_files.py +11 -7
- janito/tools/prompt_user.py +26 -0
- janito/tools/replace_file.py +36 -0
- janito/tools/search_text.py +226 -196
- janito/tools/str_replace_editor/editor.py +52 -43
- janito/tools/str_replace_editor/handlers.py +102 -105
- janito/tools/str_replace_editor/utils.py +8 -62
- {janito-0.10.0.dist-info → janito-0.11.0.dist-info}/METADATA +86 -88
- janito-0.11.0.dist-info/RECORD +26 -0
- {janito-0.10.0.dist-info → janito-0.11.0.dist-info}/WHEEL +1 -2
- janito/cli.py +0 -202
- janito-0.10.0.dist-info/RECORD +0 -23
- janito-0.10.0.dist-info/top_level.txt +0 -1
- {janito-0.10.0.dist-info → janito-0.11.0.dist-info}/entry_points.txt +0 -0
- {janito-0.10.0.dist-info → janito-0.11.0.dist-info/licenses}/LICENSE +0 -0
janito/tools/search_text.py
CHANGED
@@ -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: "*"
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
if
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
#
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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="
|
16
|
-
def str_replace_editor(**kwargs) -> Tuple[str, bool]:
|
17
|
-
"""
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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)
|