ngpt 2.3.0__py3-none-any.whl → 2.3.2__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.
ngpt/cli.py CHANGED
@@ -5,6 +5,246 @@ from .client import NGPTClient
5
5
  from .config import load_config, get_config_path, load_configs, add_config_entry, remove_config_entry
6
6
  from . import __version__
7
7
 
8
+ # ANSI color codes for terminal output
9
+ COLORS = {
10
+ "reset": "\033[0m",
11
+ "bold": "\033[1m",
12
+ "cyan": "\033[36m",
13
+ "green": "\033[32m",
14
+ "yellow": "\033[33m",
15
+ "blue": "\033[34m",
16
+ "magenta": "\033[35m",
17
+ "gray": "\033[90m",
18
+ "bg_blue": "\033[44m",
19
+ "bg_cyan": "\033[46m"
20
+ }
21
+
22
+ # Check if ANSI colors are supported
23
+ def supports_ansi_colors():
24
+ """Check if the current terminal supports ANSI colors."""
25
+ import os
26
+ import sys
27
+
28
+ # If not a TTY, probably redirected, so no color
29
+ if not sys.stdout.isatty():
30
+ return False
31
+
32
+ # Windows specific checks
33
+ if sys.platform == "win32":
34
+ try:
35
+ # Windows 10+ supports ANSI colors in cmd/PowerShell
36
+ import ctypes
37
+ kernel32 = ctypes.windll.kernel32
38
+
39
+ # Try to enable ANSI color support
40
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
41
+
42
+ # Check if TERM_PROGRAM is set (WSL/ConEmu/etc.)
43
+ if os.environ.get('TERM_PROGRAM') or os.environ.get('WT_SESSION'):
44
+ return True
45
+
46
+ # Check Windows version - 10+ supports ANSI natively
47
+ winver = sys.getwindowsversion()
48
+ if winver.major >= 10:
49
+ return True
50
+
51
+ return False
52
+ except Exception:
53
+ return False
54
+
55
+ # Most UNIX systems support ANSI colors
56
+ return True
57
+
58
+ # Initialize color support
59
+ HAS_COLOR = supports_ansi_colors()
60
+
61
+ # If we're on Windows, use brighter colors that work better in PowerShell
62
+ if sys.platform == "win32" and HAS_COLOR:
63
+ COLORS["magenta"] = "\033[95m" # Bright magenta for metavars
64
+ COLORS["cyan"] = "\033[96m" # Bright cyan for options
65
+
66
+ # If no color support, use empty color codes
67
+ if not HAS_COLOR:
68
+ for key in COLORS:
69
+ COLORS[key] = ""
70
+
71
+ # Custom help formatter with color support
72
+ class ColoredHelpFormatter(argparse.HelpFormatter):
73
+ """Help formatter that properly handles ANSI color codes without breaking alignment."""
74
+
75
+ def __init__(self, prog):
76
+ # Import modules needed for terminal size detection
77
+ import re
78
+ import textwrap
79
+ import shutil
80
+
81
+ # Get terminal size for dynamic width adjustment
82
+ try:
83
+ self.term_width = shutil.get_terminal_size().columns
84
+ except:
85
+ self.term_width = 80 # Default if we can't detect terminal width
86
+
87
+ # Calculate dynamic layout values based on terminal width
88
+ self.formatter_width = self.term_width - 2 # Leave some margin
89
+
90
+ # For very wide terminals, limit the width to maintain readability
91
+ if self.formatter_width > 120:
92
+ self.formatter_width = 120
93
+
94
+ # Calculate help position based on terminal width (roughly 1/3 of width)
95
+ self.help_position = min(max(20, int(self.term_width * 0.33)), 36)
96
+
97
+ # Initialize the parent class with dynamic values
98
+ super().__init__(prog, max_help_position=self.help_position, width=self.formatter_width)
99
+
100
+ # Calculate wrap width based on remaining space after help position
101
+ self.wrap_width = self.formatter_width - self.help_position - 5
102
+
103
+ # Set up the text wrapper for help text
104
+ self.ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
105
+ self.wrapper = textwrap.TextWrapper(width=self.wrap_width)
106
+
107
+ def _strip_ansi(self, s):
108
+ """Strip ANSI escape sequences for width calculations"""
109
+ return self.ansi_escape.sub('', s)
110
+
111
+ def _colorize(self, text, color, bold=False):
112
+ """Helper to consistently apply color with optional bold"""
113
+ if bold:
114
+ return f"{COLORS['bold']}{COLORS[color]}{text}{COLORS['reset']}"
115
+ return f"{COLORS[color]}{text}{COLORS['reset']}"
116
+
117
+ def _format_action_invocation(self, action):
118
+ if not action.option_strings:
119
+ # For positional arguments
120
+ metavar = self._format_args(action, action.dest.upper())
121
+ return self._colorize(metavar, 'cyan', bold=True)
122
+ else:
123
+ # For optional arguments with different color for metavar
124
+ if action.nargs != argparse.SUPPRESS:
125
+ default = self._get_default_metavar_for_optional(action)
126
+ args_string = self._format_args(action, default)
127
+
128
+ # Color option name and metavar differently
129
+ option_part = ', '.join(action.option_strings)
130
+ colored_option = self._colorize(option_part, 'cyan', bold=True)
131
+
132
+ if args_string:
133
+ # Make metavars more visible with brackets and color
134
+ # If HAS_COLOR is False, brackets will help in PowerShell
135
+ if not HAS_COLOR:
136
+ # Add brackets to make metavars stand out even without color
137
+ formatted_args = f"<{args_string}>"
138
+ else:
139
+ # Use color for metavar
140
+ formatted_args = self._colorize(args_string, 'magenta')
141
+
142
+ return f"{colored_option} {formatted_args}"
143
+ else:
144
+ return colored_option
145
+ else:
146
+ return self._colorize(', '.join(action.option_strings), 'cyan', bold=True)
147
+
148
+ def _format_usage(self, usage, actions, groups, prefix):
149
+ usage_text = super()._format_usage(usage, actions, groups, prefix)
150
+
151
+ # Replace "usage:" with colored version
152
+ colored_usage = self._colorize("usage:", 'green', bold=True)
153
+ usage_text = usage_text.replace("usage:", colored_usage)
154
+
155
+ # We won't color metavars in usage text as it breaks the formatting
156
+ # Just return with the colored usage prefix
157
+ return usage_text
158
+
159
+ def _join_parts(self, part_strings):
160
+ """Override to fix any potential formatting issues with section joins"""
161
+ return '\n'.join([part for part in part_strings if part])
162
+
163
+ def start_section(self, heading):
164
+ # Remove the colon as we'll add it with color
165
+ if heading.endswith(':'):
166
+ heading = heading[:-1]
167
+ heading_text = f"{self._colorize(heading, 'yellow', bold=True)}:"
168
+ super().start_section(heading_text)
169
+
170
+ def _get_help_string(self, action):
171
+ # Add color to help strings
172
+ help_text = action.help
173
+ if help_text:
174
+ return help_text.replace('(default:', f"{COLORS['gray']}(default:") + COLORS['reset']
175
+ return help_text
176
+
177
+ def _wrap_help_text(self, text, initial_indent="", subsequent_indent=" "):
178
+ """Wrap long help text to prevent overflow"""
179
+ if not text:
180
+ return text
181
+
182
+ # Strip ANSI codes for width calculation
183
+ clean_text = self._strip_ansi(text)
184
+
185
+ # If the text is already short enough, return it as is
186
+ if len(clean_text) <= self.wrap_width:
187
+ return text
188
+
189
+ # Handle any existing ANSI codes
190
+ has_ansi = text != clean_text
191
+ wrap_text = clean_text
192
+
193
+ # Wrap the text
194
+ lines = self.wrapper.wrap(wrap_text)
195
+
196
+ # Add indentation to all but the first line
197
+ wrapped = lines[0]
198
+ for line in lines[1:]:
199
+ wrapped += f"\n{subsequent_indent}{line}"
200
+
201
+ # Re-add the ANSI codes if they were present
202
+ if has_ansi and text.endswith(COLORS['reset']):
203
+ wrapped += COLORS['reset']
204
+
205
+ return wrapped
206
+
207
+ def _format_action(self, action):
208
+ # For subparsers, just return the regular formatting
209
+ if isinstance(action, argparse._SubParsersAction):
210
+ return super()._format_action(action)
211
+
212
+ # Get the action header with colored parts (both option names and metavars)
213
+ # The coloring is now done in _format_action_invocation
214
+ action_header = self._format_action_invocation(action)
215
+
216
+ # Format help text
217
+ help_text = self._expand_help(action)
218
+
219
+ # Get the raw lengths without ANSI codes for formatting
220
+ raw_header_len = len(self._strip_ansi(action_header))
221
+
222
+ # Calculate the indent for the help text
223
+ help_position = min(self._action_max_length + 2, self._max_help_position)
224
+ help_indent = ' ' * help_position
225
+
226
+ # If the action header is too long, put help on the next line
227
+ if raw_header_len > help_position:
228
+ # An action header that's too long gets a line break
229
+ # Wrap the help text with proper indentation
230
+ wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
231
+ line = f"{action_header}\n{help_indent}{wrapped_help}"
232
+ else:
233
+ # Standard formatting with proper spacing
234
+ padding = ' ' * (help_position - raw_header_len)
235
+ # Wrap the help text with proper indentation
236
+ wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
237
+ line = f"{action_header}{padding}{wrapped_help}"
238
+
239
+ # Handle subactions
240
+ if action.help is argparse.SUPPRESS:
241
+ return line
242
+
243
+ if not action.help:
244
+ return line
245
+
246
+ return line
247
+
8
248
  # Optional imports for enhanced UI
