janito 0.13.0__py3-none-any.whl → 0.15.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/cli/agent/__init__.py +7 -0
- janito/cli/agent/conversation.py +149 -0
- janito/cli/agent/initialization.py +168 -0
- janito/cli/agent/query.py +112 -0
- janito/cli/agent.py +7 -395
- janito/cli/app.py +103 -19
- janito/cli/commands/__init__.py +12 -0
- janito/cli/commands/config.py +30 -0
- janito/cli/commands/history.py +119 -0
- janito/cli/commands/profile.py +93 -0
- janito/cli/commands/validation.py +24 -0
- janito/cli/commands/workspace.py +31 -0
- janito/cli/commands.py +9 -326
- janito/config/README.md +104 -0
- janito/config/__init__.py +16 -0
- janito/config/cli/__init__.py +28 -0
- janito/config/cli/commands.py +397 -0
- janito/config/cli/validators.py +77 -0
- janito/config/core/__init__.py +23 -0
- janito/config/core/file_operations.py +90 -0
- janito/config/core/properties.py +316 -0
- janito/config/core/singleton.py +282 -0
- janito/config/profiles/__init__.py +8 -0
- janito/config/profiles/definitions.py +38 -0
- janito/config/profiles/manager.py +80 -0
- janito/data/instructions_template.txt +12 -6
- janito/tools/__init__.py +8 -2
- janito/tools/bash/bash.py +80 -7
- janito/tools/bash/unix_persistent_bash.py +32 -1
- janito/tools/bash/win_persistent_bash.py +34 -1
- janito/tools/fetch_webpage/__init__.py +22 -33
- janito/tools/fetch_webpage/core.py +182 -155
- janito/tools/move_file.py +1 -1
- janito/tools/search_text.py +225 -239
- janito/tools/str_replace_editor/handlers/view.py +14 -8
- janito/tools/think.py +37 -0
- janito/tools/usage_tracker.py +1 -0
- {janito-0.13.0.dist-info → janito-0.15.0.dist-info}/METADATA +204 -23
- janito-0.15.0.dist-info/RECORD +64 -0
- janito/config.py +0 -358
- janito/test_file.py +0 -4
- janito/tools/fetch_webpage/chunking.py +0 -76
- janito/tools/fetch_webpage/extractors.py +0 -276
- janito/tools/fetch_webpage/news.py +0 -137
- janito/tools/fetch_webpage/utils.py +0 -108
- janito-0.13.0.dist-info/RECORD +0 -47
- {janito-0.13.0.dist-info → janito-0.15.0.dist-info}/WHEEL +0 -0
- {janito-0.13.0.dist-info → janito-0.15.0.dist-info}/entry_points.txt +0 -0
- {janito-0.13.0.dist-info → janito-0.15.0.dist-info}/licenses/LICENSE +0 -0
janito/tools/search_text.py
CHANGED
@@ -1,240 +1,226 @@
|
|
1
|
-
import os
|
2
|
-
import fnmatch
|
3
|
-
import re
|
4
|
-
|
5
|
-
from
|
6
|
-
from janito.tools.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
root_dir: Root directory to start search from (default: current directory)
|
21
|
-
recursive: Whether to search recursively in subdirectories (default: True)
|
22
|
-
|
23
|
-
Returns:
|
24
|
-
A tuple containing (message, is_error)
|
25
|
-
"""
|
26
|
-
# Simplified initial message
|
27
|
-
print_info(f"Searching for '{text_pattern}' in '{file_pattern}'", "Text Search")
|
28
|
-
try:
|
29
|
-
# Convert to absolute path if relative
|
30
|
-
abs_root = os.path.abspath(root_dir)
|
31
|
-
|
32
|
-
if not os.path.isdir(abs_root):
|
33
|
-
error_msg = f"Error: Directory '{root_dir}' does not exist"
|
34
|
-
print_error(error_msg, "Directory Error")
|
35
|
-
return error_msg, True
|
36
|
-
|
37
|
-
# Compile the regex pattern for better performance
|
38
|
-
try:
|
39
|
-
regex = re.compile(text_pattern)
|
40
|
-
except re.error:
|
41
|
-
# Simplified error message without the specific regex error details
|
42
|
-
error_msg = f"Error: Invalid regex pattern '{text_pattern}'"
|
43
|
-
print_error(error_msg, "Search Error")
|
44
|
-
return error_msg, True
|
45
|
-
|
46
|
-
matching_files = []
|
47
|
-
match_count = 0
|
48
|
-
results = []
|
49
|
-
|
50
|
-
# Get gitignore patterns
|
51
|
-
ignored_patterns = _get_gitignore_patterns(abs_root)
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
# Handle directory-specific patterns (those ending with /)
|
228
|
-
if pattern.endswith('/'):
|
229
|
-
if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
|
230
|
-
return True
|
231
|
-
|
232
|
-
# Handle file patterns
|
233
|
-
if fnmatch.fnmatch(rel_path, pattern):
|
234
|
-
return True
|
235
|
-
|
236
|
-
# Handle patterns without wildcards as path prefixes
|
237
|
-
if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
|
238
|
-
return True
|
239
|
-
|
1
|
+
import os
|
2
|
+
import fnmatch
|
3
|
+
import re
|
4
|
+
import glob
|
5
|
+
from typing import List, Tuple
|
6
|
+
from janito.tools.rich_console import print_info, print_success, print_error, print_warning
|
7
|
+
from janito.tools.usage_tracker import track_usage
|
8
|
+
|
9
|
+
|
10
|
+
@track_usage('search_operations')
|
11
|
+
def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True) -> Tuple[str, bool]:
|
12
|
+
"""
|
13
|
+
Search for text patterns within files matching a filename pattern.
|
14
|
+
Files in .gitignore are always ignored.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
text_pattern: Text pattern to search for within files
|
18
|
+
file_pattern: Pattern to match file paths against (e.g., "*.py", "*/tools/*.py")
|
19
|
+
Multiple patterns can be specified using semicolons or spaces as separators
|
20
|
+
root_dir: Root directory to start search from (default: current directory)
|
21
|
+
recursive: Whether to search recursively in subdirectories (default: True)
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
A tuple containing (message, is_error)
|
25
|
+
"""
|
26
|
+
# Simplified initial message
|
27
|
+
print_info(f"Searching for '{text_pattern}' in '{file_pattern}'", "Text Search")
|
28
|
+
try:
|
29
|
+
# Convert to absolute path if relative
|
30
|
+
abs_root = os.path.abspath(root_dir)
|
31
|
+
|
32
|
+
if not os.path.isdir(abs_root):
|
33
|
+
error_msg = f"Error: Directory '{root_dir}' does not exist"
|
34
|
+
print_error(error_msg, "Directory Error")
|
35
|
+
return error_msg, True
|
36
|
+
|
37
|
+
# Compile the regex pattern for better performance
|
38
|
+
try:
|
39
|
+
regex = re.compile(text_pattern)
|
40
|
+
except re.error:
|
41
|
+
# Simplified error message without the specific regex error details
|
42
|
+
error_msg = f"Error: Invalid regex pattern '{text_pattern}'"
|
43
|
+
print_error(error_msg, "Search Error")
|
44
|
+
return error_msg, True
|
45
|
+
|
46
|
+
matching_files = []
|
47
|
+
match_count = 0
|
48
|
+
results = []
|
49
|
+
|
50
|
+
# Get gitignore patterns
|
51
|
+
ignored_patterns = _get_gitignore_patterns(abs_root)
|
52
|
+
|
53
|
+
# Handle multiple patterns separated by semicolons or spaces
|
54
|
+
patterns = []
|
55
|
+
if ';' in file_pattern:
|
56
|
+
patterns = file_pattern.split(';')
|
57
|
+
elif ' ' in file_pattern and not (os.path.sep in file_pattern or '/' in file_pattern):
|
58
|
+
# Only split by space if the pattern doesn't appear to be a path
|
59
|
+
patterns = file_pattern.split()
|
60
|
+
else:
|
61
|
+
patterns = [file_pattern]
|
62
|
+
|
63
|
+
# Process each pattern
|
64
|
+
for pattern in patterns:
|
65
|
+
# Construct the glob pattern with the root directory
|
66
|
+
glob_pattern = os.path.join(abs_root, pattern) if not pattern.startswith(os.path.sep) else pattern
|
67
|
+
|
68
|
+
# Use recursive glob if needed
|
69
|
+
if recursive:
|
70
|
+
# Use ** pattern for recursive search if not already in the pattern
|
71
|
+
if '**' not in glob_pattern:
|
72
|
+
# Check if the pattern already has a directory component
|
73
|
+
if os.path.sep in pattern or '/' in pattern:
|
74
|
+
# Pattern already has directory component, keep as is
|
75
|
+
pass
|
76
|
+
else:
|
77
|
+
# Add ** to search in all subdirectories
|
78
|
+
glob_pattern = os.path.join(abs_root, '**', pattern)
|
79
|
+
|
80
|
+
# Use recursive=True for Python 3.5+ glob
|
81
|
+
glob_files = glob.glob(glob_pattern, recursive=True)
|
82
|
+
else:
|
83
|
+
# Non-recursive mode - only search in the specified directory
|
84
|
+
glob_files = glob.glob(glob_pattern)
|
85
|
+
|
86
|
+
# Process matching files
|
87
|
+
for file_path in glob_files:
|
88
|
+
# Skip directories and already processed files
|
89
|
+
if not os.path.isfile(file_path) or file_path in matching_files:
|
90
|
+
continue
|
91
|
+
|
92
|
+
# Skip ignored files
|
93
|
+
if _is_ignored(file_path, ignored_patterns, abs_root):
|
94
|
+
continue
|
95
|
+
|
96
|
+
file_matches = _search_file(file_path, regex, abs_root)
|
97
|
+
if file_matches:
|
98
|
+
matching_files.append(file_path)
|
99
|
+
match_count += len(file_matches)
|
100
|
+
results.append(f"\n{os.path.relpath(file_path, abs_root)} ({len(file_matches)} matches):")
|
101
|
+
results.extend(file_matches)
|
102
|
+
|
103
|
+
if matching_files:
|
104
|
+
# Only print the count summary, not the full results
|
105
|
+
summary = f"{match_count} matches in {len(matching_files)} files"
|
106
|
+
print_success(summary, "Search Results")
|
107
|
+
|
108
|
+
# Still return the full results for programmatic use
|
109
|
+
result_text = "\n".join(results)
|
110
|
+
result_msg = f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}"
|
111
|
+
return result_msg, False
|
112
|
+
else:
|
113
|
+
result_msg = f"No matches found for '{text_pattern}' in files matching '{file_pattern}'"
|
114
|
+
print_warning("No matches found.")
|
115
|
+
return result_msg, False
|
116
|
+
|
117
|
+
except Exception as e:
|
118
|
+
error_msg = f"Error searching text: {str(e)}"
|
119
|
+
print_error(error_msg, "Search Error")
|
120
|
+
return error_msg, True
|
121
|
+
|
122
|
+
|
123
|
+
def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
|
124
|
+
"""
|
125
|
+
Search for regex pattern in a file and return matching lines with line numbers.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
file_path: Path to the file to search
|
129
|
+
pattern: Compiled regex pattern to search for
|
130
|
+
root_dir: Root directory (for path display)
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
List of formatted matches with line numbers and content
|
134
|
+
"""
|
135
|
+
matches = []
|
136
|
+
try:
|
137
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
138
|
+
for i, line in enumerate(f, 1):
|
139
|
+
if pattern.search(line):
|
140
|
+
# Truncate long lines for display
|
141
|
+
display_line = line.strip()
|
142
|
+
if len(display_line) > 100:
|
143
|
+
display_line = display_line[:97] + "..."
|
144
|
+
matches.append(f" Line {i}: {display_line}")
|
145
|
+
except (UnicodeDecodeError, IOError):
|
146
|
+
# Skip binary files or files with encoding issues
|
147
|
+
pass
|
148
|
+
return matches
|
149
|
+
|
150
|
+
|
151
|
+
def _get_gitignore_patterns(root_dir: str) -> List[str]:
|
152
|
+
"""
|
153
|
+
Get patterns from .gitignore files.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
root_dir: Root directory to start from
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
List of gitignore patterns
|
160
|
+
"""
|
161
|
+
patterns = []
|
162
|
+
|
163
|
+
# Check for .gitignore in the root directory
|
164
|
+
gitignore_path = os.path.join(root_dir, '.gitignore')
|
165
|
+
if os.path.isfile(gitignore_path):
|
166
|
+
try:
|
167
|
+
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
168
|
+
for line in f:
|
169
|
+
line = line.strip()
|
170
|
+
# Skip empty lines and comments
|
171
|
+
if line and not line.startswith('#'):
|
172
|
+
patterns.append(line)
|
173
|
+
except Exception:
|
174
|
+
pass
|
175
|
+
|
176
|
+
# Add common patterns that are always ignored
|
177
|
+
common_patterns = [
|
178
|
+
'.git/', '.venv/', 'venv/', '__pycache__/', '*.pyc',
|
179
|
+
'*.pyo', '*.pyd', '.DS_Store', '*.so', '*.egg-info/'
|
180
|
+
]
|
181
|
+
patterns.extend(common_patterns)
|
182
|
+
|
183
|
+
return patterns
|
184
|
+
|
185
|
+
|
186
|
+
def _is_ignored(path: str, patterns: List[str], root_dir: str) -> bool:
|
187
|
+
"""
|
188
|
+
Check if a path should be ignored based on gitignore patterns.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
path: Path to check
|
192
|
+
patterns: List of gitignore patterns
|
193
|
+
root_dir: Root directory for relative paths
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
True if the path should be ignored, False otherwise
|
197
|
+
"""
|
198
|
+
# Get the relative path from the root directory
|
199
|
+
rel_path = os.path.relpath(path, root_dir)
|
200
|
+
|
201
|
+
# Convert to forward slashes for consistency with gitignore patterns
|
202
|
+
rel_path = rel_path.replace(os.sep, '/')
|
203
|
+
|
204
|
+
# Add trailing slash for directories
|
205
|
+
if os.path.isdir(path) and not rel_path.endswith('/'):
|
206
|
+
rel_path += '/'
|
207
|
+
|
208
|
+
for pattern in patterns:
|
209
|
+
# Handle negation patterns (those starting with !)
|
210
|
+
if pattern.startswith('!'):
|
211
|
+
continue # Skip negation patterns for simplicity
|
212
|
+
|
213
|
+
# Handle directory-specific patterns (those ending with /)
|
214
|
+
if pattern.endswith('/'):
|
215
|
+
if os.path.isdir(path) and fnmatch.fnmatch(rel_path, pattern + '*'):
|
216
|
+
return True
|
217
|
+
|
218
|
+
# Handle file patterns
|
219
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
220
|
+
return True
|
221
|
+
|
222
|
+
# Handle patterns without wildcards as path prefixes
|
223
|
+
if '*' not in pattern and '?' not in pattern and rel_path.startswith(pattern):
|
224
|
+
return True
|
225
|
+
|
240
226
|
return False
|
@@ -97,9 +97,9 @@ def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
97
97
|
|
98
98
|
# Only print count if not in trust mode
|
99
99
|
if not get_config().trust_mode:
|
100
|
-
console.print(f"
|
100
|
+
console.print(f"(", style="default", end="")
|
101
101
|
console.print(f"{file_dir_count}", style="cyan", end="")
|
102
|
-
console.print(" files and directories")
|
102
|
+
console.print(" files and directories returned)")
|
103
103
|
return (output, False)
|
104
104
|
except Exception as e:
|
105
105
|
return (f"Error listing directory {path}: {str(e)}", True)
|
@@ -139,12 +139,18 @@ def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
139
139
|
line_num_str = f"{line_number:6d}\t{line}"
|
140
140
|
numbered_content.append(line_num_str)
|
141
141
|
|
142
|
-
# Check if we need to
|
143
|
-
MAX_LINES =
|
142
|
+
# Check if we need to show a warning about large file
|
143
|
+
MAX_LINES = get_config().max_view_lines
|
144
144
|
if len(numbered_content) > MAX_LINES:
|
145
|
-
|
146
|
-
|
147
|
-
|
145
|
+
# Only print warning if not in trust mode
|
146
|
+
if not get_config().trust_mode:
|
147
|
+
console.print("(", style="default", end="")
|
148
|
+
console.print(f"{len(numbered_content)}", style="cyan", end="")
|
149
|
+
console.print(f" lines returned - warning: file exceeds recommended size of {MAX_LINES} lines)")
|
150
|
+
|
151
|
+
# Return the full content without truncation
|
152
|
+
content_to_print = "".join(numbered_content)
|
153
|
+
return (content_to_print, False)
|
148
154
|
|
149
155
|
content_to_print = "".join(numbered_content)
|
150
156
|
|
@@ -152,7 +158,7 @@ def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
152
158
|
if not get_config().trust_mode:
|
153
159
|
console.print("(", style="default", end="")
|
154
160
|
console.print(f"{len(numbered_content)}", style="cyan", end="")
|
155
|
-
console.print(")")
|
161
|
+
console.print(" lines returned)")
|
156
162
|
# Return the content as a string without any Rich objects
|
157
163
|
return (content_to_print, False)
|
158
164
|
except Exception as e:
|
janito/tools/think.py
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
"""
|
2
|
+
Tool for thinking about something without obtaining new information or changing the database.
|
3
|
+
"""
|
4
|
+
from typing import Tuple
|
5
|
+
import logging
|
6
|
+
from janito.tools.usage_tracker import track_usage
|
7
|
+
from janito.tools.rich_console import print_info
|
8
|
+
|
9
|
+
# Set up logging
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
@track_usage('thoughts')
|
13
|
+
def think(
|
14
|
+
thought: str,
|
15
|
+
) -> Tuple[str, bool]:
|
16
|
+
"""
|
17
|
+
Use the tool to think about something. It will not obtain new information or change the database,
|
18
|
+
but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
thought: A thought to think about.
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
A tuple containing (message, is_error)
|
25
|
+
"""
|
26
|
+
try:
|
27
|
+
# Log the thought
|
28
|
+
logger.info(f"Thought: {thought}")
|
29
|
+
|
30
|
+
# Print a confirmation message
|
31
|
+
print_info(f"Thought recorded: {thought[:50]}{'...' if len(thought) > 50 else ''}", "Thinking")
|
32
|
+
|
33
|
+
return (f"Thought recorded: {thought}", False)
|
34
|
+
except Exception as e:
|
35
|
+
error_msg = f"Error recording thought: {str(e)}"
|
36
|
+
logger.error(error_msg)
|
37
|
+
return (error_msg, True)
|
janito/tools/usage_tracker.py
CHANGED
@@ -33,6 +33,7 @@ class ToolUsageTracker:
|
|
33
33
|
self.search_operations = 0
|
34
34
|
self.file_views = 0
|
35
35
|
self.partial_file_views = 0
|
36
|
+
self.thoughts = 0 # Track the number of thoughts recorded
|
36
37
|
|
37
38
|
def increment(self, counter_name: str, value: int = 1):
|
38
39
|
"""Increment a specific counter by the given value."""
|