omnimancer-cli 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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced input handler with arrow key navigation and command history.
|
|
3
|
+
|
|
4
|
+
This module provides advanced terminal input capabilities including:
|
|
5
|
+
- Arrow key navigation through command history
|
|
6
|
+
- Tab completion
|
|
7
|
+
- Search functionality
|
|
8
|
+
- Multi-line input support
|
|
9
|
+
- Rich formatting and display
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import select
|
|
15
|
+
import sys
|
|
16
|
+
import termios
|
|
17
|
+
import tty
|
|
18
|
+
from typing import Callable, List, Optional
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
from .history_manager import HistoryManager
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class KeyCodes:
|
|
28
|
+
"""Terminal key codes for cross-platform compatibility."""
|
|
29
|
+
|
|
30
|
+
# Control characters
|
|
31
|
+
CTRL_C = "\x03"
|
|
32
|
+
CTRL_D = "\x04"
|
|
33
|
+
CTRL_L = "\x0c"
|
|
34
|
+
CTRL_R = "\x12"
|
|
35
|
+
CTRL_U = "\x15"
|
|
36
|
+
TAB = "\t"
|
|
37
|
+
ENTER = "\r"
|
|
38
|
+
BACKSPACE = "\x7f"
|
|
39
|
+
ESC = "\x1b"
|
|
40
|
+
|
|
41
|
+
# Arrow keys (escape sequences)
|
|
42
|
+
ARROW_UP = "\x1b[A"
|
|
43
|
+
ARROW_DOWN = "\x1b[B"
|
|
44
|
+
ARROW_RIGHT = "\x1b[C"
|
|
45
|
+
ARROW_LEFT = "\x1b[D"
|
|
46
|
+
|
|
47
|
+
# Other keys
|
|
48
|
+
HOME = "\x1b[H"
|
|
49
|
+
END = "\x1b[F"
|
|
50
|
+
DELETE = "\x1b[3~"
|
|
51
|
+
PAGE_UP = "\x1b[5~"
|
|
52
|
+
PAGE_DOWN = "\x1b[6~"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class EnhancedInput:
|
|
56
|
+
"""
|
|
57
|
+
Enhanced input handler with history navigation and advanced editing.
|
|
58
|
+
|
|
59
|
+
Features:
|
|
60
|
+
- Arrow key navigation through command history
|
|
61
|
+
- Basic line editing (cursor movement, backspace, delete)
|
|
62
|
+
- Search functionality (Ctrl+R)
|
|
63
|
+
- Multi-line support
|
|
64
|
+
- Rich display integration
|
|
65
|
+
- Tab completion support
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
history_manager: HistoryManager,
|
|
71
|
+
console: Console,
|
|
72
|
+
prompt: str = ">>> ",
|
|
73
|
+
completion_callback: Optional[Callable[[str], List[str]]] = None,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Initialize the enhanced input handler.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
history_manager: History manager instance
|
|
80
|
+
console: Rich console for output
|
|
81
|
+
prompt: Input prompt string
|
|
82
|
+
completion_callback: Optional tab completion callback
|
|
83
|
+
"""
|
|
84
|
+
self.history_manager = history_manager
|
|
85
|
+
self.console = console
|
|
86
|
+
self.prompt = prompt
|
|
87
|
+
self.completion_callback = completion_callback
|
|
88
|
+
|
|
89
|
+
# Input state
|
|
90
|
+
self.current_line = ""
|
|
91
|
+
self.cursor_pos = 0
|
|
92
|
+
self.in_search_mode = False
|
|
93
|
+
self.search_query = ""
|
|
94
|
+
|
|
95
|
+
# Terminal state
|
|
96
|
+
self.old_termios = None
|
|
97
|
+
self.is_tty = sys.stdin.isatty()
|
|
98
|
+
|
|
99
|
+
# For non-TTY fallback
|
|
100
|
+
self.fallback_mode = not self.is_tty
|
|
101
|
+
|
|
102
|
+
def get_input(self) -> Optional[str]:
|
|
103
|
+
"""
|
|
104
|
+
Get input from user with enhanced features.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
User input string or None for EOF
|
|
108
|
+
"""
|
|
109
|
+
if self.fallback_mode:
|
|
110
|
+
return self._fallback_input()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
return self._enhanced_input()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.warning(f"Enhanced input failed, falling back to basic input: {e}")
|
|
116
|
+
return self._fallback_input()
|
|
117
|
+
|
|
118
|
+
def _fallback_input(self) -> Optional[str]:
|
|
119
|
+
"""Fallback to basic input() for non-TTY environments."""
|
|
120
|
+
try:
|
|
121
|
+
return input(self.prompt)
|
|
122
|
+
except (EOFError, KeyboardInterrupt):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def _enhanced_input(self) -> Optional[str]:
|
|
126
|
+
"""Enhanced input with arrow key support."""
|
|
127
|
+
if not self.is_tty or self.fallback_mode:
|
|
128
|
+
return self._fallback_input()
|
|
129
|
+
|
|
130
|
+
# Set up raw terminal mode
|
|
131
|
+
try:
|
|
132
|
+
self._setup_terminal()
|
|
133
|
+
|
|
134
|
+
# Check if terminal setup failed
|
|
135
|
+
if self.fallback_mode:
|
|
136
|
+
return self._fallback_input()
|
|
137
|
+
|
|
138
|
+
# Display initial prompt
|
|
139
|
+
self._display_prompt()
|
|
140
|
+
|
|
141
|
+
while True:
|
|
142
|
+
try:
|
|
143
|
+
key = self._read_key()
|
|
144
|
+
|
|
145
|
+
if key is None: # EOF
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Handle different key types
|
|
149
|
+
result = self._handle_key(key)
|
|
150
|
+
|
|
151
|
+
if result is not None:
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
# Ctrl+C pressed
|
|
156
|
+
self._restore_terminal()
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
finally:
|
|
160
|
+
self._restore_terminal()
|
|
161
|
+
|
|
162
|
+
def _setup_terminal(self) -> None:
|
|
163
|
+
"""Set up terminal for raw key input."""
|
|
164
|
+
if not self.is_tty:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
self.old_termios = termios.tcgetattr(sys.stdin)
|
|
169
|
+
tty.setraw(sys.stdin.fileno())
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.warning(f"Could not set up terminal: {e}")
|
|
172
|
+
self.fallback_mode = True
|
|
173
|
+
|
|
174
|
+
def _restore_terminal(self) -> None:
|
|
175
|
+
"""Restore terminal to original state."""
|
|
176
|
+
if self.old_termios is not None:
|
|
177
|
+
try:
|
|
178
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_termios)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning(f"Could not restore terminal: {e}")
|
|
181
|
+
|
|
182
|
+
def _read_key(self) -> Optional[str]:
|
|
183
|
+
"""
|
|
184
|
+
Read a single key or key sequence from stdin.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Key string or None for EOF
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
# Check if data is available
|
|
191
|
+
if not select.select([sys.stdin], [], [], 0.1)[0]:
|
|
192
|
+
return "" # No input available, continue
|
|
193
|
+
|
|
194
|
+
# Read first character
|
|
195
|
+
char = sys.stdin.read(1)
|
|
196
|
+
|
|
197
|
+
if not char: # EOF
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
# Handle escape sequences
|
|
201
|
+
if char == KeyCodes.ESC:
|
|
202
|
+
# Read additional characters for escape sequences
|
|
203
|
+
try:
|
|
204
|
+
# Set a short timeout for escape sequences
|
|
205
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
206
|
+
seq = char + sys.stdin.read(2)
|
|
207
|
+
|
|
208
|
+
# Check for longer sequences
|
|
209
|
+
if (
|
|
210
|
+
seq.endswith("[")
|
|
211
|
+
and select.select([sys.stdin], [], [], 0.1)[0]
|
|
212
|
+
):
|
|
213
|
+
seq += sys.stdin.read(1)
|
|
214
|
+
if (
|
|
215
|
+
seq[-1] in "0123456789"
|
|
216
|
+
and select.select([sys.stdin], [], [], 0.1)[0]
|
|
217
|
+
):
|
|
218
|
+
seq += sys.stdin.read(1)
|
|
219
|
+
|
|
220
|
+
return seq
|
|
221
|
+
else:
|
|
222
|
+
return char # Just ESC
|
|
223
|
+
except:
|
|
224
|
+
return char
|
|
225
|
+
|
|
226
|
+
return char
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.debug(f"Error reading key: {e}")
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
def _handle_key(self, key: str) -> Optional[str]:
|
|
233
|
+
"""
|
|
234
|
+
Handle a key press and return command if complete.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
key: Key string from _read_key
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Complete command string or None to continue
|
|
241
|
+
"""
|
|
242
|
+
if not key: # No input
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
# Handle control characters
|
|
246
|
+
if key == KeyCodes.CTRL_C:
|
|
247
|
+
raise KeyboardInterrupt()
|
|
248
|
+
|
|
249
|
+
elif key == KeyCodes.CTRL_D:
|
|
250
|
+
if not self.current_line:
|
|
251
|
+
return None # EOF
|
|
252
|
+
else:
|
|
253
|
+
# Delete character at cursor
|
|
254
|
+
if self.cursor_pos < len(self.current_line):
|
|
255
|
+
self.current_line = (
|
|
256
|
+
self.current_line[: self.cursor_pos]
|
|
257
|
+
+ self.current_line[self.cursor_pos + 1 :]
|
|
258
|
+
)
|
|
259
|
+
self._redraw_line()
|
|
260
|
+
|
|
261
|
+
elif key == KeyCodes.CTRL_L:
|
|
262
|
+
# Clear screen
|
|
263
|
+
self.console.clear()
|
|
264
|
+
self._display_prompt()
|
|
265
|
+
self._redraw_line()
|
|
266
|
+
|
|
267
|
+
elif key == KeyCodes.CTRL_R:
|
|
268
|
+
# Start reverse search
|
|
269
|
+
self._start_search_mode()
|
|
270
|
+
|
|
271
|
+
elif key == KeyCodes.CTRL_U:
|
|
272
|
+
# Clear line
|
|
273
|
+
self.current_line = ""
|
|
274
|
+
self.cursor_pos = 0
|
|
275
|
+
self._redraw_line()
|
|
276
|
+
|
|
277
|
+
elif key in (KeyCodes.ENTER, "\n"):
|
|
278
|
+
# Submit command
|
|
279
|
+
command = self.current_line
|
|
280
|
+
self._finish_input()
|
|
281
|
+
return command
|
|
282
|
+
|
|
283
|
+
elif key == KeyCodes.BACKSPACE:
|
|
284
|
+
# Backspace
|
|
285
|
+
if self.cursor_pos > 0:
|
|
286
|
+
self.current_line = (
|
|
287
|
+
self.current_line[: self.cursor_pos - 1]
|
|
288
|
+
+ self.current_line[self.cursor_pos :]
|
|
289
|
+
)
|
|
290
|
+
self.cursor_pos -= 1
|
|
291
|
+
self._redraw_line()
|
|
292
|
+
|
|
293
|
+
elif key == KeyCodes.TAB:
|
|
294
|
+
# Tab completion
|
|
295
|
+
self._handle_tab_completion()
|
|
296
|
+
|
|
297
|
+
# Handle arrow keys
|
|
298
|
+
elif key == KeyCodes.ARROW_UP:
|
|
299
|
+
self._handle_history_up()
|
|
300
|
+
|
|
301
|
+
elif key == KeyCodes.ARROW_DOWN:
|
|
302
|
+
self._handle_history_down()
|
|
303
|
+
|
|
304
|
+
elif key == KeyCodes.ARROW_LEFT:
|
|
305
|
+
# Move cursor left
|
|
306
|
+
if self.cursor_pos > 0:
|
|
307
|
+
self.cursor_pos -= 1
|
|
308
|
+
self._update_cursor()
|
|
309
|
+
|
|
310
|
+
elif key == KeyCodes.ARROW_RIGHT:
|
|
311
|
+
# Move cursor right
|
|
312
|
+
if self.cursor_pos < len(self.current_line):
|
|
313
|
+
self.cursor_pos += 1
|
|
314
|
+
self._update_cursor()
|
|
315
|
+
|
|
316
|
+
elif key == KeyCodes.HOME:
|
|
317
|
+
# Move to beginning of line
|
|
318
|
+
self.cursor_pos = 0
|
|
319
|
+
self._update_cursor()
|
|
320
|
+
|
|
321
|
+
elif key == KeyCodes.END:
|
|
322
|
+
# Move to end of line
|
|
323
|
+
self.cursor_pos = len(self.current_line)
|
|
324
|
+
self._update_cursor()
|
|
325
|
+
|
|
326
|
+
# Handle printable characters
|
|
327
|
+
elif len(key) == 1 and key.isprintable():
|
|
328
|
+
# Insert character at cursor
|
|
329
|
+
self.current_line = (
|
|
330
|
+
self.current_line[: self.cursor_pos]
|
|
331
|
+
+ key
|
|
332
|
+
+ self.current_line[self.cursor_pos :]
|
|
333
|
+
)
|
|
334
|
+
self.cursor_pos += 1
|
|
335
|
+
self._redraw_line()
|
|
336
|
+
|
|
337
|
+
return None # Continue input
|
|
338
|
+
|
|
339
|
+
def _handle_history_up(self) -> None:
|
|
340
|
+
"""Handle up arrow key - previous command in history."""
|
|
341
|
+
if self.in_search_mode:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
prev_command = self.history_manager.get_previous_command()
|
|
345
|
+
if prev_command is not None:
|
|
346
|
+
self.current_line = prev_command
|
|
347
|
+
self.cursor_pos = len(self.current_line)
|
|
348
|
+
self._redraw_line()
|
|
349
|
+
|
|
350
|
+
def _handle_history_down(self) -> None:
|
|
351
|
+
"""Handle down arrow key - next command in history."""
|
|
352
|
+
if self.in_search_mode:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
next_command = self.history_manager.get_next_command()
|
|
356
|
+
if next_command is not None:
|
|
357
|
+
self.current_line = next_command
|
|
358
|
+
self.cursor_pos = len(self.current_line)
|
|
359
|
+
self._redraw_line()
|
|
360
|
+
|
|
361
|
+
def _handle_tab_completion(self) -> None:
|
|
362
|
+
"""Handle tab completion."""
|
|
363
|
+
if not self.completion_callback:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
# Get completion text (word before cursor)
|
|
368
|
+
words = self.current_line[: self.cursor_pos].split()
|
|
369
|
+
if words:
|
|
370
|
+
current_word = words[-1]
|
|
371
|
+
else:
|
|
372
|
+
current_word = ""
|
|
373
|
+
|
|
374
|
+
# Get completions
|
|
375
|
+
completions = self.completion_callback(current_word)
|
|
376
|
+
|
|
377
|
+
if not completions:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
if len(completions) == 1:
|
|
381
|
+
# Single completion - insert it
|
|
382
|
+
completion = completions[0]
|
|
383
|
+
if words:
|
|
384
|
+
# Replace last word
|
|
385
|
+
prefix = self.current_line[: self.cursor_pos - len(current_word)]
|
|
386
|
+
suffix = self.current_line[self.cursor_pos :]
|
|
387
|
+
self.current_line = prefix + completion + suffix
|
|
388
|
+
self.cursor_pos = len(prefix + completion)
|
|
389
|
+
else:
|
|
390
|
+
# Insert at cursor
|
|
391
|
+
self.current_line = (
|
|
392
|
+
completion + self.current_line[self.cursor_pos :]
|
|
393
|
+
)
|
|
394
|
+
self.cursor_pos = len(completion)
|
|
395
|
+
|
|
396
|
+
self._redraw_line()
|
|
397
|
+
else:
|
|
398
|
+
# Multiple completions - show them
|
|
399
|
+
self._show_completions(completions)
|
|
400
|
+
|
|
401
|
+
except Exception as e:
|
|
402
|
+
logger.debug(f"Tab completion error: {e}")
|
|
403
|
+
|
|
404
|
+
def _start_search_mode(self) -> None:
|
|
405
|
+
"""Start reverse search mode."""
|
|
406
|
+
self.in_search_mode = True
|
|
407
|
+
self.search_query = ""
|
|
408
|
+
# TODO: Implement search mode display
|
|
409
|
+
# For now, just exit search mode
|
|
410
|
+
self.in_search_mode = False
|
|
411
|
+
|
|
412
|
+
def _show_completions(self, completions: List[str]) -> None:
|
|
413
|
+
"""Show available completions."""
|
|
414
|
+
# Move to new line
|
|
415
|
+
sys.stdout.write("\n")
|
|
416
|
+
|
|
417
|
+
# Display completions in columns
|
|
418
|
+
max_width = max(len(c) for c in completions) if completions else 0
|
|
419
|
+
term_width = os.get_terminal_size().columns
|
|
420
|
+
cols = max(1, term_width // (max_width + 2))
|
|
421
|
+
|
|
422
|
+
for i, completion in enumerate(completions):
|
|
423
|
+
if i > 0 and i % cols == 0:
|
|
424
|
+
sys.stdout.write("\n")
|
|
425
|
+
sys.stdout.write(f"{completion:<{max_width + 2}}")
|
|
426
|
+
|
|
427
|
+
sys.stdout.write("\n")
|
|
428
|
+
sys.stdout.flush()
|
|
429
|
+
|
|
430
|
+
# Redraw prompt and current line
|
|
431
|
+
self._display_prompt()
|
|
432
|
+
self._redraw_line()
|
|
433
|
+
|
|
434
|
+
def _display_prompt(self) -> None:
|
|
435
|
+
"""Display the input prompt."""
|
|
436
|
+
sys.stdout.write(self.prompt)
|
|
437
|
+
sys.stdout.flush()
|
|
438
|
+
|
|
439
|
+
def _redraw_line(self) -> None:
|
|
440
|
+
"""Redraw the current input line."""
|
|
441
|
+
# Clear from cursor to end of line
|
|
442
|
+
sys.stdout.write("\033[K")
|
|
443
|
+
# Write current line
|
|
444
|
+
sys.stdout.write(self.current_line)
|
|
445
|
+
# Move cursor to correct position
|
|
446
|
+
if self.cursor_pos < len(self.current_line):
|
|
447
|
+
sys.stdout.write(f"\033[{len(self.current_line) - self.cursor_pos}D")
|
|
448
|
+
sys.stdout.flush()
|
|
449
|
+
|
|
450
|
+
def _update_cursor(self) -> None:
|
|
451
|
+
"""Update cursor position without redrawing line."""
|
|
452
|
+
# This is a simplified version - in practice you'd calculate
|
|
453
|
+
# the exact cursor movement needed
|
|
454
|
+
self._redraw_line()
|
|
455
|
+
|
|
456
|
+
def _finish_input(self) -> None:
|
|
457
|
+
"""Finish input and prepare for next line."""
|
|
458
|
+
sys.stdout.write("\n")
|
|
459
|
+
sys.stdout.flush()
|
|
460
|
+
|
|
461
|
+
# Reset state
|
|
462
|
+
self.current_line = ""
|
|
463
|
+
self.cursor_pos = 0
|
|
464
|
+
self.in_search_mode = False
|
|
465
|
+
self.search_query = ""
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def create_completion_callback(engine) -> Callable[[str], List[str]]:
|
|
469
|
+
"""
|
|
470
|
+
Create a tab completion callback for Omnimancer commands.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
engine: Omnimancer engine instance
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Completion callback function
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
def completion_callback(text: str) -> List[str]:
|
|
480
|
+
"""Generate completions for the given text."""
|
|
481
|
+
completions = []
|
|
482
|
+
|
|
483
|
+
# Slash command completions
|
|
484
|
+
if text.startswith("/"):
|
|
485
|
+
from ..cli.commands import SlashCommand
|
|
486
|
+
|
|
487
|
+
slash_commands = [cmd.value for cmd in SlashCommand]
|
|
488
|
+
completions.extend([cmd for cmd in slash_commands if cmd.startswith(text)])
|
|
489
|
+
|
|
490
|
+
# File path completions
|
|
491
|
+
elif "/" in text or text.startswith(".") or text.startswith("~"):
|
|
492
|
+
try:
|
|
493
|
+
import glob
|
|
494
|
+
|
|
495
|
+
pattern = text + "*"
|
|
496
|
+
matches = glob.glob(pattern)
|
|
497
|
+
# Return only the basename for display
|
|
498
|
+
completions.extend(
|
|
499
|
+
[
|
|
500
|
+
(
|
|
501
|
+
os.path.basename(match)
|
|
502
|
+
if os.path.isfile(match)
|
|
503
|
+
else os.path.basename(match) + "/"
|
|
504
|
+
)
|
|
505
|
+
for match in matches[:20]
|
|
506
|
+
]
|
|
507
|
+
) # Limit results
|
|
508
|
+
except Exception:
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
# Provider name completions (if engine available)
|
|
512
|
+
try:
|
|
513
|
+
if hasattr(engine, "config_manager") and engine.config_manager:
|
|
514
|
+
provider_names = list(engine.config_manager.providers.keys())
|
|
515
|
+
completions.extend(
|
|
516
|
+
[name for name in provider_names if name.startswith(text)]
|
|
517
|
+
)
|
|
518
|
+
except Exception:
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
return sorted(list(set(completions)))
|
|
522
|
+
|
|
523
|
+
return completion_callback
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment variable loader for API keys.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to load API keys from environment variables
|
|
5
|
+
and inject them into provider configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# Mapping of provider names to environment variable names
|
|
15
|
+
ENV_VAR_MAPPING = {
|
|
16
|
+
"claude": "ANTHROPIC_API_KEY",
|
|
17
|
+
"openai": "OPENAI_API_KEY",
|
|
18
|
+
"gemini": "GOOGLE_API_KEY",
|
|
19
|
+
"perplexity": "PERPLEXITY_API_KEY",
|
|
20
|
+
"xai": "XAI_API_KEY",
|
|
21
|
+
"mistral": "MISTRAL_API_KEY",
|
|
22
|
+
"cohere": "COHERE_API_KEY",
|
|
23
|
+
"azure": "AZURE_OPENAI_KEY",
|
|
24
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_api_key_from_env(provider_name: str) -> Optional[str]:
|
|
29
|
+
"""
|
|
30
|
+
Load API key from environment variable for a specific provider.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
provider_name: Name of the provider
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
API key from environment or None if not found
|
|
37
|
+
"""
|
|
38
|
+
env_var = ENV_VAR_MAPPING.get(provider_name)
|
|
39
|
+
if not env_var:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
api_key = os.environ.get(env_var)
|
|
43
|
+
if api_key:
|
|
44
|
+
logger.debug(
|
|
45
|
+
f"Loaded API key for {provider_name} from environment variable {env_var}"
|
|
46
|
+
)
|
|
47
|
+
return api_key
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def inject_env_api_keys(provider_configs: Dict) -> Dict:
|
|
51
|
+
"""
|
|
52
|
+
Inject API keys from environment variables into provider configurations.
|
|
53
|
+
|
|
54
|
+
This function checks each provider configuration and if the API key is missing
|
|
55
|
+
or is a placeholder, it attempts to load it from the environment.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
provider_configs: Dictionary of provider configurations
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Updated provider configurations with environment API keys
|
|
62
|
+
"""
|
|
63
|
+
for provider_name, config in provider_configs.items():
|
|
64
|
+
# Skip if provider doesn't need API key
|
|
65
|
+
if provider_name in ["ollama", "claude-code"]:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# Check if API key is missing or is a placeholder
|
|
69
|
+
current_key = getattr(config, "api_key", None)
|
|
70
|
+
if (
|
|
71
|
+
not current_key
|
|
72
|
+
or current_key.startswith("your-")
|
|
73
|
+
or current_key.startswith("sk-your")
|
|
74
|
+
):
|
|
75
|
+
# Try to load from environment
|
|
76
|
+
env_key = load_api_key_from_env(provider_name)
|
|
77
|
+
if env_key:
|
|
78
|
+
config.api_key = env_key
|
|
79
|
+
logger.info(f"Injected API key for {provider_name} from environment")
|
|
80
|
+
else:
|
|
81
|
+
logger.debug(f"No environment API key found for {provider_name}")
|
|
82
|
+
|
|
83
|
+
return provider_configs
|