9
249
  try:
10
250
  from prompt_toolkit import prompt as pt_prompt
@@ -26,81 +266,67 @@ except ImportError:
26
266
 
27
267
  def show_config_help():
28
268
  """Display help information about configuration."""
29
- print("\nConfiguration Help:")
30
- print(" 1. Create a config file at one of these locations:")
269
+ print(f"\n{COLORS['green']}{COLORS['bold']}Configuration Help:{COLORS['reset']}")
270
+ print(f" 1. {COLORS['cyan']}Create a config file at one of these locations:{COLORS['reset']}")
31
271
  if sys.platform == "win32":
32
- print(f" - %APPDATA%\\ngpt\\ngpt.conf")
272
+ print(f" - {COLORS['yellow']}%APPDATA%\\ngpt\\ngpt.conf{COLORS['reset']}")
33
273
  elif sys.platform == "darwin":
34
- print(f" - ~/Library/Application Support/ngpt/ngpt.conf")
274
+ print(f" - {COLORS['yellow']}~/Library/Application Support/ngpt/ngpt.conf{COLORS['reset']}")
35
275
  else:
36
- print(f" - ~/.config/ngpt/ngpt.conf")
276
+ print(f" - {COLORS['yellow']}~/.config/ngpt/ngpt.conf{COLORS['reset']}")
37
277
 
