janito 0.1.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 +6 -0
- janito/__main__.py +9 -0
- janito/change.py +382 -0
- janito/claude.py +112 -0
- janito/commands.py +377 -0
- janito/console.py +354 -0
- janito/janito.py +354 -0
- janito/prompts.py +181 -0
- janito/watcher.py +82 -0
- janito/workspace.py +169 -0
- janito/xmlchangeparser.py +202 -0
- janito-0.1.0.dist-info/LICENSE +21 -0
- janito-0.1.0.dist-info/METADATA +106 -0
- janito-0.1.0.dist-info/RECORD +19 -0
- janito-0.1.0.dist-info/WHEEL +5 -0
- janito-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +4 -0
- tests/conftest.py +9 -0
- tests/test_change.py +393 -0
janito/console.py
ADDED
@@ -0,0 +1,354 @@
|
|
1
|
+
from prompt_toolkit import PromptSession
|
2
|
+
from prompt_toolkit.completion import WordCompleter, Completer, Completion
|
3
|
+
from prompt_toolkit.formatted_text import HTML
|
4
|
+
from prompt_toolkit.styles import Style
|
5
|
+
from prompt_toolkit.document import Document
|
6
|
+
from prompt_toolkit.completion.base import CompleteEvent
|
7
|
+
from pathlib import Path
|
8
|
+
import os
|
9
|
+
import sys
|
10
|
+
import readline
|
11
|
+
import signal
|
12
|
+
import subprocess
|
13
|
+
import traceback
|
14
|
+
from rich.markdown import Markdown
|
15
|
+
from typing import Optional, Iterable, AsyncGenerator
|
16
|
+
from janito.commands import JanitoCommands # Add this import
|
17
|
+
from janito.watcher import FileWatcher # Add this import
|
18
|
+
|
19
|
+
class PathCompleter(Completer):
|
20
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
|
21
|
+
text = document.text_before_cursor
|
22
|
+
|
23
|
+
# Handle dot command completion
|
24
|
+
if text.startswith('.') or text == '':
|
25
|
+
commands = [
|
26
|
+
'.help', '.exit', '.clear', '.save', '.load',
|
27
|
+
'.debug', '.cache', '.content', '.show' # Added .show
|
28
|
+
]
|
29
|
+
word = text.lstrip('.')
|
30
|
+
for cmd in commands:
|
31
|
+
if cmd[1:].startswith(word):
|
32
|
+
yield Completion(
|
33
|
+
cmd,
|
34
|
+
start_position=-len(text),
|
35
|
+
display=HTML(f'<cmd>{cmd}</cmd>')
|
36
|
+
)
|
37
|
+
return
|
38
|
+
|
39
|
+
# Handle path completion
|
40
|
+
path = Path('.' if not text else text)
|
41
|
+
|
42
|
+
try:
|
43
|
+
if path.is_dir():
|
44
|
+
directory = path
|
45
|
+
prefix = ''
|
46
|
+
else:
|
47
|
+
directory = path.parent
|
48
|
+
prefix = path.name
|
49
|
+
|
50
|
+
for item in directory.iterdir():
|
51
|
+
if item.name.startswith(prefix):
|
52
|
+
yield Completion(
|
53
|
+
str(item),
|
54
|
+
start_position=-len(prefix) if prefix else 0,
|
55
|
+
display=HTML(f'{"/" if item.is_dir() else ""}{item.name}')
|
56
|
+
)
|
57
|
+
except Exception:
|
58
|
+
pass
|
59
|
+
|
60
|
+
async def get_completions_async(self, document: Document, complete_event: CompleteEvent) -> AsyncGenerator[Completion, None]:
|
61
|
+
for completion in self.get_completions(document, complete_event):
|
62
|
+
yield completion
|
63
|
+
|
64
|
+
class JanitoConsole:
|
65
|
+
"""Interactive console for Janito with command handling and REPL"""
|
66
|
+
def __init__(self):
|
67
|
+
self.commands = {
|
68
|
+
'.help': self.help,
|
69
|
+
'.exit': self.exit,
|
70
|
+
'.clear': lambda _: self.janito.clear_console() if self.janito else "Janito not initialized",
|
71
|
+
'.debug': lambda _: self.janito.toggle_debug() if self.janito else "Janito not initialized",
|
72
|
+
'.workspace': lambda _: self.janito.show_workspace() if self.janito else "Janito not initialized",
|
73
|
+
'.last': lambda _: self.janito.get_last_response() if self.janito else "Janito not initialized",
|
74
|
+
'.show': lambda args: self.janito.show_file(args[0]) if args and self.janito else "File path required",
|
75
|
+
'.check': lambda _: self.janito.check_syntax() if self.janito else "Janito not initialized",
|
76
|
+
'.p': lambda args: self.janito.run_python(args[0]) if args and self.janito else "File path required",
|
77
|
+
'.python': lambda args: self.janito.run_python(args[0]) if args and self.janito else "File path required",
|
78
|
+
'.edit': lambda args: self.janito.edit_file(args[0]) if args and self.janito else "File path required",
|
79
|
+
}
|
80
|
+
|
81
|
+
try:
|
82
|
+
api_key = os.getenv('ANTHROPIC_API_KEY')
|
83
|
+
if not api_key:
|
84
|
+
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
85
|
+
self.janito = JanitoCommands(api_key=api_key)
|
86
|
+
except ValueError as e:
|
87
|
+
print(f"Warning: Janito initialization failed - {str(e)}")
|
88
|
+
self.janito = None
|
89
|
+
|
90
|
+
self.session = PromptSession(
|
91
|
+
completer=PathCompleter(),
|
92
|
+
style=Style.from_dict({
|
93
|
+
'ai': '#00aa00 bold',
|
94
|
+
'path': '#3388ff bold',
|
95
|
+
'sep': '#888888',
|
96
|
+
'prompt': '#ff3333 bold',
|
97
|
+
'cmd': '#00aa00',
|
98
|
+
})
|
99
|
+
)
|
100
|
+
self.running = True
|
101
|
+
self.restart_requested = False
|
102
|
+
self.package_dir = os.path.dirname(os.path.dirname(__file__))
|
103
|
+
self.workspace = None
|
104
|
+
self._setup_file_watcher()
|
105
|
+
self._setup_signal_handlers()
|
106
|
+
self._load_history()
|
107
|
+
|
108
|
+
def _load_history(self):
|
109
|
+
"""Load command history from file"""
|
110
|
+
try:
|
111
|
+
if self.janito and self.janito.workspace.history_file.exists():
|
112
|
+
with open(self.janito.workspace.history_file) as f:
|
113
|
+
# Clear existing history first
|
114
|
+
readline.clear_history()
|
115
|
+
for line in f:
|
116
|
+
line = line.strip()
|
117
|
+
if line: # Only add non-empty lines
|
118
|
+
readline.add_history(line)
|
119
|
+
except Exception as e:
|
120
|
+
print(f"Warning: Could not load command history: {e}")
|
121
|
+
|
122
|
+
def _save_history(self, new_command: str = None):
|
123
|
+
"""Save command history to file, optionally adding a new command first"""
|
124
|
+
try:
|
125
|
+
if self.janito:
|
126
|
+
# Add new command to history if provided
|
127
|
+
if new_command and new_command.strip():
|
128
|
+
readline.add_history(new_command)
|
129
|
+
|
130
|
+
history_path = self.janito.workspace.history_file
|
131
|
+
history_path.parent.mkdir(exist_ok=True)
|
132
|
+
|
133
|
+
# Get all history items
|
134
|
+
history = []
|
135
|
+
for i in range(readline.get_current_history_length()):
|
136
|
+
item = readline.get_history_item(i + 1)
|
137
|
+
if item and item.strip(): # Only save non-empty commands
|
138
|
+
history.append(item)
|
139
|
+
|
140
|
+
# Write to file
|
141
|
+
with open(history_path, 'w') as f:
|
142
|
+
f.write('\n'.join(history) + '\n')
|
143
|
+
|
144
|
+
except Exception as e:
|
145
|
+
print(f"Warning: Could not save command history: {e}")
|
146
|
+
|
147
|
+
def _setup_signal_handlers(self):
|
148
|
+
"""Setup signal handlers for clean terminal state"""
|
149
|
+
signal.signal(signal.SIGINT, self._handle_interrupt)
|
150
|
+
signal.signal(signal.SIGTERM, self._handle_interrupt)
|
151
|
+
|
152
|
+
def _handle_interrupt(self, signum, frame):
|
153
|
+
"""Handle interrupt signals"""
|
154
|
+
self.cleanup_terminal()
|
155
|
+
# Signal any waiting operations to stop
|
156
|
+
if self.janito:
|
157
|
+
self.janito.stop_progress.set()
|
158
|
+
if not self.restart_requested:
|
159
|
+
print("\nOperation cancelled.")
|
160
|
+
|
161
|
+
def cleanup_terminal(self):
|
162
|
+
"""Restore terminal settings"""
|
163
|
+
try:
|
164
|
+
# Save history before cleaning up
|
165
|
+
self._save_history()
|
166
|
+
# Reset terminal state
|
167
|
+
os.system('stty sane')
|
168
|
+
# Clear readline state
|
169
|
+
readline.set_startup_hook(None)
|
170
|
+
readline.clear_history()
|
171
|
+
except Exception as e:
|
172
|
+
print(f"Warning: Error cleaning up terminal: {e}")
|
173
|
+
|
174
|
+
def _setup_file_watcher(self):
|
175
|
+
"""Set up file watcher for auto-restart"""
|
176
|
+
def on_file_change(path, content):
|
177
|
+
print("\nJanito source file changed - restarting...")
|
178
|
+
self.restart_requested = True
|
179
|
+
self.running = False
|
180
|
+
self.restart_process()
|
181
|
+
|
182
|
+
try:
|
183
|
+
package_dir = os.path.dirname(os.path.dirname(__file__))
|
184
|
+
self.watcher = FileWatcher(on_file_change, package_dir)
|
185
|
+
self.watcher.start()
|
186
|
+
except Exception as e:
|
187
|
+
print(f"Warning: Could not set up file watcher: {e}")
|
188
|
+
|
189
|
+
def restart_process(self):
|
190
|
+
"""Restart the current process using module invocation"""
|
191
|
+
try:
|
192
|
+
if self.watcher:
|
193
|
+
self.watcher.stop()
|
194
|
+
print("\nRestarting Janito process...")
|
195
|
+
self.cleanup_terminal()
|
196
|
+
|
197
|
+
# Change to package directory for module import
|
198
|
+
os.chdir(self.package_dir)
|
199
|
+
|
200
|
+
python_exe = sys.executable
|
201
|
+
args = [python_exe, "-m", "janito"]
|
202
|
+
|
203
|
+
# Add workspace argument if it was provided, stripping any quotes
|
204
|
+
if self.workspace:
|
205
|
+
workspace_str = str(self.workspace).strip('"\'')
|
206
|
+
args.append(workspace_str)
|
207
|
+
|
208
|
+
os.execv(python_exe, args)
|
209
|
+
except Exception as e:
|
210
|
+
print(f"Error during restart: {e}")
|
211
|
+
self.cleanup_terminal()
|
212
|
+
sys.exit(1)
|
213
|
+
|
214
|
+
def get_prompt(self, cwd=None):
|
215
|
+
"""Generate the command prompt"""
|
216
|
+
return HTML('🤖 ')
|
217
|
+
|
218
|
+
def render_status_bar(self):
|
219
|
+
"""Render the persistent status bar"""
|
220
|
+
cwd = os.getcwd()
|
221
|
+
# Combine the HTML strings before creating HTML object
|
222
|
+
return HTML(
|
223
|
+
'<path>{}</path>'
|
224
|
+
' <hint>(!modify, ?info, normal, $shell_cmd)</hint>'.format(cwd)
|
225
|
+
)
|
226
|
+
|
227
|
+
def help(self, args):
|
228
|
+
"""Show help information"""
|
229
|
+
if args:
|
230
|
+
cmd = args[0]
|
231
|
+
if cmd in self.commands:
|
232
|
+
print(f"{cmd}: {self.commands[cmd].__doc__}")
|
233
|
+
else:
|
234
|
+
print(f"Unknown command: {cmd}")
|
235
|
+
else:
|
236
|
+
print("Available commands:")
|
237
|
+
print(" .help - Show this help")
|
238
|
+
print(" .exit - End session")
|
239
|
+
print(" .clear - Clear console")
|
240
|
+
print(" .debug - Toggle debug mode")
|
241
|
+
print(" .workspace - Show workspace structure")
|
242
|
+
print(" .last - Show last Claude response")
|
243
|
+
print(" .show - Show file content with syntax highlighting")
|
244
|
+
print(" .check - Check workspace Python files for syntax errors")
|
245
|
+
print(" .p - Run a Python file")
|
246
|
+
print(" .python - Run a Python file (alias for .p)")
|
247
|
+
print(" .edit - Open file in system editor")
|
248
|
+
print("\nMessage Modes:")
|
249
|
+
print(" 1. Regular message:")
|
250
|
+
print(" Example: how does the file watcher work")
|
251
|
+
print(" Use for: General discussion and questions about code")
|
252
|
+
print("\n 2. Question mode (ends with ?):")
|
253
|
+
print(" Example: what are the main classes in utils.py?")
|
254
|
+
print(" Use for: Deep analysis and explanations without changes")
|
255
|
+
print("\n 3. Change mode (starts with !):")
|
256
|
+
print(" Example: !add error handling to get_files_content")
|
257
|
+
print(" Use for: Requesting code modifications")
|
258
|
+
print("\n 4. Shell commands (starts with $):")
|
259
|
+
print(" Example: $ls -la")
|
260
|
+
print(" Use for: Executing shell commands")
|
261
|
+
|
262
|
+
def exit(self, args):
|
263
|
+
"""Exit the console"""
|
264
|
+
self._save_history() # Save history before exiting
|
265
|
+
self.running = False
|
266
|
+
self.cleanup_terminal()
|
267
|
+
|
268
|
+
def _execute_shell_command(self, command: str) -> None:
|
269
|
+
"""Execute a shell command and print output"""
|
270
|
+
try:
|
271
|
+
process = subprocess.run(
|
272
|
+
command,
|
273
|
+
shell=True,
|
274
|
+
text=True,
|
275
|
+
capture_output=True
|
276
|
+
)
|
277
|
+
if process.stdout:
|
278
|
+
print(process.stdout.strip())
|
279
|
+
if process.stderr:
|
280
|
+
print(process.stderr.strip(), file=sys.stderr)
|
281
|
+
except Exception as e:
|
282
|
+
print(f"Error executing command: {e}", file=sys.stderr)
|
283
|
+
|
284
|
+
def run(self):
|
285
|
+
"""Main command loop"""
|
286
|
+
try:
|
287
|
+
while self.running and self.session: # Check session is valid
|
288
|
+
try:
|
289
|
+
command = self.session.prompt(
|
290
|
+
self.get_prompt(),
|
291
|
+
bottom_toolbar=self.render_status_bar()
|
292
|
+
).strip()
|
293
|
+
|
294
|
+
if not command:
|
295
|
+
continue
|
296
|
+
|
297
|
+
# Save history after each command
|
298
|
+
self._save_history(command)
|
299
|
+
|
300
|
+
if command.startswith('$'):
|
301
|
+
# Handle shell command
|
302
|
+
self._execute_shell_command(command[1:].strip())
|
303
|
+
elif command.startswith('.'):
|
304
|
+
parts = command.split()
|
305
|
+
cmd, args = parts[0], parts[1:]
|
306
|
+
if cmd in self.commands:
|
307
|
+
result = self.commands[cmd](args)
|
308
|
+
if result:
|
309
|
+
print(result)
|
310
|
+
else:
|
311
|
+
print(f"Unknown command: {cmd}")
|
312
|
+
elif command.startswith('!'):
|
313
|
+
# Handle file change request
|
314
|
+
print("\n[Using Change Request Prompt]")
|
315
|
+
result = self.janito.handle_file_change(command[1:]) # Remove ! prefix
|
316
|
+
print(f"\n{result}")
|
317
|
+
elif command.endswith('?'):
|
318
|
+
# Handle information request
|
319
|
+
print("\n[Using Information Request Prompt]")
|
320
|
+
workspace_status = self.janito.get_workspace_status()
|
321
|
+
result = self.janito.handle_info_request(command[:-1], workspace_status) # Remove ? suffix
|
322
|
+
print(f"\n{result}")
|
323
|
+
else:
|
324
|
+
# Handle regular message with markdown rendering
|
325
|
+
print("\n[Using General Message Prompt]")
|
326
|
+
result = self.janito.send_message(command)
|
327
|
+
md = Markdown(result)
|
328
|
+
self.janito.console.print("\n") # Add newline before response
|
329
|
+
self.janito.console.print(md)
|
330
|
+
print("") # Add newline after response
|
331
|
+
|
332
|
+
except EOFError:
|
333
|
+
self.exit([])
|
334
|
+
break
|
335
|
+
except (KeyboardInterrupt, SystemExit):
|
336
|
+
if self.restart_requested:
|
337
|
+
break
|
338
|
+
if not self.restart_requested: # Only exit if not restarting
|
339
|
+
self.exit([])
|
340
|
+
break
|
341
|
+
finally:
|
342
|
+
if self.watcher:
|
343
|
+
self.watcher.stop()
|
344
|
+
# Save history one final time before cleanup
|
345
|
+
self._save_history()
|
346
|
+
if self.restart_requested:
|
347
|
+
self.restart_process()
|
348
|
+
else:
|
349
|
+
self.cleanup_terminal()
|
350
|
+
|
351
|
+
# Print welcome message after file watcher setup
|
352
|
+
print("\nWelcome to Janito - your friendly AI coding assistant!")
|
353
|
+
print("Type '.help' to see available commands.")
|
354
|
+
print("")
|