janito 0.10.1__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/__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 -4
- 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/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.1.dist-info → janito-0.11.0.dist-info}/METADATA +1 -1
- janito-0.11.0.dist-info/RECORD +26 -0
- janito/cli.py +0 -202
- janito-0.10.1.dist-info/RECORD +0 -23
- {janito-0.10.1.dist-info → janito-0.11.0.dist-info}/WHEEL +0 -0
- {janito-0.10.1.dist-info → janito-0.11.0.dist-info}/entry_points.txt +0 -0
- {janito-0.10.1.dist-info → janito-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ import os
|
|
5
5
|
import pathlib
|
6
6
|
from typing import Dict, Any, Tuple
|
7
7
|
from janito.config import get_config
|
8
|
-
from .utils import normalize_path, _file_history
|
8
|
+
from .utils import normalize_path, _file_history
|
9
9
|
|
10
10
|
def handle_create(args: Dict[str, Any]) -> Tuple[str, bool]:
|
11
11
|
"""
|
@@ -30,9 +30,9 @@ def handle_create(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
30
30
|
# Convert to Path object for better path handling
|
31
31
|
file_path = pathlib.Path(path)
|
32
32
|
|
33
|
-
# Check if the file already exists
|
34
|
-
if file_path.exists():
|
35
|
-
return (f"File {path} already exists", True)
|
33
|
+
# Check if the file already exists - according to spec, create cannot be used if file exists
|
34
|
+
if file_path.exists() and file_path.is_file():
|
35
|
+
return (f"File {path} already exists. The 'create' command cannot be used if the specified path already exists as a file.", True)
|
36
36
|
|
37
37
|
# Create parent directories if they don't exist
|
38
38
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
@@ -72,35 +72,48 @@ def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
72
72
|
if not file_path.exists():
|
73
73
|
return (f"File or directory {path} does not exist", True)
|
74
74
|
|
75
|
-
# If the path is a directory, list
|
75
|
+
# If the path is a directory, list non-hidden files and directories up to 2 levels deep
|
76
76
|
if file_path.is_dir():
|
77
77
|
try:
|
78
|
-
|
79
|
-
|
78
|
+
result = []
|
79
|
+
# Process the first level
|
80
|
+
for item in sorted(file_path.iterdir()):
|
81
|
+
if item.name.startswith('.'):
|
82
|
+
continue # Skip hidden files/directories
|
83
|
+
|
84
|
+
if item.is_dir():
|
85
|
+
result.append(f"{item.name}/")
|
86
|
+
# Process the second level
|
87
|
+
try:
|
88
|
+
for subitem in sorted(item.iterdir()):
|
89
|
+
if subitem.name.startswith('.'):
|
90
|
+
continue # Skip hidden files/directories
|
91
|
+
|
92
|
+
if subitem.is_dir():
|
93
|
+
result.append(f"{item.name}/{subitem.name}/")
|
94
|
+
else:
|
95
|
+
result.append(f"{item.name}/{subitem.name}")
|
96
|
+
except PermissionError:
|
97
|
+
# Skip directories we can't access
|
98
|
+
pass
|
99
|
+
else:
|
100
|
+
result.append(item.name)
|
80
101
|
|
81
|
-
|
82
|
-
|
83
|
-
files = [item.name for item in items if item.is_file()]
|
102
|
+
if not result:
|
103
|
+
return (f"Directory {path} is empty or contains only hidden files", False)
|
84
104
|
|
85
|
-
|
86
|
-
|
105
|
+
# Determine if we need to truncate the output
|
106
|
+
MAX_LINES = 100 # Arbitrary limit for demonstration
|
107
|
+
output = "\n".join(result)
|
108
|
+
if len(result) > MAX_LINES:
|
109
|
+
truncated_output = "\n".join(result[:MAX_LINES])
|
110
|
+
return (truncated_output + "\n<response clipped>", False)
|
87
111
|
|
88
|
-
|
89
|
-
contents = dirs + files
|
90
|
-
|
91
|
-
if not contents:
|
92
|
-
return (f"Directory {path} is empty", False)
|
93
|
-
|
94
|
-
# Add count information to the output
|
95
|
-
dir_count = len(dirs)
|
96
|
-
file_count = len(files)
|
97
|
-
count_info = f"Total: {len(contents)} ({dir_count} directories, {file_count} files)"
|
98
|
-
|
99
|
-
return ("\n".join(contents) + f"\n{count_info}", False)
|
112
|
+
return (output, False)
|
100
113
|
except Exception as e:
|
101
114
|
return (f"Error listing directory {path}: {str(e)}", True)
|
102
115
|
|
103
|
-
# If the path is a file, view its contents
|
116
|
+
# If the path is a file, view its contents with cat -n style output
|
104
117
|
try:
|
105
118
|
with open(file_path, 'r', encoding='utf-8') as f:
|
106
119
|
content = f.readlines()
|
@@ -113,34 +126,27 @@ def handle_view(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
113
126
|
|
114
127
|
# Adjust content to only include the specified lines
|
115
128
|
content = content[start_line:end_line]
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
# Add line count information
|
139
|
-
# Show relative path if it's not an absolute path
|
140
|
-
display_path = path if os.path.isabs(path) else os.path.relpath(file_path, get_config().workspace_dir)
|
141
|
-
line_info = f"Viewed {len(content)} lines from {display_path}"
|
142
|
-
|
143
|
-
return ("".join(numbered_content) + f"\n{line_info}", False)
|
129
|
+
|
130
|
+
# Add line numbers to each line (cat -n style)
|
131
|
+
numbered_content = []
|
132
|
+
start_idx = 1 if view_range is None else view_range[0]
|
133
|
+
for i, line in enumerate(content):
|
134
|
+
line_number = start_idx + i
|
135
|
+
# Ensure line ends with newline
|
136
|
+
if not line.endswith('\n'):
|
137
|
+
line += '\n'
|
138
|
+
numbered_content.append(f"{line_number:6d}\t{line}")
|
139
|
+
|
140
|
+
# Show relative path if it's not an absolute path
|
141
|
+
display_path = path if os.path.isabs(path) else os.path.relpath(file_path, get_config().workspace_dir)
|
142
|
+
|
143
|
+
# Check if we need to truncate the output
|
144
|
+
MAX_LINES = 500 # Arbitrary limit for demonstration
|
145
|
+
if len(numbered_content) > MAX_LINES:
|
146
|
+
truncated_content = "".join(numbered_content[:MAX_LINES])
|
147
|
+
return (truncated_content + "\n<response clipped>", False)
|
148
|
+
|
149
|
+
return ("".join(numbered_content), False)
|
144
150
|
except Exception as e:
|
145
151
|
return (f"Error viewing file {path}: {str(e)}", True)
|
146
152
|
|
@@ -152,7 +158,7 @@ def handle_str_replace(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
152
158
|
Args:
|
153
159
|
args: Dictionary containing:
|
154
160
|
- path: Path to the file to modify
|
155
|
-
- old_str: The text to replace
|
161
|
+
- old_str: The text to replace (must match EXACTLY)
|
156
162
|
- new_str: The new text to insert
|
157
163
|
|
158
164
|
Returns:
|
@@ -160,14 +166,12 @@ def handle_str_replace(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
160
166
|
"""
|
161
167
|
path = args.get("path")
|
162
168
|
old_str = args.get("old_str")
|
163
|
-
new_str = args.get("new_str")
|
169
|
+
new_str = args.get("new_str", "") # new_str can be empty to effectively delete text
|
164
170
|
|
165
171
|
if not path:
|
166
172
|
return ("Missing required parameter: path", True)
|
167
173
|
if old_str is None:
|
168
174
|
return ("Missing required parameter: old_str", True)
|
169
|
-
if new_str is None:
|
170
|
-
return ("Missing required parameter: new_str", True)
|
171
175
|
|
172
176
|
path = normalize_path(path)
|
173
177
|
file_path = pathlib.Path(path)
|
@@ -180,22 +184,19 @@ def handle_str_replace(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
180
184
|
with open(file_path, 'r', encoding='utf-8') as f:
|
181
185
|
content = f.read()
|
182
186
|
|
183
|
-
#
|
184
|
-
backup_file(path, content)
|
185
|
-
|
186
|
-
# Save the current content for undo (legacy approach, will be deprecated)
|
187
|
+
# Save the current content for undo
|
187
188
|
if path not in _file_history:
|
188
189
|
_file_history[path] = []
|
189
190
|
_file_history[path].append(content)
|
190
191
|
|
191
|
-
# Check if old_str exists in the content
|
192
|
+
# Check if old_str exists in the content (must match EXACTLY)
|
192
193
|
if old_str not in content:
|
193
|
-
return ("Error: No match found for replacement. Please check your text and
|
194
|
+
return ("Error: No exact match found for replacement. Please check your text and ensure whitespaces match exactly.", True)
|
194
195
|
|
195
196
|
# Count occurrences to check for multiple matches
|
196
197
|
match_count = content.count(old_str)
|
197
198
|
if match_count > 1:
|
198
|
-
return (f"Error: Found {match_count} matches for replacement text. Please
|
199
|
+
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)
|
199
200
|
|
200
201
|
# Replace the string
|
201
202
|
new_content = content.replace(old_str, new_str)
|
@@ -204,9 +205,13 @@ def handle_str_replace(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
204
205
|
with open(file_path, 'w', encoding='utf-8') as f:
|
205
206
|
f.write(new_content)
|
206
207
|
|
207
|
-
|
208
|
+
# Show relative path if it's not an absolute path in the original input
|
209
|
+
display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
|
210
|
+
return (f"Successfully replaced string in file {display_path}", False)
|
208
211
|
except Exception as e:
|
209
|
-
|
212
|
+
# Show relative path if it's not an absolute path in the original input
|
213
|
+
display_path = args.get("path") if os.path.isabs(args.get("path")) else os.path.relpath(file_path, get_config().workspace_dir)
|
214
|
+
return (f"Error replacing string in file {display_path}: {str(e)}", True)
|
210
215
|
|
211
216
|
|
212
217
|
def handle_insert(args: Dict[str, Any]) -> Tuple[str, bool]:
|
@@ -233,13 +238,11 @@ def handle_insert(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
233
238
|
if new_str is None:
|
234
239
|
return ("Missing required parameter: new_str", True)
|
235
240
|
|
236
|
-
#
|
237
|
-
|
238
|
-
|
239
|
-
# Make path absolute if it's not already
|
240
|
-
if not os.path.isabs(path):
|
241
|
-
path = os.path.join(workspace_dir, path)
|
241
|
+
# Store the original path for display purposes
|
242
|
+
original_path = path
|
242
243
|
|
244
|
+
# Normalize the path (converts to absolute path)
|
245
|
+
path = normalize_path(path)
|
243
246
|
file_path = pathlib.Path(path)
|
244
247
|
|
245
248
|
if not file_path.exists():
|
@@ -251,10 +254,7 @@ def handle_insert(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
251
254
|
lines = f.readlines()
|
252
255
|
content = "".join(lines)
|
253
256
|
|
254
|
-
#
|
255
|
-
backup_file(path, content)
|
256
|
-
|
257
|
-
# Save the current content for undo (legacy approach, will be deprecated)
|
257
|
+
# Save the current content for undo
|
258
258
|
if path not in _file_history:
|
259
259
|
_file_history[path] = []
|
260
260
|
_file_history[path].append(content)
|
@@ -274,14 +274,23 @@ def handle_insert(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
274
274
|
with open(file_path, 'w', encoding='utf-8') as f:
|
275
275
|
f.writelines(lines)
|
276
276
|
|
277
|
-
|
277
|
+
# Show relative path if it's not an absolute path in the original input
|
278
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
279
|
+
|
280
|
+
# If the response is too long, truncate it
|
281
|
+
response = f"Successfully inserted text at line {insert_line} in file {display_path}"
|
282
|
+
if len(response) > 1000: # Arbitrary limit for demonstration
|
283
|
+
return (response[:1000] + "\n<response clipped>", False)
|
284
|
+
|
285
|
+
return (response, False)
|
278
286
|
except Exception as e:
|
279
|
-
|
287
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
288
|
+
return (f"Error inserting text in file {display_path}: {str(e)}", True)
|
280
289
|
|
281
290
|
|
282
291
|
def handle_undo_edit(args: Dict[str, Any]) -> Tuple[str, bool]:
|
283
292
|
"""
|
284
|
-
Undo the last edit made to a file.
|
293
|
+
Undo the last edit made to a file using in-memory history.
|
285
294
|
|
286
295
|
Args:
|
287
296
|
args: Dictionary containing:
|
@@ -295,33 +304,18 @@ def handle_undo_edit(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
295
304
|
if not path:
|
296
305
|
return ("Missing required parameter: path", True)
|
297
306
|
|
298
|
-
#
|
299
|
-
|
307
|
+
# Store the original path for display purposes
|
308
|
+
original_path = path
|
300
309
|
|
301
|
-
#
|
302
|
-
|
303
|
-
|
310
|
+
# Normalize the path (converts to absolute path)
|
311
|
+
path = normalize_path(path)
|
312
|
+
file_path = pathlib.Path(path)
|
304
313
|
|
305
|
-
#
|
306
|
-
|
307
|
-
|
308
|
-
backup_path = backup_info['path']
|
309
|
-
backup_content = backup_info['content']
|
310
|
-
|
311
|
-
# If a path was provided, check if it matches the backup
|
312
|
-
if path != backup_path:
|
313
|
-
return (f"No backup found for file {path}. Last edited file was {backup_path}", True)
|
314
|
-
|
315
|
-
try:
|
316
|
-
# Write the backup content back to the file
|
317
|
-
with open(path, 'w', encoding='utf-8') as f:
|
318
|
-
f.write(backup_content)
|
319
|
-
|
320
|
-
return (f"Successfully undid last edit to file {path}", False)
|
321
|
-
except Exception as e:
|
322
|
-
return (f"Error undoing edit to file {path}: {str(e)}", True)
|
314
|
+
# Check if file exists
|
315
|
+
if not file_path.exists():
|
316
|
+
return (f"File {path} does not exist", True)
|
323
317
|
|
324
|
-
#
|
318
|
+
# Check in-memory history
|
325
319
|
if path not in _file_history or not _file_history[path]:
|
326
320
|
return (f"No edit history for file {path}", True)
|
327
321
|
|
@@ -333,6 +327,9 @@ def handle_undo_edit(args: Dict[str, Any]) -> Tuple[str, bool]:
|
|
333
327
|
with open(path, 'w', encoding='utf-8') as f:
|
334
328
|
f.write(last_content)
|
335
329
|
|
336
|
-
|
330
|
+
# Show relative path if it's not an absolute path in the original input
|
331
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
332
|
+
return (f"Successfully reverted the last edit made to the file {display_path}", False)
|
337
333
|
except Exception as e:
|
338
|
-
|
334
|
+
display_path = original_path if os.path.isabs(original_path) else os.path.relpath(file_path, get_config().workspace_dir)
|
335
|
+
return (f"Error undoing edit to file {display_path}: {str(e)}", True)
|
@@ -2,19 +2,21 @@
|
|
2
2
|
Utility functions for the str_replace_editor package.
|
3
3
|
"""
|
4
4
|
import os
|
5
|
-
import
|
6
|
-
from typing import Dict, Any, Optional
|
5
|
+
from typing import Dict
|
7
6
|
from janito.config import get_config
|
8
7
|
|
9
8
|
def normalize_path(path: str) -> str:
|
10
9
|
"""
|
11
10
|
Normalizes a path relative to the workspace directory.
|
12
11
|
|
12
|
+
For internal operations, converts relative paths to absolute paths
|
13
|
+
based on the workspace directory.
|
14
|
+
|
13
15
|
Args:
|
14
16
|
path: The original path
|
15
17
|
|
16
18
|
Returns:
|
17
|
-
The normalized path
|
19
|
+
The normalized absolute path
|
18
20
|
"""
|
19
21
|
# If path is absolute, return it as is
|
20
22
|
if os.path.isabs(path):
|
@@ -24,65 +26,9 @@ def normalize_path(path: str) -> str:
|
|
24
26
|
if path.startswith('./'):
|
25
27
|
path = path[2:]
|
26
28
|
|
27
|
-
#
|
28
|
-
# Only prepend workspace_dir if we need to resolve the path
|
29
|
-
# against the workspace directory
|
30
|
-
return path
|
31
|
-
|
32
|
-
def backup_file(file_path: str, content: str) -> None:
|
33
|
-
"""
|
34
|
-
Backup a file before editing it.
|
35
|
-
|
36
|
-
Args:
|
37
|
-
file_path: Path to the file being edited
|
38
|
-
content: Current content of the file
|
39
|
-
"""
|
40
|
-
# Get workspace directory
|
41
|
-
workspace_dir = get_config().workspace_dir
|
42
|
-
|
43
|
-
# Create .janito/undo directory in the workspace if it doesn't exist
|
44
|
-
backup_dir = pathlib.Path(workspace_dir) / ".janito" / "undo"
|
45
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
46
|
-
|
47
|
-
# Store the original path
|
48
|
-
path_file = backup_dir / "path"
|
49
|
-
with open(path_file, 'w', encoding='utf-8') as f:
|
50
|
-
f.write(file_path)
|
51
|
-
|
52
|
-
# Store the original content
|
53
|
-
content_file = backup_dir / "content"
|
54
|
-
with open(content_file, 'w', encoding='utf-8') as f:
|
55
|
-
f.write(content)
|
56
|
-
|
57
|
-
def get_backup_info() -> Optional[Dict[str, str]]:
|
58
|
-
"""
|
59
|
-
Get the backup information for the last edited file.
|
60
|
-
|
61
|
-
Returns:
|
62
|
-
Dictionary with 'path' and 'content' keys, or None if no backup exists
|
63
|
-
"""
|
64
|
-
# Get workspace directory
|
29
|
+
# Convert relative paths to absolute paths for internal operations
|
65
30
|
workspace_dir = get_config().workspace_dir
|
66
|
-
|
67
|
-
path_file = pathlib.Path(workspace_dir) / ".janito" / "undo" / "path"
|
68
|
-
content_file = pathlib.Path(workspace_dir) / ".janito" / "undo" / "content"
|
69
|
-
|
70
|
-
if not path_file.exists() or not content_file.exists():
|
71
|
-
return None
|
72
|
-
|
73
|
-
try:
|
74
|
-
with open(path_file, 'r', encoding='utf-8') as f:
|
75
|
-
path = f.read()
|
76
|
-
|
77
|
-
with open(content_file, 'r', encoding='utf-8') as f:
|
78
|
-
content = f.read()
|
79
|
-
|
80
|
-
return {
|
81
|
-
'path': path,
|
82
|
-
'content': content
|
83
|
-
}
|
84
|
-
except Exception:
|
85
|
-
return None
|
31
|
+
return os.path.normpath(os.path.join(workspace_dir, path))
|
86
32
|
|
87
|
-
# Store file history for undo operations (in-memory backup
|
33
|
+
# Store file history for undo operations (in-memory backup)
|
88
34
|
_file_history = {}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
janito/__init__.py,sha256=Ral9Ev43UBuUjoDGLTUMJ62fAiWUC6zMMkOKTkzAQmo,53
|
2
|
+
janito/__main__.py,sha256=IvUnkU8o5JEvwDkv-gXgPXdQWVubY7-0tiVOZrieu-s,8779
|
3
|
+
janito/callbacks.py,sha256=5SoXyyvksSK_xusch3Wb5dIBohChAD2g3B51CehUxYY,4916
|
4
|
+
janito/chat_history.py,sha256=LH1vBq472Duw3kis8FushjwZtI052IPsFZcKwaVjytM,3622
|
5
|
+
janito/config.py,sha256=ZKW4PtiSkayaXaT0jtLFUALl3CnXfgqFwxRwuZfb0es,4432
|
6
|
+
janito/test_file.py,sha256=c6GWGdTYG3z-Y5XBao9Tmhmq3G-v0L37OfwLgBo8zIU,126
|
7
|
+
janito/token_report.py,sha256=1ja93AGTMCXNV0WMUq2nmstKrrt9UEukjheOUVKEmzY,8842
|
8
|
+
janito/data/instructions.txt,sha256=ivNjw7s5qXYqV7K8ZLlPII_XSQSvtWq7WCHx3sr0b-I,232
|
9
|
+
janito/tools/__init__.py,sha256=XW9EaEishvAf4qtylJquti77Q8OL3dVyAIPO_LUUFFg,646
|
10
|
+
janito/tools/bash.py,sha256=FkVUYaPfo_yZ2zUiGKtE_7sPKJlWVBdtIdCB7m7-g4s,737
|
11
|
+
janito/tools/decorators.py,sha256=SMirsokAwesiS4ZoZhxXSenMR78Vh-C9PGLpyvW0Xwo,3095
|
12
|
+
janito/tools/delete_file.py,sha256=L5oilAuO3OtJ1qhmtRO-lVQMwDKteP-P3S55PI9g-AE,1442
|
13
|
+
janito/tools/find_files.py,sha256=VDoait4sMaJtQdECltTQzOAN_5FRpp8j5bFTznOnbcw,6280
|
14
|
+
janito/tools/prompt_user.py,sha256=TMa9BZk5t8AGwj_Zzxu9ciIXm1J0PhRMolGv6V5MPUA,738
|
15
|
+
janito/tools/replace_file.py,sha256=NFlmrvXagFwxfDk1Py-qZYLMXDkc_MiPAzaeQqvDBtk,1115
|
16
|
+
janito/tools/search_text.py,sha256=d3iYMGHS7ZeMJxek4A_7DwMrnsWkp_XOtp3bkyNor0M,9292
|
17
|
+
janito/tools/str_replace_editor/__init__.py,sha256=kYmscmQgft3Jzt3oCNz7k2FiRbJvku6OFDDC3Q_zoAA,144
|
18
|
+
janito/tools/str_replace_editor/editor.py,sha256=DqoK5Yn_3hClisUOkRfSIMvkPVofBuGIe8UpD93K9W0,2306
|
19
|
+
janito/tools/str_replace_editor/handlers.py,sha256=2HlBAnzg6S8YSfGtGZuQp857Rfn6nybew3TWf5TPnuI,13395
|
20
|
+
janito/tools/str_replace_editor/utils.py,sha256=h-unWcrVEcH0xgsW-iJuApUa0Lc4gsPyBlmtn2mw64I,979
|
21
|
+
janito/data/instructions.txt,sha256=ivNjw7s5qXYqV7K8ZLlPII_XSQSvtWq7WCHx3sr0b-I,232
|
22
|
+
janito-0.11.0.dist-info/METADATA,sha256=pTblnOR4OX2AqiuV6wyDPj7gDD2vRxDx4CvLLa281Nc,2146
|
23
|
+
janito-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
24
|
+
janito-0.11.0.dist-info/entry_points.txt,sha256=JMbF_1jg-xQddidpAYkzjOKdw70fy_ymJfcmerY2wIY,47
|
25
|
+
janito-0.11.0.dist-info/licenses/LICENSE,sha256=6-H8LXExbBIAuT4cyiE-Qy8Bad1K4pagQRVTWr6wkhk,1096
|
26
|
+
janito-0.11.0.dist-info/RECORD,,
|
janito/cli.py
DELETED
@@ -1,202 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
CLI functionality for the janito tool.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import os
|
6
|
-
import sys
|
7
|
-
from pathlib import Path
|
8
|
-
from typing import Optional
|
9
|
-
import typer
|
10
|
-
from rich.console import Console
|
11
|
-
from rich.markdown import Markdown
|
12
|
-
from rich import print as rprint
|
13
|
-
import claudine
|
14
|
-
from claudine.exceptions import MaxTokensExceededException, MaxRoundsExceededException
|
15
|
-
|
16
|
-
from janito.config import get_config
|
17
|
-
from janito.tools import find_files
|
18
|
-
from janito.tools.str_replace_editor.editor import str_replace_editor
|
19
|
-
from janito.tools.delete_file import delete_file
|
20
|
-
from janito.tools.search_text import search_text
|
21
|
-
from janito.callbacks import pre_tool_callback, post_tool_callback
|
22
|
-
|
23
|
-
app = typer.Typer(help="Janito CLI tool")
|
24
|
-
|
25
|
-
@app.command()
|
26
|
-
def hello(name: str = typer.Argument("World", help="Name to greet")):
|
27
|
-
"""
|
28
|
-
Say hello to someone.
|
29
|
-
"""
|
30
|
-
rprint(f"[bold green]Hello {name}[/bold green]")
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def debug_tokens(agent):
|
35
|
-
"""
|
36
|
-
Display detailed token usage and pricing information.
|
37
|
-
"""
|
38
|
-
from claudine.token_tracking import MODEL_PRICING, DEFAULT_MODEL
|
39
|
-
|
40
|
-
console = Console()
|
41
|
-
usage = agent.get_token_usage()
|
42
|
-
text_usage = usage.text_usage
|
43
|
-
tools_usage = usage.tools_usage
|
44
|
-
total_usage = usage.total_usage
|
45
|
-
|
46
|
-
# Get the pricing model
|
47
|
-
pricing = MODEL_PRICING.get(DEFAULT_MODEL)
|
48
|
-
|
49
|
-
# Calculate costs manually
|
50
|
-
text_input_cost = pricing.input_tokens.calculate_cost(text_usage.input_tokens)
|
51
|
-
text_output_cost = pricing.output_tokens.calculate_cost(text_usage.output_tokens)
|
52
|
-
tools_input_cost = pricing.input_tokens.calculate_cost(tools_usage.input_tokens)
|
53
|
-
tools_output_cost = pricing.output_tokens.calculate_cost(tools_usage.output_tokens)
|
54
|
-
|
55
|
-
# Format costs
|
56
|
-
format_cost = lambda cost: f"{cost * 100:.2f}¢" if cost < 1.0 else f"${cost:.6f}"
|
57
|
-
|
58
|
-
console.print("\n[bold blue]Detailed Token Usage:[/bold blue]")
|
59
|
-
console.print(f"Text Input tokens: {text_usage.input_tokens}")
|
60
|
-
console.print(f"Text Output tokens: {text_usage.output_tokens}")
|
61
|
-
console.print(f"Text Total tokens: {text_usage.input_tokens + text_usage.output_tokens}")
|
62
|
-
console.print(f"Tool Input tokens: {tools_usage.input_tokens}")
|
63
|
-
console.print(f"Tool Output tokens: {tools_usage.output_tokens}")
|
64
|
-
console.print(f"Tool Total tokens: {tools_usage.input_tokens + tools_usage.output_tokens}")
|
65
|
-
console.print(f"Total tokens: {total_usage.input_tokens + total_usage.output_tokens}")
|
66
|
-
|
67
|
-
console.print("\n[bold blue]Pricing Information:[/bold blue]")
|
68
|
-
console.print(f"Input pricing: ${pricing.input_tokens.cost_per_million_tokens}/million tokens")
|
69
|
-
console.print(f"Output pricing: ${pricing.output_tokens.cost_per_million_tokens}/million tokens")
|
70
|
-
console.print(f"Text Input cost: {format_cost(text_input_cost)}")
|
71
|
-
console.print(f"Text Output cost: {format_cost(text_output_cost)}")
|
72
|
-
console.print(f"Text Total cost: {format_cost(text_input_cost + text_output_cost)}")
|
73
|
-
console.print(f"Tool Input cost: {format_cost(tools_input_cost)}")
|
74
|
-
console.print(f"Tool Output cost: {format_cost(tools_output_cost)}")
|
75
|
-
console.print(f"Tool Total cost: {format_cost(tools_input_cost + tools_output_cost)}")
|
76
|
-
console.print(f"Total cost: {format_cost(text_input_cost + text_output_cost + tools_input_cost + tools_output_cost)}")
|
77
|
-
|
78
|
-
# Display per-tool breakdown if available
|
79
|
-
if usage.by_tool:
|
80
|
-
console.print("\n[bold blue]Per-Tool Breakdown:[/bold blue]")
|
81
|
-
for tool_name, tool_usage in usage.by_tool.items():
|
82
|
-
tool_input_cost = pricing.input_tokens.calculate_cost(tool_usage.input_tokens)
|
83
|
-
tool_output_cost = pricing.output_tokens.calculate_cost(tool_usage.output_tokens)
|
84
|
-
console.print(f" Tool: {tool_name}")
|
85
|
-
console.print(f" Input tokens: {tool_usage.input_tokens}")
|
86
|
-
console.print(f" Output tokens: {tool_usage.output_tokens}")
|
87
|
-
console.print(f" Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens}")
|
88
|
-
console.print(f" Total cost: {format_cost(tool_input_cost + tool_output_cost)}")
|
89
|
-
|
90
|
-
def process_query(query: str, debug: bool, verbose: bool):
|
91
|
-
"""
|
92
|
-
Process a query using the claudine agent.
|
93
|
-
"""
|
94
|
-
console = Console()
|
95
|
-
|
96
|
-
# Get API key from environment variable or ask the user
|
97
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
98
|
-
if not api_key:
|
99
|
-
console.print("[bold yellow]Warning:[/bold yellow] ANTHROPIC_API_KEY environment variable not set.")
|
100
|
-
console.print("Please set it or provide your API key now:")
|
101
|
-
api_key = typer.prompt("Anthropic API Key", hide_input=True)
|
102
|
-
|
103
|
-
# Load instructions from file
|
104
|
-
import importlib.resources as pkg_resources
|
105
|
-
try:
|
106
|
-
# For Python 3.9+
|
107
|
-
try:
|
108
|
-
from importlib.resources import files
|
109
|
-
instructions = files('janito.data').joinpath('instructions.txt').read_text()
|
110
|
-
# Fallback for older Python versions
|
111
|
-
except (ImportError, AttributeError):
|
112
|
-
instructions = pkg_resources.read_text('janito.data', 'instructions.txt')
|
113
|
-
instructions = instructions.strip()
|
114
|
-
except Exception as e:
|
115
|
-
console.print(f"[bold yellow]Warning:[/bold yellow] Could not load instructions file: {str(e)}")
|
116
|
-
console.print("[dim]Using default instructions instead.[/dim]")
|
117
|
-
instructions = "You are a helpful AI assistant. Answer the user's questions to the best of your ability."
|
118
|
-
|
119
|
-
# Initialize the agent with the tools
|
120
|
-
agent = claudine.Agent(
|
121
|
-
api_key=api_key,
|
122
|
-
tools=[
|
123
|
-
delete_file,
|
124
|
-
find_files,
|
125
|
-
search_text
|
126
|
-
],
|
127
|
-
text_editor_tool=str_replace_editor,
|
128
|
-
tool_callbacks=(pre_tool_callback, post_tool_callback),
|
129
|
-
max_tokens=4096,
|
130
|
-
temperature=0.7,
|
131
|
-
instructions=instructions,
|
132
|
-
debug_mode=debug # Enable debug mode
|
133
|
-
)
|
134
|
-
|
135
|
-
# Process the query
|
136
|
-
console.print(f"[bold blue]Query:[/bold blue] {query}")
|
137
|
-
console.print("[bold blue]Generating response...[/bold blue]")
|
138
|
-
|
139
|
-
try:
|
140
|
-
response = agent.process_prompt(query)
|
141
|
-
|
142
|
-
console.print("\n[bold magenta]Janito:[/bold magenta] ", end="")
|
143
|
-
# Use rich's enhanced Markdown rendering for the response
|
144
|
-
console.print(Markdown(response, code_theme="monokai"))
|
145
|
-
|
146
|
-
except MaxTokensExceededException as e:
|
147
|
-
# Display the partial response if available
|
148
|
-
if e.response_text:
|
149
|
-
console.print("\n[bold magenta]Janito:[/bold magenta] ", end="")
|
150
|
-
console.print(Markdown(e.response_text, code_theme="monokai"))
|
151
|
-
|
152
|
-
console.print("\n[bold red]Error:[/bold red] Response was truncated because it reached the maximum token limit.")
|
153
|
-
console.print("[dim]Consider increasing the max_tokens parameter or simplifying your query.[/dim]")
|
154
|
-
|
155
|
-
except MaxRoundsExceededException as e:
|
156
|
-
# Display the final response if available
|
157
|
-
if e.response_text:
|
158
|
-
console.print("\n[bold magenta]Janito:[/bold magenta] ", end="")
|
159
|
-
console.print(Markdown(e.response_text, code_theme="monokai"))
|
160
|
-
|
161
|
-
console.print(f"\n[bold red]Error:[/bold red] Maximum number of tool execution rounds ({e.rounds}) reached. Some tasks may be incomplete.")
|
162
|
-
console.print("[dim]Consider increasing the max_rounds parameter or breaking down your task into smaller steps.[/dim]")
|
163
|
-
|
164
|
-
# Show token usage
|
165
|
-
usage = agent.get_token_usage()
|
166
|
-
text_usage = usage.text_usage
|
167
|
-
tools_usage = usage.tools_usage
|
168
|
-
|
169
|
-
if verbose:
|
170
|
-
debug_tokens(agent)
|
171
|
-
else:
|
172
|
-
total_tokens = text_usage.input_tokens + text_usage.output_tokens + tools_usage.input_tokens + tools_usage.output_tokens
|
173
|
-
cost_info = agent.get_cost()
|
174
|
-
cost_display = cost_info.format_total_cost() if hasattr(cost_info, 'format_total_cost') else ""
|
175
|
-
# Consolidated tokens and cost in a single line with a ruler
|
176
|
-
console.print(Rule(f"Tokens: {total_tokens} | Cost: {cost_display}", style="dim", align="center"))
|
177
|
-
|
178
|
-
@app.callback(invoke_without_command=True)
|
179
|
-
def main(ctx: typer.Context,
|
180
|
-
query: Optional[str] = typer.Argument(None, help="Query to send to the claudine agent"),
|
181
|
-
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug mode"),
|
182
|
-
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed token usage and pricing information"),
|
183
|
-
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Set the workspace directory")):
|
184
|
-
"""
|
185
|
-
Janito CLI tool. If a query is provided without a command, it will be sent to the claudine agent.
|
186
|
-
"""
|
187
|
-
console = Console()
|
188
|
-
|
189
|
-
# Set debug mode in config
|
190
|
-
get_config().debug_mode = debug
|
191
|
-
|
192
|
-
if workspace:
|
193
|
-
try:
|
194
|
-
print(f"Setting workspace directory to: {workspace}")
|
195
|
-
get_config().workspace_dir = workspace
|
196
|
-
print(f"Workspace directory set to: {get_config().workspace_dir}")
|
197
|
-
except ValueError as e:
|
198
|
-
console.print(f"[bold red]Error:[/bold red] {str(e)}")
|
199
|
-
sys.exit(1)
|
200
|
-
|
201
|
-
if ctx.invoked_subcommand is None and query:
|
202
|
-
process_query(query, debug, verbose)
|