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/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("")