38
- print(" 2. Format your config file as JSON:")
39
- print(""" [
40
- {
278
+ print(f" 2. {COLORS['cyan']}Format your config file as JSON:{COLORS['reset']}")
279
+ print(f"""{COLORS['yellow']} [
280
+ {{
41
281
  "api_key": "your-api-key-here",
42
282
  "base_url": "https://api.openai.com/v1/",
43
283
  "provider": "OpenAI",
44
284
  "model": "gpt-3.5-turbo"
45
- },
46
- {
285
+ }},
286
+ {{
47
287
  "api_key": "your-second-api-key",
48
288
  "base_url": "http://localhost:1337/v1/",
49
289
  "provider": "Another Provider",
50
290
  "model": "different-model"
51
- }
52
- ]""")
53
-
54
- print(" 3. Or set environment variables:")
55
- print(" - OPENAI_API_KEY")
56
- print(" - OPENAI_BASE_URL")
57
- print(" - OPENAI_MODEL")
58
-
59
- print(" 4. Or provide command line arguments:")
60
- print(" ngpt --api-key your-key --base-url https://api.example.com --model your-model \"Your prompt\"")
61
-
62
- print(" 5. Use --config-index to specify which configuration to use or edit:")
63
- print(" ngpt --config-index 1 \"Your prompt\"")
64
-
65
- print(" 6. Use --config without arguments to add a new configuration:")
66
- print(" ngpt --config")
67
- print(" Or specify an index to edit an existing configuration:")
68
- print(" ngpt --config --config-index 1")
69
- print(" 7. Remove a configuration at a specific index:")
70
- print(" ngpt --config --remove --config-index 1")
71
- print(" 8. List available models for the current configuration:")
72
- print(" ngpt --list-models")
291
+ }}
292
+ ]{COLORS['reset']}""")
293
+
294
+ print(f" 3. {COLORS['cyan']}Or set environment variables:{COLORS['reset']}")
295
+ print(f" - {COLORS['yellow']}OPENAI_API_KEY{COLORS['reset']}")
296
+ print(f" - {COLORS['yellow']}OPENAI_BASE_URL{COLORS['reset']}")
297
+ print(f" - {COLORS['yellow']}OPENAI_MODEL{COLORS['reset']}")
298
+
299
+ print(f" 4. {COLORS['cyan']}Or provide command line arguments:{COLORS['reset']}")
300
+ print(f" {COLORS['yellow']}ngpt --api-key your-key --base-url https://api.example.com --model your-model \"Your prompt\"{COLORS['reset']}")
301
+
302
+ print(f" 5. {COLORS['cyan']}Use --config-index to specify which configuration to use or edit:{COLORS['reset']}")
303
+ print(f" {COLORS['yellow']}ngpt --config-index 1 \"Your prompt\"{COLORS['reset']}")
304
+
305
+ print(f" 6. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
306
+ print(f" {COLORS['yellow']}ngpt --config{COLORS['reset']}")
307
+ print(f" Or specify an index to edit an existing configuration:")
308
+ print(f" {COLORS['yellow']}ngpt --config --config-index 1{COLORS['reset']}")
309
+ print(f" 7. {COLORS['cyan']}Remove a configuration at a specific index:{COLORS['reset']}")
310
+ print(f" {COLORS['yellow']}ngpt --config --remove --config-index 1{COLORS['reset']}")
311
+ print(f" 8. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
312
+ print(f" {COLORS['yellow']}ngpt --list-models{COLORS['reset']}")
73
313
 
74
314
  def check_config(config):
75
315
  """Check config for common issues and provide guidance."""
76
316
  if not config.get("api_key"):
77
- print("Error: API key is not set.")
317
+ print(f"{COLORS['yellow']}{COLORS['bold']}Error: API key is not set.{COLORS['reset']}")
78
318
  show_config_help()
79
319
  return False
80
320
 
81
321
  # Check for common URL mistakes
82
322
  base_url = config.get("base_url", "")
83
323
  if base_url and not (base_url.startswith("http://") or base_url.startswith("https://")):
84
- print(f"Warning: Base URL '{base_url}' doesn't start with http:// or https://")
324
+ print(f"{COLORS['yellow']}Warning: Base URL '{base_url}' doesn't start with http:// or https://{COLORS['reset']}")
85
325
 
86
326
  return True
87
327
 
88
328
  def interactive_chat_session(client, web_search=False, no_stream=False, temperature=0.7, top_p=1.0, max_length=None, log_file=None, preprompt=None):
89
329
  """Run an interactive chat session with conversation history."""
90
- # Define ANSI color codes for terminal output
91
- COLORS = {
92
- "reset": "\033[0m",
93
- "bold": "\033[1m",
94
- "cyan": "\033[36m",
95
- "green": "\033[32m",
96
- "yellow": "\033[33m",
97
- "blue": "\033[34m",
98
- "magenta": "\033[35m",
99
- "gray": "\033[90m",
100
- "bg_blue": "\033[44m",
101
- "bg_cyan": "\033[46m"
102
- }
103
-
104
330
  # Get terminal width for better formatting
105
331
  try:
106
332
  term_width = shutil.get_terminal_size().columns
@@ -300,10 +526,25 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
300
526
  log_handle.close()
301
527
 
302
528
  def main():
303
- parser = argparse.ArgumentParser(description="nGPT - A CLI tool for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints")
529
+ # Colorize description - use a shorter description to avoid line wrapping issues
530
+ description = f"{COLORS['cyan']}{COLORS['bold']}nGPT{COLORS['reset']} - Interact with AI language models via OpenAI-compatible APIs"
531
+ parser = argparse.ArgumentParser(description=description, formatter_class=ColoredHelpFormatter)
532
+
533
+ # Add custom error method with color
534
+ original_error = parser.error
535
+ def error_with_color(message):
536
+ parser.print_usage(sys.stderr)
537
+ parser.exit(2, f"{COLORS['bold']}{COLORS['yellow']}error: {COLORS['reset']}{message}\n")
538
+ parser.error = error_with_color
539
+
540
+ # Custom version action with color
541
+ class ColoredVersionAction(argparse.Action):
542
+ def __call__(self, parser, namespace, values, option_string=None):
543
+ print(f"{COLORS['green']}{COLORS['bold']}nGPT{COLORS['reset']} version {COLORS['yellow']}{__version__}{COLORS['reset']}")
544
+ parser.exit()
304
545
 
305
546
  # Version flag
306
- parser.add_argument('-v', '--version', action='version', version=f'nGPT {__version__}', help='Show version information and exit')
547
+ parser.add_argument('-v', '--version', action=ColoredVersionAction, nargs=0, help='Show version information and exit')
307
548
 
308
549
  # Config options
309
550
  config_group = parser.add_argument_group('Configuration Options')
@@ -582,9 +823,9 @@ def main():
582
823
  style="class:input-area",
583
824
  multiline=True,
584
825
  wrap_lines=True,
585
- width=term_width - 4,
586
- height=min(20, term_height - 8),
587
- prompt=HTML("<ansicyan>>>> </ansicyan>"),
826
+ width=term_width - 10,
827
+ height=min(15, term_height - 10),
828
+ prompt=HTML("<ansicyan><b>> </b></ansicyan>"),
588
829
  scrollbar=True,
589
830
  focus_on_click=True,
590
831
  lexer=None,
@@ -593,7 +834,7 @@ def main():
593
834
 
594
835
  # Create a title bar
595
836
  title_bar = FormattedTextControl(
596
- HTML("<style bg='ansicyan' fg='ansiblack'><b> NGPT Multi-line Editor </b></style>")
837
+ HTML("<ansicyan><b> nGPT Multi-line Editor </b></ansicyan>")
597
838
  )
598
839
 
599
840
  # Create a status bar with key bindings info
@@ -605,17 +846,17 @@ def main():
605
846
  layout = Layout(
606
847
  HSplit([
607
848
  Window(title_bar, height=1),
608
- Window(height=1, char="-", style="class:separator"),
849
+ Window(height=1, char="", style="class:separator"),
609
850
  text_area,
610
- Window(height=1, char="-", style="class:separator"),
851
+ Window(height=1, char="", style="class:separator"),
611
852
  Window(status_bar, height=1),
612
853
  ])
613
854
  )
614
855
 
615
856
  # Create a style
616
857
  style = Style.from_dict({
617
- "separator": "ansigray",
618
- "input-area": "bg:ansiblack fg:ansiwhite",
858
+ "separator": "ansicyan",
859
+ "input-area": "fg:ansiwhite",
619
860
  "cursor": "bg:ansiwhite fg:ansiblack",
620
861
  })
621
862
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ngpt
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: A lightweight Python CLI and library for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints.
5
5
  Project-URL: Homepage, https://github.com/nazdridoy/ngpt
6
6
  Project-URL: Repository, https://github.com/nazdridoy/ngpt
@@ -0,0 +1,9 @@
1
+ ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
2
+ ngpt/cli.py,sha256=Or59XajZRf1Gl4zExygLIeIbwsJTkT_YLK_23ViwW2k,43230
3
+ ngpt/client.py,sha256=75xmzO7e9wQ7y_LzZCacg3mkZdheewcBxB6moPftqYw,13067
4
+ ngpt/config.py,sha256=BF0G3QeiPma8l7EQyc37bR7LWZog7FHJQNe7uj9cr4w,6896
5
+ ngpt-2.3.2.dist-info/METADATA,sha256=PnZr050walUIcVAWQz_xVd_yEK8ZA7UlKHYZEX5k9hI,13535
6
+ ngpt-2.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ ngpt-2.3.2.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
8
+ ngpt-2.3.2.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
9
+ ngpt-2.3.2.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
2
- ngpt/cli.py,sha256=MfZ_QDKdylLvhPwUPRgCCBUD3iRDDh8Gy2Xnzf1MbbY,32511
3
- ngpt/client.py,sha256=75xmzO7e9wQ7y_LzZCacg3mkZdheewcBxB6moPftqYw,13067
4
- ngpt/config.py,sha256=BF0G3QeiPma8l7EQyc37bR7LWZog7FHJQNe7uj9cr4w,6896
5
- ngpt-2.3.0.dist-info/METADATA,sha256=TG0WxgRFDG8oWOE3dtJgqRoyrJUfWB-QlgLkEq34Sqg,13535
6
- ngpt-2.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- ngpt-2.3.0.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
8
- ngpt-2.3.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
9
- ngpt-2.3.0.dist-info/RECORD,,
File without changes