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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. 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