janito 0.11.0__py3-none-any.whl → 0.13.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 +6 -204
- janito/callbacks.py +34 -132
- janito/cli/__init__.py +6 -0
- janito/cli/agent.py +400 -0
- janito/cli/app.py +94 -0
- janito/cli/commands.py +329 -0
- janito/cli/output.py +29 -0
- janito/cli/utils.py +22 -0
- janito/config.py +358 -121
- janito/data/instructions_template.txt +28 -0
- janito/token_report.py +154 -145
- janito/tools/__init__.py +38 -21
- janito/tools/bash/bash.py +84 -0
- janito/tools/bash/unix_persistent_bash.py +184 -0
- janito/tools/bash/win_persistent_bash.py +308 -0
- janito/tools/decorators.py +2 -13
- janito/tools/delete_file.py +27 -9
- janito/tools/fetch_webpage/__init__.py +34 -0
- janito/tools/fetch_webpage/chunking.py +76 -0
- janito/tools/fetch_webpage/core.py +155 -0
- janito/tools/fetch_webpage/extractors.py +276 -0
- janito/tools/fetch_webpage/news.py +137 -0
- janito/tools/fetch_webpage/utils.py +108 -0
- janito/tools/find_files.py +106 -44
- janito/tools/move_file.py +72 -0
- janito/tools/prompt_user.py +37 -6
- janito/tools/replace_file.py +31 -4
- janito/tools/rich_console.py +176 -0
- janito/tools/search_text.py +35 -22
- janito/tools/str_replace_editor/editor.py +7 -4
- janito/tools/str_replace_editor/handlers/__init__.py +16 -0
- janito/tools/str_replace_editor/handlers/create.py +60 -0
- janito/tools/str_replace_editor/handlers/insert.py +100 -0
- janito/tools/str_replace_editor/handlers/str_replace.py +94 -0
- janito/tools/str_replace_editor/handlers/undo.py +64 -0
- janito/tools/str_replace_editor/handlers/view.py +159 -0
- janito/tools/str_replace_editor/utils.py +0 -1
- janito/tools/usage_tracker.py +136 -0
- janito-0.13.0.dist-info/METADATA +300 -0
- janito-0.13.0.dist-info/RECORD +47 -0
- janito/chat_history.py +0 -117
- janito/data/instructions.txt +0 -4
- janito/tools/bash.py +0 -22
- janito/tools/str_replace_editor/handlers.py +0 -335
- janito-0.11.0.dist-info/METADATA +0 -86
- janito-0.11.0.dist-info/RECORD +0 -26
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/WHEEL +0 -0
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/entry_points.txt +0 -0
- {janito-0.11.0.dist-info → janito-0.13.0.dist-info}/licenses/LICENSE +0 -0
janito/tools/search_text.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
import os
|
2
2
|
import fnmatch
|
3
3
|
import re
|
4
|
-
import
|
5
|
-
from
|
6
|
-
from janito.tools.
|
4
|
+
from typing import List, Tuple
|
5
|
+
from janito.tools.rich_console import print_info, print_success, print_error, print_warning
|
6
|
+
from janito.tools.usage_tracker import track_usage
|
7
7
|
|
8
8
|
|
9
|
-
@
|
10
|
-
def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True
|
9
|
+
@track_usage('search_operations')
|
10
|
+
def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".", recursive: bool = True) -> Tuple[str, bool]:
|
11
11
|
"""
|
12
12
|
Search for text patterns within files matching a filename pattern.
|
13
|
+
Files in .gitignore are always ignored.
|
13
14
|
|
14
15
|
Args:
|
15
16
|
text_pattern: Text pattern to search for within files
|
@@ -18,39 +19,42 @@ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".",
|
|
18
19
|
Examples: "*.py *.toml *.sh *.md test*"
|
19
20
|
root_dir: Root directory to start search from (default: current directory)
|
20
21
|
recursive: Whether to search recursively in subdirectories (default: True)
|
21
|
-
respect_gitignore: Whether to respect .gitignore files (default: True)
|
22
22
|
|
23
23
|
Returns:
|
24
24
|
A tuple containing (message, is_error)
|
25
25
|
"""
|
26
|
+
# Simplified initial message
|
27
|
+
print_info(f"Searching for '{text_pattern}' in '{file_pattern}'", "Text Search")
|
26
28
|
try:
|
27
29
|
# Convert to absolute path if relative
|
28
30
|
abs_root = os.path.abspath(root_dir)
|
29
31
|
|
30
32
|
if not os.path.isdir(abs_root):
|
31
|
-
|
33
|
+
error_msg = f"Error: Directory '{root_dir}' does not exist"
|
34
|
+
print_error(error_msg, "Directory Error")
|
35
|
+
return error_msg, True
|
32
36
|
|
33
37
|
# Compile the regex pattern for better performance
|
34
38
|
try:
|
35
39
|
regex = re.compile(text_pattern)
|
36
|
-
except re.error
|
37
|
-
|
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
|
38
45
|
|
39
46
|
matching_files = []
|
40
47
|
match_count = 0
|
41
48
|
results = []
|
42
49
|
|
43
|
-
# Get gitignore patterns
|
44
|
-
ignored_patterns =
|
45
|
-
if respect_gitignore:
|
46
|
-
ignored_patterns = _get_gitignore_patterns(abs_root)
|
50
|
+
# Get gitignore patterns
|
51
|
+
ignored_patterns = _get_gitignore_patterns(abs_root)
|
47
52
|
|
48
53
|
# Use os.walk for recursive behavior
|
49
54
|
if recursive:
|
50
55
|
for dirpath, dirnames, filenames in os.walk(abs_root):
|
51
56
|
# Skip ignored directories
|
52
|
-
if
|
53
|
-
dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
|
57
|
+
dirnames[:] = [d for d in dirnames if not _is_ignored(os.path.join(dirpath, d), ignored_patterns, abs_root)]
|
54
58
|
|
55
59
|
# Handle multiple patterns separated by semicolons or spaces
|
56
60
|
patterns = []
|
@@ -66,7 +70,7 @@ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".",
|
|
66
70
|
file_path = os.path.join(dirpath, filename)
|
67
71
|
|
68
72
|
# Skip ignored files
|
69
|
-
if
|
73
|
+
if _is_ignored(file_path, ignored_patterns, abs_root):
|
70
74
|
continue
|
71
75
|
|
72
76
|
# Skip if already processed this file
|
@@ -95,7 +99,7 @@ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".",
|
|
95
99
|
file_path = os.path.join(abs_root, filename)
|
96
100
|
|
97
101
|
# Skip ignored files
|
98
|
-
if
|
102
|
+
if _is_ignored(file_path, ignored_patterns, abs_root):
|
99
103
|
continue
|
100
104
|
|
101
105
|
# Skip if already processed this file
|
@@ -111,14 +115,23 @@ def search_text(text_pattern: str, file_pattern: str = "*", root_dir: str = ".",
|
|
111
115
|
results.extend(file_matches)
|
112
116
|
|
113
117
|
if matching_files:
|
118
|
+
# Only print the count summary, not the full results
|
119
|
+
summary = f"{match_count} matches in {len(matching_files)} files"
|
120
|
+
print_success(summary, "Search Results")
|
121
|
+
|
122
|
+
# Still return the full results for programmatic use
|
114
123
|
result_text = "\n".join(results)
|
115
|
-
|
116
|
-
return
|
124
|
+
result_msg = f"Searching for '{text_pattern}' in files matching '{file_pattern}':{result_text}\n{summary}"
|
125
|
+
return result_msg, False
|
117
126
|
else:
|
118
|
-
|
127
|
+
result_msg = f"No matches found for '{text_pattern}' in files matching '{file_pattern}'"
|
128
|
+
print_warning("No matches found.")
|
129
|
+
return result_msg, False
|
119
130
|
|
120
131
|
except Exception as e:
|
121
|
-
|
132
|
+
error_msg = f"Error searching text: {str(e)}"
|
133
|
+
print_error(error_msg, "Search Error")
|
134
|
+
return error_msg, True
|
122
135
|
|
123
136
|
|
124
137
|
def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str]:
|
@@ -143,7 +156,7 @@ def _search_file(file_path: str, pattern: re.Pattern, root_dir: str) -> List[str
|
|
143
156
|
if len(display_line) > 100:
|
144
157
|
display_line = display_line[:97] + "..."
|
145
158
|
matches.append(f" Line {i}: {display_line}")
|
146
|
-
except (UnicodeDecodeError, IOError)
|
159
|
+
except (UnicodeDecodeError, IOError):
|
147
160
|
# Skip binary files or files with encoding issues
|
148
161
|
pass
|
149
162
|
return matches
|
@@ -1,7 +1,8 @@
|
|
1
1
|
"""
|
2
2
|
Main module for implementing the Claude text editor functionality.
|
3
3
|
"""
|
4
|
-
from typing import
|
4
|
+
from typing import Tuple
|
5
|
+
from janito.config import get_config
|
5
6
|
from .handlers import (
|
6
7
|
handle_create,
|
7
8
|
handle_view,
|
@@ -9,10 +10,7 @@ from .handlers import (
|
|
9
10
|
handle_insert,
|
10
11
|
handle_undo_edit
|
11
12
|
)
|
12
|
-
from .utils import normalize_path
|
13
|
-
from janito.tools.decorators import tool_meta
|
14
13
|
|
15
|
-
@tool_meta(label="File Command: ({command})")
|
16
14
|
def str_replace_editor(**kwargs) -> Tuple[str, bool]:
|
17
15
|
"""
|
18
16
|
Custom editing tool for viewing, creating and editing files
|
@@ -21,6 +19,7 @@ def str_replace_editor(**kwargs) -> Tuple[str, bool]:
|
|
21
19
|
* The `create` command cannot be used if the specified `path` already exists as a file
|
22
20
|
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
|
23
21
|
* The `undo_edit` command will revert the last edit made to the file at `path`
|
22
|
+
* When in ask mode, only the `view` command is allowed
|
24
23
|
|
25
24
|
Notes for using the `str_replace` command:
|
26
25
|
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
|
@@ -38,6 +37,10 @@ def str_replace_editor(**kwargs) -> Tuple[str, bool]:
|
|
38
37
|
"""
|
39
38
|
command = kwargs.get("command")
|
40
39
|
|
40
|
+
# If in ask mode, only allow view operations
|
41
|
+
if get_config().ask_mode and command != "view":
|
42
|
+
return ("Cannot perform file modifications in ask mode. Use --ask option to disable modifications.", True)
|
43
|
+
|
41
44
|
if command == "create":
|
42
45
|
return handle_create(kwargs)
|
43
46
|
elif command == "view":
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
Package for str_replace_editor command handlers.
|
3
|
+
"""
|
4
|
+
from .create import handle_create
|
5
|
+
from .view import handle_view
|
6
|
+
from .str_replace import handle_str_replace
|
7
|
+
from .insert import handle_insert
|
8
|
+
from .undo import handle_undo_edit
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"handle_create",
|
12
|
+
"handle_view",
|
13
|
+
"handle_str_replace",
|
14
|
+
"handle_insert",
|
15
|
+
"handle_undo_edit"
|
16
|
+
]
|
@@ -0,0 +1,60 @@
|
|
1
|
+
"""
|
2
|
+
Handler for the create command in str_replace_editor.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.rich_console import print_info, print_success, print_error
|
9
|
+
from janito.tools.usage_tracker import get_tracker
|
10
|
+
from ..utils import normalize_path
|
11
|
+
|
12
|
+
def handle_create(args: Dict[str, Any]) -> Tuple[str, bool]:
|
13
|
+
"""
|
14
|
+
Create a new file with the specified content.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
args: Dictionary containing:
|
18
|
+
- path: Path to the file to create
|
19
|
+
- file_text: Content to write to the file
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
A tuple containing (message, is_error)
|
23
|
+
"""
|
24
|
+
path = args.get("path")
|
25
|
+
file_text = args.get("file_text", "")
|
26
|
+
|
27
|
+
# Count the number of lines in the file content
|
28
|
+
line_count = len(file_text.splitlines())
|
29
|
+
print_info(f"Creating file: {path} (+{line_count} lines)", "File Creation")
|
30
|
+
|
31
|
+
if not path:
|
32
|
+
return ("Missing required parameter: path", True)
|
33
|
+
|
34
|
+
path = normalize_path(path)
|
35
|
+
|
36
|
+
# Convert to Path object for better path handling
|
37
|
+
file_path = pathlib.Path(path)
|
38
|
+
|
39
|
+
# Check if the file already exists - according to spec, create cannot be used if file exists
|
40
|
+
if file_path.exists() and file_path.is_file():
|
41
|
+
print_error(f"File {path} already exists. The 'create' command cannot be used if the specified path already exists as a file.", "Error")
|
42
|
+
return (f"File {path} already exists. The 'create' command cannot be used if the specified path already exists as a file.", True)
|
43
|
+
|
44
|
+
# Create parent directories if they don't exist
|
45
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
46
|
+
|
47
|
+
# Write the content to the file
|
48
|
+
try:
|
49
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
50
|
+
f.write(file_text)
|
51
|
+
# Track file creation and line delta
|
52
|
+
get_tracker().increment('files_created')
|
53
|
+
get_tracker().increment('lines_delta', line_count)
|
54
|
+
# Show relative path if it's not an absolute path
|
55
|
+
display_path = path if os.path.isabs(path) else os.path.relpath(file_path, get_config().workspace_dir)
|
56
|
+
print_success(f"", "Success")
|
57
|
+
return (f"Successfully created file {display_path}", False)
|
58
|
+
except Exception as e:
|
59
|
+
print_error(f"Error creating file {path}: {str(e)}", "Error")
|
60
|
+
return (f"Error creating file {path}: {str(e)}", True)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
"""
|
2
|
+
Handler for the insert command in str_replace_editor.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.rich_console import print_info, print_success, print_error
|
9
|
+
from janito.tools.usage_tracker import get_tracker
|
10
|
+
from ..utils import normalize_path, _file_history
|
11
|
+
|
12
|
+
def handle_insert(args: Dict[str, Any]) -> Tuple[str, bool]:
|
13
|
+
"""
|
14
|
+
Insert text at a specific location in a file.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
args: Dictionary containing:
|
18
|
+
- path: Path to the file to modify
|
19
|
+
- insert_line: The line number after which to insert the text
|
20
|
+
- new_str: The text to insert
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
A tuple containing (message, is_error)
|
24
|
+
"""
|
25
|
+
path = args.get("path")
|
26
|
+
insert_line = args.get("insert_line")
|
27
|
+
new_str = args.get("new_str")
|
28
|
+
|
29
|
+
# Count lines in new string
|
30
|
+
new_lines_count = len(new_str.splitlines()) if new_str else 0
|
31
|
+
|
32
|
+
print_info(f"Inserting text in file: {path}, after line {insert_line} (+{new_lines_count} lines)", "Insert Operation")
|
33
|
+
|
34
|
+
if not path:
|
35
|
+
print_error("Missing required parameter: path", "Error")
|
36
|
+
return ("Missing required parameter: path", True)
|
37
|
+
if insert_line is None:
|
38
|
+
print_error("Missing required parameter: insert_line", "Error")
|
39
|
+
return ("Missing required parameter: insert_line", True)
|
40
|
+
if new_str is None:
|
41
|
+
print_error("Missing required parameter: new_str", "Error")
|
42
|
+
return ("Missing required parameter: new_str", True)
|
43
|
+
|
44
|
+
# Store the original path for display purposes
|
45
|
+
original_path = path
|
46
|
+
|
47
|
+
# Normalize the path (converts to absolute path)
|
48
|
+
path = normalize_path(path)
|
49
|
+
file_path = pathlib.Path(path)
|
50
|
+
|
51
|
+
if not file_path.exists():
|
52
|
+
print_error(f"File {path} does not exist", "Error")
|
53
|
+
return (f"File {path} does not exist", True)
|
54
|
+
|
55
|
+
try:
|
56
|
+
# Read the file content
|
57
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
58
|
+
lines = f.readlines()
|
59
|
+
content = "".join(lines)
|
60
|
+
|
61
|
+
# Save the current content for undo
|
62
|
+
if path not in _file_history:
|
63
|
+
_file_history[path] = []
|
64
|
+
_file_history[path].append(content)
|
65
|
+
|
66
|
+
# Check if insert_line is valid
|
67
|
+
if insert_line < 0 or insert_line > len(lines):
|
68
|
+
print_error(f"Invalid insert line {insert_line} for file {path}", "Error")
|
69
|
+
return (f"Invalid insert line {insert_line} for file {path}", True)
|
70
|
+
|
71
|
+
# Ensure new_str ends with a newline if it doesn't already
|
72
|
+
if new_str and not new_str.endswith('\n'):
|
73
|
+
new_str += '\n'
|
74
|
+
|
75
|
+
# Insert the new string
|
76
|
+
lines.insert(insert_line, new_str)
|
77
|
+
|
78
|
+
# Track the number of lines inserted
|
79
|
+
lines_count = len(new_str.splitlines())
|
80
|
+
get_tracker().increment('lines_replaced', lines_count)
|
81
|
+
|
82
|
+
# Write the new content
|
83
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
84
|
+
f.writelines(lines)
|
85
|
+
|
86
|
+
# Show relative path if it's not an absolute path in the original input
|
87
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
88
|
+
|
89
|
+
# If the response is too long, truncate it
|
90
|
+
response = f"Successfully inserted text at line {insert_line} in file {display_path}"
|
91
|
+
print_success(response, "Success")
|
92
|
+
if len(response) > 1000: # Arbitrary limit for demonstration
|
93
|
+
return (response[:1000] + "\n<response clipped>", False)
|
94
|
+
|
95
|
+
return (response, False)
|
96
|
+
except Exception as e:
|
97
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
98
|
+
error_msg = f"Error inserting text in file {display_path}: {str(e)}"
|
99
|
+
print_error(error_msg, "Error")
|
100
|
+
return (error_msg, True)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Handler for the str_replace command in str_replace_editor.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.rich_console import print_info, print_success, print_error
|
9
|
+
from janito.tools.usage_tracker import get_tracker, count_lines_in_string
|
10
|
+
from ..utils import normalize_path, _file_history
|
11
|
+
|
12
|
+
def handle_str_replace(args: Dict[str, Any]) -> Tuple[str, bool]:
|
13
|
+
"""
|
14
|
+
Replace a specific string in a file with a new string.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
args: Dictionary containing:
|
18
|
+
- path: Path to the file to modify
|
19
|
+
- old_str: The text to replace (must match EXACTLY)
|
20
|
+
- new_str: The new text to insert
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
A tuple containing (message, is_error)
|
24
|
+
"""
|
25
|
+
path = args.get("path")
|
26
|
+
old_str = args.get("old_str")
|
27
|
+
new_str = args.get("new_str", "") # new_str can be empty to effectively delete text
|
28
|
+
|
29
|
+
# Count lines in old and new strings
|
30
|
+
old_lines_count = len(old_str.splitlines()) if old_str else 0
|
31
|
+
new_lines_count = len(new_str.splitlines()) if new_str else 0
|
32
|
+
line_delta = new_lines_count - old_lines_count
|
33
|
+
delta_sign = "+" if line_delta > 0 else "" if line_delta == 0 else "-"
|
34
|
+
|
35
|
+
print_info(f"Replacing text in file: {path} ({old_lines_count} -> {new_lines_count} lines, {delta_sign}{abs(line_delta)})", "Replacing text in file")
|
36
|
+
|
37
|
+
if not path:
|
38
|
+
print_error("Missing required parameter: path", "Error")
|
39
|
+
return ("Missing required parameter: path", True)
|
40
|
+
if old_str is None:
|
41
|
+
print_error("Missing required parameter: old_str", "Error")
|
42
|
+
return ("Missing required parameter: old_str", True)
|
43
|
+
|
44
|
+
path = normalize_path(path)
|
45
|
+
file_path = pathlib.Path(path)
|
46
|
+
|
47
|
+
if not file_path.exists():
|
48
|
+
print_error(f"File {path} does not exist", "Error")
|
49
|
+
return (f"File {path} does not exist", True)
|
50
|
+
|
51
|
+
try:
|
52
|
+
# Read the file content
|
53
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
54
|
+
content = f.read()
|
55
|
+
|
56
|
+
# Save the current content for undo
|
57
|
+
if path not in _file_history:
|
58
|
+
_file_history[path] = []
|
59
|
+
_file_history[path].append(content)
|
60
|
+
|
61
|
+
# Check if old_str exists in the content (must match EXACTLY)
|
62
|
+
if old_str not in content:
|
63
|
+
# Only print error if not in trust mode
|
64
|
+
if not get_config().trust_mode:
|
65
|
+
print_error("No exact match", "?")
|
66
|
+
return ("Error: No exact match found for replacement. Please check your text and ensure whitespaces match exactly.", True)
|
67
|
+
|
68
|
+
# Count occurrences to check for multiple matches
|
69
|
+
match_count = content.count(old_str)
|
70
|
+
if match_count > 1:
|
71
|
+
print_error(f"Found {match_count} matches for replacement text. The old_str parameter is not unique in the file. Please include more context to make it unique.", "Error")
|
72
|
+
return (f"Error: Found {match_count} matches for replacement text. The old_str parameter is not unique in the file. Please include more context to make it unique.", True)
|
73
|
+
|
74
|
+
# Replace the string
|
75
|
+
new_content = content.replace(old_str, new_str)
|
76
|
+
|
77
|
+
# Track the number of lines replaced and the line delta
|
78
|
+
lines_changed, line_delta = count_lines_in_string(old_str, new_str)
|
79
|
+
get_tracker().increment('lines_replaced', lines_changed)
|
80
|
+
get_tracker().increment('lines_delta', line_delta)
|
81
|
+
|
82
|
+
# Write the new content
|
83
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
84
|
+
f.write(new_content)
|
85
|
+
|
86
|
+
# Show relative path if it's not an absolute path in the original input
|
87
|
+
display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
|
88
|
+
print_success(f"", "Success")
|
89
|
+
return (f"Successfully replaced string in file {display_path}", False)
|
90
|
+
except Exception as e:
|
91
|
+
# Show relative path if it's not an absolute path in the original input
|
92
|
+
display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
|
93
|
+
print_error(f"Error replacing string in file {display_path}: {str(e)}", "Error")
|
94
|
+
return (f"Error replacing string in file {display_path}: {str(e)}", True)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Handler for the undo_edit command in str_replace_editor.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.rich_console import print_info, print_success, print_error, print_warning
|
9
|
+
from ..utils import normalize_path, _file_history
|
10
|
+
|
11
|
+
def handle_undo_edit(args: Dict[str, Any]) -> Tuple[str, bool]:
|
12
|
+
"""
|
13
|
+
Undo the last edit made to a file using in-memory history.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
args: Dictionary containing:
|
17
|
+
- path: Path to the file whose last edit should be undone
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
A tuple containing (message, is_error)
|
21
|
+
"""
|
22
|
+
path = args.get("path")
|
23
|
+
|
24
|
+
print_info(f"Undoing last edit to file: {path}", "Undo Operation")
|
25
|
+
|
26
|
+
if not path:
|
27
|
+
print_error("Missing required parameter: path", "Error")
|
28
|
+
return ("Missing required parameter: path", True)
|
29
|
+
|
30
|
+
# Store the original path for display purposes
|
31
|
+
original_path = path
|
32
|
+
|
33
|
+
# Normalize the path (converts to absolute path)
|
34
|
+
path = normalize_path(path)
|
35
|
+
file_path = pathlib.Path(path)
|
36
|
+
|
37
|
+
# Check if file exists
|
38
|
+
if not file_path.exists():
|
39
|
+
print_error(f"File {path} does not exist", "Error")
|
40
|
+
return (f"File {path} does not exist", True)
|
41
|
+
|
42
|
+
# Check in-memory history
|
43
|
+
if path not in _file_history or not _file_history[path]:
|
44
|
+
print_warning(f"Warning: No edit history for file {path}")
|
45
|
+
return (f"No edit history for file {path}", True)
|
46
|
+
|
47
|
+
try:
|
48
|
+
# Get the last content
|
49
|
+
last_content = _file_history[path].pop()
|
50
|
+
|
51
|
+
# Write the last content back to the file
|
52
|
+
with open(path, 'w', encoding='utf-8') as f:
|
53
|
+
f.write(last_content)
|
54
|
+
|
55
|
+
# Show relative path if it's not an absolute path in the original input
|
56
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
57
|
+
success_msg = f"Successfully reverted the last edit made to the file {display_path}"
|
58
|
+
print_success(success_msg, "Success")
|
59
|
+
return (success_msg, False)
|
60
|
+
except Exception as e:
|
61
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
62
|
+
error_msg = f"Error undoing edit to file {display_path}: {str(e)}"
|
63
|
+
print_error(error_msg, "Error")
|
64
|
+
return (error_msg, True)
|
@@ -0,0 +1,159 @@
|
|
1
|
+
"""
|
2
|
+
Handler for the view command in str_replace_editor.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import pathlib
|
6
|
+
from typing import Dict, Any, Tuple
|
7
|
+
from janito.config import get_config
|
8
|
+
from janito.tools.rich_console import print_info, print_error, console
|
9
|
+
from janito.tools.usage_tracker import get_tracker
|
10
|
+
from ..utils import normalize_path
|
11
|
+
|
12
|
+
def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
13
|
+
"""
|
14
|
+
View the contents of a file or list directory contents.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
args: Dictionary containing:
|
18
|
+
- path: Path to the file or directory to view
|
19
|
+
- view_range (optional): Array of two integers specifying start and end line numbers
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
A tuple containing (content_or_message, is_error)
|
23
|
+
"""
|
24
|
+
path = args.get("path")
|
25
|
+
view_range = args.get("view_range")
|
26
|
+
|
27
|
+
# First normalize the path to check if it's a file or directory
|
28
|
+
normalized_path = normalize_path(path)
|
29
|
+
file_path = pathlib.Path(normalized_path)
|
30
|
+
|
31
|
+
if file_path.exists():
|
32
|
+
if file_path.is_dir():
|
33
|
+
print_info(f"Viewing directory: {path}: ", "Directory View")
|
34
|
+
else:
|
35
|
+
if view_range:
|
36
|
+
# Print with proper title for File View
|
37
|
+
print_info(f"Viewing file: {path}, from line {view_range[0]} to {view_range[1]}: ", "File View")
|
38
|
+
else:
|
39
|
+
# Print with proper title for File View
|
40
|
+
print_info(f"Viewing file: {path}, all lines: ", "File View")
|
41
|
+
else:
|
42
|
+
# If path doesn't exist yet, assume it's a file (will be validated later)
|
43
|
+
if view_range:
|
44
|
+
# Print with proper title for File View
|
45
|
+
print_info(f"Viewing file: {path}, from line {view_range[0]} to {view_range[1]}: ", "File View")
|
46
|
+
else:
|
47
|
+
# Print with proper title for File View
|
48
|
+
print_info(f"Viewing file: {path}, all lines: ", "File View")
|
49
|
+
|
50
|
+
if not path:
|
51
|
+
print_error("Missing required parameter: path", "Error")
|
52
|
+
return ("Missing required parameter: path", True)
|
53
|
+
|
54
|
+
path = normalize_path(path)
|
55
|
+
file_path = pathlib.Path(path)
|
56
|
+
|
57
|
+
if not file_path.exists():
|
58
|
+
print_error(f"❓ (not found)", "Error")
|
59
|
+
return (f"❓ (not found)", True)
|
60
|
+
|
61
|
+
# If the path is a directory, list non-hidden files and directories up to 2 levels deep
|
62
|
+
if file_path.is_dir():
|
63
|
+
try:
|
64
|
+
result = []
|
65
|
+
# Process the first level
|
66
|
+
for item in sorted(file_path.iterdir()):
|
67
|
+
if item.name.startswith('.'):
|
68
|
+
continue # Skip hidden files/directories
|
69
|
+
|
70
|
+
if item.is_dir():
|
71
|
+
result.append(f"{item.name}/")
|
72
|
+
# Process the second level
|
73
|
+
try:
|
74
|
+
for subitem in sorted(item.iterdir()):
|
75
|
+
if subitem.name.startswith('.'):
|
76
|
+
continue # Skip hidden files/directories
|
77
|
+
|
78
|
+
if subitem.is_dir():
|
79
|
+
result.append(f"{item.name}/{subitem.name}/")
|
80
|
+
else:
|
81
|
+
result.append(f"{item.name}/{subitem.name}")
|
82
|
+
except PermissionError:
|
83
|
+
# Skip directories we can't access
|
84
|
+
pass
|
85
|
+
else:
|
86
|
+
result.append(item.name)
|
87
|
+
|
88
|
+
if not result:
|
89
|
+
return (f"Directory {path} is empty or contains only hidden files", False)
|
90
|
+
|
91
|
+
# Track directory view
|
92
|
+
get_tracker().increment('file_views')
|
93
|
+
|
94
|
+
# Directory listings should not be truncated
|
95
|
+
file_dir_count = len(result)
|
96
|
+
output = "\n".join(result)
|
97
|
+
|
98
|
+
# Only print count if not in trust mode
|
99
|
+
if not get_config().trust_mode:
|
100
|
+
console.print(f"Found ", style="default", end="")
|
101
|
+
console.print(f"{file_dir_count}", style="cyan", end="")
|
102
|
+
console.print(" files and directories")
|
103
|
+
return (output, False)
|
104
|
+
except Exception as e:
|
105
|
+
return (f"Error listing directory {path}: {str(e)}", True)
|
106
|
+
|
107
|
+
# If the path is a file, view its contents with cat -n style output
|
108
|
+
try:
|
109
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
110
|
+
content = f.readlines()
|
111
|
+
|
112
|
+
# If view_range is specified, return only the specified lines
|
113
|
+
if view_range:
|
114
|
+
start_line = max(1, view_range[0]) - 1 # Convert to 0-indexed
|
115
|
+
end_line = view_range[1] if view_range[1] != -1 else len(content)
|
116
|
+
end_line = min(end_line, len(content))
|
117
|
+
|
118
|
+
# Adjust content to only include the specified lines
|
119
|
+
content = content[start_line:end_line]
|
120
|
+
|
121
|
+
# Track partial file view
|
122
|
+
get_tracker().increment('partial_file_views')
|
123
|
+
else:
|
124
|
+
# Track full file view
|
125
|
+
get_tracker().increment('file_views')
|
126
|
+
|
127
|
+
# Add line numbers to each line (cat -n style)
|
128
|
+
numbered_content = []
|
129
|
+
start_idx = 1 if view_range is None else view_range[0]
|
130
|
+
for i, line in enumerate(content):
|
131
|
+
line_number = start_idx + i
|
132
|
+
# Ensure line ends with newline
|
133
|
+
if not line.endswith('\n'):
|
134
|
+
line += '\n'
|
135
|
+
# Format line number in cyan color using Rich's styling
|
136
|
+
# Use a simpler approach with f-strings and Rich's console
|
137
|
+
|
138
|
+
# Create a string with the line number that will be styled as cyan
|
139
|
+
line_num_str = f"{line_number:6d}\t{line}"
|
140
|
+
numbered_content.append(line_num_str)
|
141
|
+
|
142
|
+
# Check if we need to truncate the output
|
143
|
+
MAX_LINES = 500 # Arbitrary limit for demonstration
|
144
|
+
if len(numbered_content) > MAX_LINES:
|
145
|
+
truncated_content = "".join(numbered_content[:MAX_LINES])
|
146
|
+
print(truncated_content + "\n<response clipped>")
|
147
|
+
return (truncated_content + "\n<response clipped>", False)
|
148
|
+
|
149
|
+
content_to_print = "".join(numbered_content)
|
150
|
+
|
151
|
+
# Only print line count if not in trust mode
|
152
|
+
if not get_config().trust_mode:
|
153
|
+
console.print("(", style="default", end="")
|
154
|
+
console.print(f"{len(numbered_content)}", style="cyan", end="")
|
155
|
+
console.print(")")
|
156
|
+
# Return the content as a string without any Rich objects
|
157
|
+
return (content_to_print, False)
|
158
|
+
except Exception as e:
|
159
|
+
return (f"Error viewing file {path}: {str(e)}", True)
|