janito 0.11.0__py3-none-any.whl → 0.12.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.
Files changed (50) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +6 -204
  3. janito/callbacks.py +34 -132
  4. janito/cli/__init__.py +6 -0
  5. janito/cli/agent.py +287 -0
  6. janito/cli/app.py +86 -0
  7. janito/cli/commands.py +329 -0
  8. janito/cli/output.py +29 -0
  9. janito/cli/utils.py +22 -0
  10. janito/config.py +338 -121
  11. janito/data/instructions_template.txt +27 -0
  12. janito/token_report.py +154 -145
  13. janito/tools/__init__.py +38 -21
  14. janito/tools/bash/bash.py +82 -0
  15. janito/tools/bash/unix_persistent_bash.py +182 -0
  16. janito/tools/bash/win_persistent_bash.py +306 -0
  17. janito/tools/decorators.py +2 -13
  18. janito/tools/delete_file.py +27 -9
  19. janito/tools/fetch_webpage/__init__.py +34 -0
  20. janito/tools/fetch_webpage/chunking.py +76 -0
  21. janito/tools/fetch_webpage/core.py +155 -0
  22. janito/tools/fetch_webpage/extractors.py +276 -0
  23. janito/tools/fetch_webpage/news.py +137 -0
  24. janito/tools/fetch_webpage/utils.py +108 -0
  25. janito/tools/find_files.py +106 -44
  26. janito/tools/move_file.py +72 -0
  27. janito/tools/prompt_user.py +37 -6
  28. janito/tools/replace_file.py +31 -4
  29. janito/tools/rich_console.py +139 -0
  30. janito/tools/search_text.py +33 -21
  31. janito/tools/str_replace_editor/editor.py +7 -4
  32. janito/tools/str_replace_editor/handlers/__init__.py +16 -0
  33. janito/tools/str_replace_editor/handlers/create.py +60 -0
  34. janito/tools/str_replace_editor/handlers/insert.py +100 -0
  35. janito/tools/str_replace_editor/handlers/str_replace.py +92 -0
  36. janito/tools/str_replace_editor/handlers/undo.py +64 -0
  37. janito/tools/str_replace_editor/handlers/view.py +153 -0
  38. janito/tools/str_replace_editor/utils.py +0 -1
  39. janito/tools/usage_tracker.py +136 -0
  40. janito-0.12.0.dist-info/METADATA +203 -0
  41. janito-0.12.0.dist-info/RECORD +47 -0
  42. janito/chat_history.py +0 -117
  43. janito/data/instructions.txt +0 -4
  44. janito/tools/bash.py +0 -22
  45. janito/tools/str_replace_editor/handlers.py +0 -335
  46. janito-0.11.0.dist-info/METADATA +0 -86
  47. janito-0.11.0.dist-info/RECORD +0 -26
  48. {janito-0.11.0.dist-info → janito-0.12.0.dist-info}/WHEEL +0 -0
  49. {janito-0.11.0.dist-info → janito-0.12.0.dist-info}/entry_points.txt +0 -0
  50. {janito-0.11.0.dist-info → janito-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,8 @@
1
1
  """
2
2
  Main module for implementing the Claude text editor functionality.
3
3
  """
4
- from typing import Dict, Any, Tuple
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,92 @@
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
+ print_error("No exact match found for replacement. Please check your text and ensure whitespaces match exactly.", "Error")
64
+ return ("Error: No exact match found for replacement. Please check your text and ensure whitespaces match exactly.", True)
65
+
66
+ # Count occurrences to check for multiple matches
67
+ match_count = content.count(old_str)
68
+ if match_count > 1:
69
+ 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")
70
+ 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)
71
+
72
+ # Replace the string
73
+ new_content = content.replace(old_str, new_str)
74
+
75
+ # Track the number of lines replaced and the line delta
76
+ lines_changed, line_delta = count_lines_in_string(old_str, new_str)
77
+ get_tracker().increment('lines_replaced', lines_changed)
78
+ get_tracker().increment('lines_delta', line_delta)
79
+
80
+ # Write the new content
81
+ with open(file_path, 'w', encoding='utf-8') as f:
82
+ f.write(new_content)
83
+
84
+ # Show relative path if it's not an absolute path in the original input
85
+ display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
86
+ print_success(f"", "Success")
87
+ return (f"Successfully replaced string in file {display_path}", False)
88
+ except Exception as e:
89
+ # Show relative path if it's not an absolute path in the original input
90
+ display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
91
+ print_error(f"Error replacing string in file {display_path}: {str(e)}", "Error")
92
+ 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,153 @@
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"File or directory {path} does not exist", "Error")
59
+ return (f"File or directory {path} does not exist", 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
+ console.print(f"Found ", style="default", end="")
98
+ console.print(f"{file_dir_count}", style="cyan", end="")
99
+ console.print(" files and directories")
100
+ return (output, False)
101
+ except Exception as e:
102
+ return (f"Error listing directory {path}: {str(e)}", True)
103
+
104
+ # If the path is a file, view its contents with cat -n style output
105
+ try:
106
+ with open(file_path, 'r', encoding='utf-8') as f:
107
+ content = f.readlines()
108
+
109
+ # If view_range is specified, return only the specified lines
110
+ if view_range:
111
+ start_line = max(1, view_range[0]) - 1 # Convert to 0-indexed
112
+ end_line = view_range[1] if view_range[1] != -1 else len(content)
113
+ end_line = min(end_line, len(content))
114
+
115
+ # Adjust content to only include the specified lines
116
+ content = content[start_line:end_line]
117
+
118
+ # Track partial file view
119
+ get_tracker().increment('partial_file_views')
120
+ else:
121
+ # Track full file view
122
+ get_tracker().increment('file_views')
123
+
124
+ # Add line numbers to each line (cat -n style)
125
+ numbered_content = []
126
+ start_idx = 1 if view_range is None else view_range[0]
127
+ for i, line in enumerate(content):
128
+ line_number = start_idx + i
129
+ # Ensure line ends with newline
130
+ if not line.endswith('\n'):
131
+ line += '\n'
132
+ # Format line number in cyan color using Rich's styling
133
+ # Use a simpler approach with f-strings and Rich's console
134
+
135
+ # Create a string with the line number that will be styled as cyan
136
+ line_num_str = f"{line_number:6d}\t{line}"
137
+ numbered_content.append(line_num_str)
138
+
139
+ # Check if we need to truncate the output
140
+ MAX_LINES = 500 # Arbitrary limit for demonstration
141
+ if len(numbered_content) > MAX_LINES:
142
+ truncated_content = "".join(numbered_content[:MAX_LINES])
143
+ print(truncated_content + "\n<response clipped>")
144
+ return (truncated_content + "\n<response clipped>", False)
145
+
146
+ content_to_print = "".join(numbered_content)
147
+ console.print("(", style="default", end="")
148
+ console.print(f"{len(numbered_content)}", style="cyan", end="")
149
+ console.print(")")
150
+ # Return the content as a string without any Rich objects
151
+ return (content_to_print, False)
152
+ except Exception as e:
153
+ return (f"Error viewing file {path}: {str(e)}", True)
@@ -2,7 +2,6 @@
2
2
  Utility functions for the str_replace_editor package.
3
3
  """
4
4
  import os
5
- from typing import Dict
6
5
  from janito.config import get_config
7
6
 
8
7
  def normalize_path(path: str) -> str:
@@ -0,0 +1,136 @@
1
+ """
2
+ Tool usage tracking module for Janito.
3
+
4
+ This module provides functionality to track tool usage statistics
5
+ such as files modified, created, deleted, and lines replaced.
6
+ """
7
+
8
+ from functools import wraps
9
+ from typing import Dict, Any, Callable
10
+ import threading
11
+
12
+ # Global tracker instance
13
+ _tracker = None
14
+ _tracker_lock = threading.Lock()
15
+
16
+ class ToolUsageTracker:
17
+ """Tracks usage statistics for Janito tools."""
18
+
19
+ def __init__(self):
20
+ self.reset()
21
+
22
+ def reset(self):
23
+ """Reset all counters to zero."""
24
+ self.files_modified = 0
25
+ self.files_created = 0
26
+ self.files_deleted = 0
27
+ self.files_moved = 0
28
+ self.lines_replaced = 0
29
+ self.lines_delta = 0 # Track the net change in number of lines
30
+ self.web_requests = 0
31
+ self.bash_commands = 0
32
+ self.user_prompts = 0
33
+ self.search_operations = 0
34
+ self.file_views = 0
35
+ self.partial_file_views = 0
36
+
37
+ def increment(self, counter_name: str, value: int = 1):
38
+ """Increment a specific counter by the given value."""
39
+ if hasattr(self, counter_name):
40
+ setattr(self, counter_name, getattr(self, counter_name) + value)
41
+
42
+ def get_stats(self) -> Dict[str, int]:
43
+ """Get all non-zero statistics as a dictionary."""
44
+ stats = {}
45
+ for attr_name in dir(self):
46
+ if not attr_name.startswith('_') and not callable(getattr(self, attr_name)):
47
+ value = getattr(self, attr_name)
48
+ if value > 0:
49
+ # Convert attribute_name to "Attribute Name" format
50
+ display_name = ' '.join(word.capitalize() for word in attr_name.split('_'))
51
+ stats[display_name] = value
52
+ return stats
53
+
54
+
55
+ def get_tracker() -> ToolUsageTracker:
56
+ """Get the global tracker instance."""
57
+ global _tracker
58
+ with _tracker_lock:
59
+ if _tracker is None:
60
+ _tracker = ToolUsageTracker()
61
+ return _tracker
62
+
63
+
64
+ def reset_tracker():
65
+ """Reset the global tracker."""
66
+ get_tracker().reset()
67
+
68
+
69
+ def track_usage(counter_name: str, increment_value: int = 1):
70
+ """
71
+ Decorator to track tool usage.
72
+
73
+ Args:
74
+ counter_name: The name of the counter to increment
75
+ increment_value: Value to increment the counter by (default: 1)
76
+ """
77
+ def decorator(func):
78
+ @wraps(func)
79
+ def wrapper(*args, **kwargs):
80
+ result = func(*args, **kwargs)
81
+ # Only track successful operations
82
+ if isinstance(result, tuple) and len(result) >= 2:
83
+ message, is_error = result[0], result[1]
84
+ if not is_error:
85
+ get_tracker().increment(counter_name, increment_value)
86
+ return result
87
+ return wrapper
88
+ return decorator
89
+
90
+
91
+ def count_lines_in_string(old_str: str, new_str: str) -> tuple[int, int]:
92
+ """
93
+ Count the number of lines that differ between old_str and new_str.
94
+
95
+ Args:
96
+ old_str: Original string
97
+ new_str: New string
98
+
99
+ Returns:
100
+ Tuple of (number of lines that differ, line delta)
101
+ """
102
+ old_lines = old_str.splitlines()
103
+ new_lines = new_str.splitlines()
104
+
105
+ # Calculate the line delta (positive for added lines, negative for removed lines)
106
+ line_delta = len(new_lines) - len(old_lines)
107
+
108
+ # Simple approach: count the total number of lines changed
109
+ # For tracking purposes, we'll use the max to ensure we don't undercount
110
+ return max(len(old_lines), len(new_lines)), line_delta
111
+
112
+
113
+ def print_usage_stats():
114
+ """Print the current usage statistics if any values are non-zero."""
115
+ stats = get_tracker().get_stats()
116
+ if stats:
117
+ from rich.console import Console
118
+
119
+ console = Console()
120
+
121
+ # Create a single-line summary of tool usage
122
+ summary_parts = []
123
+ for name, value in stats.items():
124
+ # Format lines delta with a sign
125
+ if name == "Lines Delta":
126
+ sign = "+" if value > 0 else "" if value == 0 else "-"
127
+ formatted_value = f"{sign}{abs(value)}"
128
+ summary_parts.append(f"{name}: {formatted_value}")
129
+ else:
130
+ summary_parts.append(f"{name}: {value}")
131
+
132
+ summary = " | ".join(summary_parts)
133
+
134
+ # Display with a rule similar to token usage
135
+ console.rule("[blue]Tool Usage[/blue]")
136
+ console.print(f"[blue]{summary}[/blue]", justify="center")