ngpt 2.9.0__py3-none-any.whl → 2.9.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/main.py ADDED
@@ -0,0 +1,593 @@
1
+ import argparse
2
+ import sys
3
+ import os
4
+ from ..client import NGPTClient
5
+ from ..config import load_config, get_config_path, load_configs, add_config_entry, remove_config_entry
6
+ from ..cli_config import (
7
+ set_cli_config_option,
8
+ get_cli_config_option,
9
+ unset_cli_config_option,
10
+ apply_cli_config,
11
+ list_cli_config_options,
12
+ CLI_CONFIG_OPTIONS,
13
+ load_cli_config
14
+ )
15
+ from .. import __version__
16
+
17
+ from .formatters import COLORS, ColoredHelpFormatter
18
+ from .renderers import has_markdown_renderer, warn_if_no_markdown_renderer, show_available_renderers
19
+ from .config_manager import show_config_help, check_config
20
+ from .interactive import interactive_chat_session
21
+ from .modes.chat import chat_mode
22
+ from .modes.code import code_mode
23
+ from .modes.shell import shell_mode
24
+ from .modes.text import text_mode
25
+
26
+ def show_cli_config_help():
27
+ """Display help information about CLI configuration."""
28
+ print(f"\n{COLORS['green']}{COLORS['bold']}CLI Configuration Help:{COLORS['reset']}")
29
+ print(f" {COLORS['cyan']}Command syntax:{COLORS['reset']}")
30
+ print(f" {COLORS['yellow']}ngpt --cli-config help{COLORS['reset']} - Show this help message")
31
+ print(f" {COLORS['yellow']}ngpt --cli-config set OPTION VALUE{COLORS['reset']} - Set a default value for OPTION")
32
+ print(f" {COLORS['yellow']}ngpt --cli-config get OPTION{COLORS['reset']} - Get the current value of OPTION")
33
+ print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all CLI configuration settings")
34
+ print(f" {COLORS['yellow']}ngpt --cli-config unset OPTION{COLORS['reset']} - Remove OPTION from configuration")
35
+ print(f" {COLORS['yellow']}ngpt --cli-config list{COLORS['reset']} - List all available options")
36
+
37
+ print(f"\n {COLORS['cyan']}Available options:{COLORS['reset']}")
38
+
39
+ # Group options by context
40
+ context_groups = {
41
+ "all": [],
42
+ "code": [],
43
+ "interactive": [],
44
+ "text": [],
45
+ "shell": []
46
+ }
47
+
48
+ for option, meta in CLI_CONFIG_OPTIONS.items():
49
+ for context in meta["context"]:
50
+ if context in context_groups:
51
+ if context == "all":
52
+ context_groups[context].append(option)
53
+ break
54
+ else:
55
+ context_groups[context].append(option)
56
+
57
+ # Print general options (available in all contexts)
58
+ print(f" {COLORS['yellow']}General options (all modes):{COLORS['reset']}")
59
+ for option in sorted(context_groups["all"]):
60
+ meta = CLI_CONFIG_OPTIONS[option]
61
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
62
+ exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
63
+ print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
64
+
65
+ # Print mode-specific options
66
+ for mode, options in [
67
+ ("code", "Code generation mode"),
68
+ ("interactive", "Interactive mode"),
69
+ ("text", "Text mode"),
70
+ ("shell", "Shell mode")
71
+ ]:
72
+ if context_groups[mode]:
73
+ print(f"\n {COLORS['yellow']}Options for {options}:{COLORS['reset']}")
74
+ for option in sorted(context_groups[mode]):
75
+ # Skip if already listed in general options
76
+ if option in context_groups["all"]:
77
+ continue
78
+ meta = CLI_CONFIG_OPTIONS[option]
79
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
80
+ exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
81
+ print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
82
+
83
+ print(f"\n {COLORS['cyan']}Example usage:{COLORS['reset']}")
84
+ print(f" {COLORS['yellow']}ngpt --cli-config set language java{COLORS['reset']} - Set default language to java for code generation")
85
+ print(f" {COLORS['yellow']}ngpt --cli-config set temperature 0.9{COLORS['reset']} - Set default temperature to 0.9")
86
+ print(f" {COLORS['yellow']}ngpt --cli-config set no-stream true{COLORS['reset']} - Disable streaming by default")
87
+ print(f" {COLORS['yellow']}ngpt --cli-config get temperature{COLORS['reset']} - Check the current temperature setting")
88
+ print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all current CLI settings")
89
+ print(f" {COLORS['yellow']}ngpt --cli-config unset language{COLORS['reset']} - Remove language setting")
90
+
91
+ print(f"\n {COLORS['cyan']}Notes:{COLORS['reset']}")
92
+ print(f" - CLI configuration is stored in:")
93
+ print(f" • Linux: {COLORS['yellow']}~/.config/ngpt/ngpt-cli.conf{COLORS['reset']}")
94
+ print(f" • macOS: {COLORS['yellow']}~/Library/Application Support/ngpt/ngpt-cli.conf{COLORS['reset']}")
95
+ print(f" • Windows: {COLORS['yellow']}%APPDATA%\\ngpt\\ngpt-cli.conf{COLORS['reset']}")
96
+ print(f" - Settings are applied based on context (e.g., language only applies to code generation mode)")
97
+ print(f" - Command-line arguments always override CLI configuration")
98
+ print(f" - Some options are mutually exclusive and will not be applied together")
99
+
100
+ def handle_cli_config(action, option=None, value=None):
101
+ """Handle CLI configuration commands."""
102
+ if action == "help":
103
+ show_cli_config_help()
104
+ return
105
+
106
+ if action == "list":
107
+ # List all available options
108
+ print(f"{COLORS['green']}{COLORS['bold']}Available CLI configuration options:{COLORS['reset']}")
109
+ for option in list_cli_config_options():
110
+ meta = CLI_CONFIG_OPTIONS[option]
111
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
112
+ contexts = ', '.join(meta['context'])
113
+ if "all" in meta['context']:
114
+ contexts = "all modes"
115
+ print(f" {COLORS['cyan']}{option}{COLORS['reset']} - {meta['type']} {default} - Available in: {contexts}")
116
+ return
117
+
118
+ if action == "get":
119
+ if option is None:
120
+ # Get all options
121
+ success, config = get_cli_config_option()
122
+ if success and config:
123
+ print(f"{COLORS['green']}{COLORS['bold']}Current CLI configuration:{COLORS['reset']}")
124
+ for opt, val in config.items():
125
+ if opt in CLI_CONFIG_OPTIONS:
126
+ print(f" {COLORS['cyan']}{opt}{COLORS['reset']} = {val}")
127
+ else:
128
+ print(f" {COLORS['yellow']}{opt}{COLORS['reset']} = {val} (unknown option)")
129
+ else:
130
+ print(f"{COLORS['yellow']}No CLI configuration set. Use 'ngpt --cli-config set OPTION VALUE' to set options.{COLORS['reset']}")
131
+ else:
132
+ # Get specific option
133
+ success, result = get_cli_config_option(option)
134
+ if success:
135
+ if result is None:
136
+ print(f"{COLORS['cyan']}{option}{COLORS['reset']} is not set (default: {CLI_CONFIG_OPTIONS.get(option, {}).get('default', 'N/A')})")
137
+ else:
138
+ print(f"{COLORS['cyan']}{option}{COLORS['reset']} = {result}")
139
+ else:
140
+ print(f"{COLORS['yellow']}{result}{COLORS['reset']}")
141
+ return
142
+
143
+ if action == "set":
144
+ if option is None or value is None:
145
+ print(f"{COLORS['yellow']}Error: Both OPTION and VALUE are required for 'set' command.{COLORS['reset']}")
146
+ print(f"Usage: ngpt --cli-config set OPTION VALUE")
147
+ return
148
+
149
+ success, message = set_cli_config_option(option, value)
150
+ if success:
151
+ print(f"{COLORS['green']}{message}{COLORS['reset']}")
152
+ else:
153
+ print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
154
+ return
155
+
156
+ if action == "unset":
157
+ if option is None:
158
+ print(f"{COLORS['yellow']}Error: OPTION is required for 'unset' command.{COLORS['reset']}")
159
+ print(f"Usage: ngpt --cli-config unset OPTION")
160
+ return
161
+
162
+ success, message = unset_cli_config_option(option)
163
+ if success:
164
+ print(f"{COLORS['green']}{message}{COLORS['reset']}")
165
+ else:
166
+ print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
167
+ return
168
+
169
+ # If we get here, the action is not recognized
170
+ print(f"{COLORS['yellow']}Error: Unknown action '{action}'. Use 'set', 'get', 'unset', or 'list'.{COLORS['reset']}")
171
+ show_cli_config_help()
172
+
173
+ def main():
174
+ # Colorize description - use a shorter description to avoid line wrapping issues
175
+ description = f"{COLORS['cyan']}{COLORS['bold']}nGPT{COLORS['reset']} - Interact with AI language models via OpenAI-compatible APIs"
176
+
177
+ # Minimalist, clean epilog design
178
+ epilog = f"\n{COLORS['yellow']}nGPT {COLORS['bold']}v{__version__}{COLORS['reset']} • {COLORS['green']}Docs: {COLORS['bold']}https://nazdridoy.github.io/ngpt/usage/cli_usage.html{COLORS['reset']}"
179
+
180
+ parser = argparse.ArgumentParser(description=description, formatter_class=ColoredHelpFormatter, epilog=epilog)
181
+
182
+ # Add custom error method with color
183
+ original_error = parser.error
184
+ def error_with_color(message):
185
+ parser.print_usage(sys.stderr)
186
+ parser.exit(2, f"{COLORS['bold']}{COLORS['yellow']}error: {COLORS['reset']}{message}\n")
187
+ parser.error = error_with_color
188
+
189
+ # Custom version action with color
190
+ class ColoredVersionAction(argparse.Action):
191
+ def __call__(self, parser, namespace, values, option_string=None):
192
+ print(f"{COLORS['green']}{COLORS['bold']}nGPT{COLORS['reset']} version {COLORS['yellow']}{__version__}{COLORS['reset']}")
193
+ parser.exit()
194
+
195
+ # Version flag
196
+ parser.add_argument('-v', '--version', action=ColoredVersionAction, nargs=0, help='Show version information and exit')
197
+
198
+ # Config options
199
+ config_group = parser.add_argument_group('Configuration Options')
200
+ config_group.add_argument('--config', nargs='?', const=True, help='Path to a custom config file or, if no value provided, enter interactive configuration mode to create a new config')
201
+ config_group.add_argument('--config-index', type=int, default=0, help='Index of the configuration to use or edit (default: 0)')
202
+ config_group.add_argument('--provider', help='Provider name to identify the configuration to use')
203
+ config_group.add_argument('--remove', action='store_true', help='Remove the configuration at the specified index (requires --config and --config-index)')
204
+ config_group.add_argument('--show-config', action='store_true', help='Show the current configuration(s) and exit')
205
+ config_group.add_argument('--all', action='store_true', help='Show details for all configurations (requires --show-config)')
206
+ config_group.add_argument('--list-models', action='store_true', help='List all available models for the current configuration and exit')
207
+ config_group.add_argument('--list-renderers', action='store_true', help='Show available markdown renderers for use with --prettify')
208
+
209
+ # Global options
210
+ global_group = parser.add_argument_group('Global Options')
211
+ global_group.add_argument('--api-key', help='API key for the service')
212
+ global_group.add_argument('--base-url', help='Base URL for the API')
213
+ global_group.add_argument('--model', help='Model to use')
214
+ global_group.add_argument('--web-search', action='store_true',
215
+ help='Enable web search capability (Note: Your API endpoint must support this feature)')
216
+ global_group.add_argument('-n', '--no-stream', action='store_true',
217
+ help='Return the whole response without streaming')
218
+ global_group.add_argument('--temperature', type=float, default=0.7,
219
+ help='Set temperature (controls randomness, default: 0.7)')
220
+ global_group.add_argument('--top_p', type=float, default=1.0,
221
+ help='Set top_p (controls diversity, default: 1.0)')
222
+ global_group.add_argument('--max_tokens', type=int,
223
+ help='Set max response length in tokens')
224
+ global_group.add_argument('--log', metavar='FILE',
225
+ help='Set filepath to log conversation to (For interactive modes)')
226
+ global_group.add_argument('--preprompt',
227
+ help='Set custom system prompt to control AI behavior')
228
+ global_group.add_argument('--prettify', action='store_const', const='auto',
229
+ help='Render markdown responses and code with syntax highlighting and formatting')
230
+ global_group.add_argument('--stream-prettify', action='store_true',
231
+ help='Enable streaming with markdown rendering (automatically uses Rich renderer)')
232
+ global_group.add_argument('--renderer', choices=['auto', 'rich', 'glow'], default='auto',
233
+ help='Select which markdown renderer to use with --prettify (auto, rich, or glow)')
234
+
235
+ # Mode flags (mutually exclusive)
236
+ mode_group = parser.add_argument_group('Modes (mutually exclusive)')
237
+ mode_exclusive_group = mode_group.add_mutually_exclusive_group()
238
+ mode_exclusive_group.add_argument('-i', '--interactive', action='store_true', help='Start an interactive chat session')
239
+ mode_exclusive_group.add_argument('-s', '--shell', action='store_true', help='Generate and execute shell commands')
240
+ mode_exclusive_group.add_argument('-c', '--code', action='store_true', help='Generate code')
241
+ mode_exclusive_group.add_argument('-t', '--text', action='store_true', help='Enter multi-line text input (submit with Ctrl+D)')
242
+ # Note: --show-config is handled separately and implicitly acts as a mode
243
+
244
+ # Language option for code mode
245
+ parser.add_argument('--language', default="python", help='Programming language to generate code in (for code mode)')
246
+
247
+ # Prompt argument
248
+ parser.add_argument('prompt', nargs='?', default=None, help='The prompt to send')
249
+
250
+ # Add CLI configuration command
251
+ config_group.add_argument('--cli-config', nargs='*', metavar='COMMAND',
252
+ help='Manage CLI configuration (set, get, unset, list)')
253
+
254
+ args = parser.parse_args()
255
+
256
+ # Handle CLI configuration command
257
+ if args.cli_config is not None:
258
+ # Show help if no arguments or "help" argument
259
+ if len(args.cli_config) == 0 or (len(args.cli_config) > 0 and args.cli_config[0].lower() == "help"):
260
+ show_cli_config_help()
261
+ return
262
+
263
+ action = args.cli_config[0].lower()
264
+ option = args.cli_config[1] if len(args.cli_config) > 1 else None
265
+ value = args.cli_config[2] if len(args.cli_config) > 2 else None
266
+
267
+ if action in ("set", "get", "unset", "list", "help"):
268
+ handle_cli_config(action, option, value)
269
+ return
270
+ else:
271
+ show_cli_config_help()
272
+ return
273
+
274
+ # Validate --all usage
275
+ if args.all and not args.show_config:
276
+ parser.error("--all can only be used with --show-config")
277
+
278
+ # Handle --renderers flag to show available markdown renderers
279
+ if args.list_renderers:
280
+ show_available_renderers()
281
+ return
282
+
283
+ # Load CLI configuration early
284
+ cli_config = load_cli_config()
285
+
286
+ # Priority order for config selection:
287
+ # 1. Command-line arguments (args.provider, args.config_index)
288
+ # 2. CLI configuration (cli_config["provider"], cli_config["config-index"])
289
+ # 3. Default values (None, 0)
290
+
291
+ # Get provider/config-index from CLI config if not specified in args
292
+ effective_provider = args.provider
293
+ effective_config_index = args.config_index
294
+
295
+ # Only apply CLI config for provider/config-index if not explicitly set on command line
296
+ if not effective_provider and 'provider' in cli_config and '--provider' not in sys.argv:
297
+ effective_provider = cli_config['provider']
298
+
299
+ if effective_config_index == 0 and 'config-index' in cli_config and '--config-index' not in sys.argv:
300
+ effective_config_index = cli_config['config-index']
301
+
302
+ # Check for mutual exclusivity between provider and config-index
303
+ if effective_config_index != 0 and effective_provider:
304
+ parser.error("--config-index and --provider cannot be used together")
305
+
306
+ # Handle interactive configuration mode
307
+ if args.config is True: # --config was used without a value
308
+ config_path = get_config_path()
309
+
310
+ # Handle configuration removal if --remove flag is present
311
+ if args.remove:
312
+ # Validate that config_index is explicitly provided
313
+ if '--config-index' not in sys.argv and not effective_provider:
314
+ parser.error("--remove requires explicitly specifying --config-index or --provider")
315
+
316
+ # Show config details before asking for confirmation
317
+ configs = load_configs(str(config_path))
318
+
319
+ # Determine the config index to remove
320
+ config_index = effective_config_index
321
+ if effective_provider:
322
+ # Find config index by provider name
323
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
324
+ if not matching_configs:
325
+ print(f"Error: No configuration found for provider '{effective_provider}'")
326
+ return
327
+ elif len(matching_configs) > 1:
328
+ print(f"Multiple configurations found for provider '{effective_provider}':")
329
+ for i, idx in enumerate(matching_configs):
330
+ print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
331
+
332
+ try:
333
+ choice = input("Choose a configuration to remove (or press Enter to cancel): ")
334
+ if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
335
+ config_index = matching_configs[int(choice)]
336
+ else:
337
+ print("Configuration removal cancelled.")
338
+ return
339
+ except (ValueError, IndexError, KeyboardInterrupt):
340
+ print("\nConfiguration removal cancelled.")
341
+ return
342
+ else:
343
+ config_index = matching_configs[0]
344
+
345
+ # Check if index is valid
346
+ if config_index < 0 or config_index >= len(configs):
347
+ print(f"Error: Configuration index {config_index} is out of range. Valid range: 0-{len(configs)-1}")
348
+ return
349
+
350
+ # Show the configuration that will be removed
351
+ config = configs[config_index]
352
+ print(f"Configuration to remove (index {config_index}):")
353
+ print(f" Provider: {config.get('provider', 'N/A')}")
354
+ print(f" Model: {config.get('model', 'N/A')}")
355
+ print(f" Base URL: {config.get('base_url', 'N/A')}")
356
+ print(f" API Key: {'[Set]' if config.get('api_key') else '[Not Set]'}")
357
+
358
+ # Ask for confirmation
359
+ try:
360
+ print("\nAre you sure you want to remove this configuration? [y/N] ", end='')
361
+ response = input().lower()
362
+ if response in ('y', 'yes'):
363
+ remove_config_entry(config_path, config_index)
364
+ else:
365
+ print("Configuration removal cancelled.")
366
+ except KeyboardInterrupt:
367
+ print("\nConfiguration removal cancelled by user.")
368
+
369
+ return
370
+
371
+ # Regular config addition/editing (existing code)
372
+ # If --config-index was not explicitly specified, create a new entry by passing None
373
+ # This will cause add_config_entry to create a new entry at the end of the list
374
+ # Otherwise, edit the existing config at the specified index
375
+ config_index = None
376
+
377
+ # Determine if we're editing an existing config or creating a new one
378
+ if effective_provider:
379
+ # Find config by provider name
380
+ configs = load_configs(str(config_path))
381
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
382
+
383
+ if not matching_configs:
384
+ print(f"No configuration found for provider '{effective_provider}'. Creating a new configuration.")
385
+ elif len(matching_configs) > 1:
386
+ print(f"Multiple configurations found for provider '{effective_provider}':")
387
+ for i, idx in enumerate(matching_configs):
388
+ print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
389
+
390
+ try:
391
+ choice = input("Choose a configuration to edit (or press Enter for the first one): ")
392
+ if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
393
+ config_index = matching_configs[int(choice)]
394
+ else:
395
+ config_index = matching_configs[0]
396
+ except (ValueError, IndexError, KeyboardInterrupt):
397
+ config_index = matching_configs[0]
398
+ else:
399
+ config_index = matching_configs[0]
400
+
401
+ print(f"Editing existing configuration at index {config_index}")
402
+ elif effective_config_index != 0 or '--config-index' in sys.argv:
403
+ # Check if the index is valid
404
+ configs = load_configs(str(config_path))
405
+ if effective_config_index >= 0 and effective_config_index < len(configs):
406
+ config_index = effective_config_index
407
+ print(f"Editing existing configuration at index {config_index}")
408
+ else:
409
+ print(f"Configuration index {effective_config_index} is out of range. Creating a new configuration.")
410
+ else:
411
+ # Creating a new config
412
+ configs = load_configs(str(config_path))
413
+ print(f"Creating new configuration at index {len(configs)}")
414
+
415
+ add_config_entry(config_path, config_index)
416
+ return
417
+
418
+ # Load configuration using the effective provider/config-index
419
+ active_config = load_config(args.config, effective_config_index, effective_provider)
420
+
421
+ # Command-line arguments override config settings for active config display
422
+ if args.api_key:
423
+ active_config["api_key"] = args.api_key
424
+ if args.base_url:
425
+ active_config["base_url"] = args.base_url
426
+ if args.model:
427
+ active_config["model"] = args.model
428
+
429
+ # Show config if requested
430
+ if args.show_config:
431
+ config_path = get_config_path(args.config)
432
+ configs = load_configs(args.config)
433
+
434
+ print(f"Configuration file: {config_path}")
435
+ print(f"Total configurations: {len(configs)}")
436
+
437
+ # Determine active configuration and display identifier
438
+ active_identifier = f"index {effective_config_index}"
439
+ if effective_provider:
440
+ active_identifier = f"provider '{effective_provider}'"
441
+ print(f"Active configuration: {active_identifier}")
442
+
443
+ if args.all:
444
+ # Show details for all configurations
445
+ print("\nAll configuration details:")
446
+ for i, cfg in enumerate(configs):
447
+ provider = cfg.get('provider', 'N/A')
448
+ active_str = '(Active)' if (
449
+ (effective_provider and provider.lower() == effective_provider.lower()) or
450
+ (not effective_provider and i == effective_config_index)
451
+ ) else ''
452
+ print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
453
+ print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
454
+ print(f" Base URL: {cfg.get('base_url', 'N/A')}")
455
+ print(f" Model: {cfg.get('model', 'N/A')}")
456
+ else:
457
+ # Show active config details and summary list
458
+ print("\nActive configuration details:")
459
+ print(f" Provider: {COLORS['green']}{active_config.get('provider', 'N/A')}{COLORS['reset']}")
460
+ print(f" API Key: {'[Set]' if active_config.get('api_key') else '[Not Set]'}")
461
+ print(f" Base URL: {active_config.get('base_url', 'N/A')}")
462
+ print(f" Model: {active_config.get('model', 'N/A')}")
463
+
464
+ if len(configs) > 1:
465
+ print("\nAvailable configurations:")
466
+ # Check for duplicate provider names for warning
467
+ provider_counts = {}
468
+ for cfg in configs:
469
+ provider = cfg.get('provider', 'N/A').lower()
470
+ provider_counts[provider] = provider_counts.get(provider, 0) + 1
471
+
472
+ for i, cfg in enumerate(configs):
473
+ provider = cfg.get('provider', 'N/A')
474
+ provider_display = provider
475
+ # Add warning for duplicate providers
476
+ if provider_counts.get(provider.lower(), 0) > 1:
477
+ provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
478
+
479
+ active_marker = "*" if (
480
+ (effective_provider and provider.lower() == effective_provider.lower()) or
481
+ (not effective_provider and i == effective_config_index)
482
+ ) else " "
483
+ print(f"[{i}]{active_marker} {COLORS['green']}{provider_display}{COLORS['reset']} - {cfg.get('model', 'N/A')} ({'[API Key Set]' if cfg.get('api_key') else '[API Key Not Set]'})")
484
+
485
+ # Show instruction for using --provider
486
+ print(f"\nTip: Use {COLORS['yellow']}--provider NAME{COLORS['reset']} to select a configuration by provider name.")
487
+
488
+ return
489
+
490
+ # For interactive mode, we'll allow continuing without a specific prompt
491
+ if not args.prompt and not (args.shell or args.code or args.text or args.interactive or args.show_config or args.list_models):
492
+ parser.print_help()
493
+ return
494
+
495
+ # Check configuration (using the potentially overridden active_config)
496
+ if not args.show_config and not args.list_models and not check_config(active_config):
497
+ return
498
+
499
+ # Check if --prettify is used but no markdown renderer is available
500
+ # This will warn the user immediately if they request prettify but don't have the tools
501
+ has_renderer = True
502
+ if args.prettify:
503
+ has_renderer = warn_if_no_markdown_renderer(args.renderer)
504
+ if not has_renderer:
505
+ # Set a flag to disable prettify since we already warned the user
506
+ print(f"{COLORS['yellow']}Continuing without markdown rendering.{COLORS['reset']}")
507
+ show_available_renderers()
508
+ args.prettify = False
509
+
510
+ # Check if --prettify is used with --stream-prettify (conflict)
511
+ if args.prettify and args.stream_prettify:
512
+ parser.error("--prettify and --stream-prettify cannot be used together. Choose one option.")
513
+
514
+ # Check if --stream-prettify is used but Rich is not available
515
+ if args.stream_prettify and not has_markdown_renderer('rich'):
516
+ parser.error("--stream-prettify requires Rich to be installed. Install with: pip install \"ngpt[full]\" or pip install rich")
517
+
518
+ # Initialize client using the potentially overridden active_config
519
+ client = NGPTClient(**active_config)
520
+
521
+ try:
522
+ # Handle listing models
523
+ if args.list_models:
524
+ print("Retrieving available models...")
525
+ models = client.list_models()
526
+ if models:
527
+ print(f"\nAvailable models for {active_config.get('provider', 'API')}:")
528
+ print("-" * 50)
529
+ for model in models:
530
+ if "id" in model:
531
+ owned_by = f" ({model.get('owned_by', 'Unknown')})" if "owned_by" in model else ""
532
+ current = " [active]" if model["id"] == active_config["model"] else ""
533
+ print(f"- {model['id']}{owned_by}{current}")
534
+ print("\nUse --model MODEL_NAME to select a specific model")
535
+ else:
536
+ print("No models available or could not retrieve models.")
537
+ return
538
+
539
+ # Handle modes
540
+ if args.interactive:
541
+ # Apply CLI config for interactive mode
542
+ args = apply_cli_config(args, "interactive")
543
+
544
+ # Interactive chat mode
545
+ interactive_chat_session(
546
+ client,
547
+ web_search=args.web_search,
548
+ no_stream=args.no_stream,
549
+ temperature=args.temperature,
550
+ top_p=args.top_p,
551
+ max_tokens=args.max_tokens,
552
+ log_file=args.log,
553
+ preprompt=args.preprompt,
554
+ prettify=args.prettify,
555
+ renderer=args.renderer,
556
+ stream_prettify=args.stream_prettify
557
+ )
558
+ elif args.shell:
559
+ # Apply CLI config for shell mode
560
+ args = apply_cli_config(args, "shell")
561
+
562
+ # Shell command generation mode
563
+ shell_mode(client, args)
564
+
565
+ elif args.code:
566
+ # Apply CLI config for code mode
567
+ args = apply_cli_config(args, "code")
568
+
569
+ # Code generation mode
570
+ code_mode(client, args)
571
+
572
+ elif args.text:
573
+ # Apply CLI config for text mode
574
+ args = apply_cli_config(args, "text")
575
+
576
+ # Text mode (multiline input)
577
+ text_mode(client, args)
578
+
579
+ else:
580
+ # Default to chat mode
581
+ # Apply CLI config for default chat mode
582
+ args = apply_cli_config(args, "all")
583
+
584
+ # Standard chat mode
585
+ chat_mode(client, args)
586
+
587
+ except KeyboardInterrupt:
588
+ print("\nOperation cancelled by user. Exiting gracefully.")
589
+ # Make sure we exit with a non-zero status code to indicate the operation was cancelled
590
+ sys.exit(130) # 130 is the standard exit code for SIGINT (Ctrl+C)
591
+ except Exception as e:
592
+ print(f"Error: {e}")
593
+ sys.exit(1) # Exit with error code
@@ -0,0 +1,6 @@
1
+ from .chat import chat_mode
2
+ from .code import code_mode
3
+ from .shell import shell_mode
4
+ from .text import text_mode
5
+
6
+ __all__ = ['chat_mode', 'code_mode', 'shell_mode', 'text_mode']
ngpt/cli/modes/chat.py ADDED
@@ -0,0 +1,72 @@
1
+ from ..formatters import COLORS
2
+ from ..renderers import prettify_markdown, prettify_streaming_markdown
3
+ import sys
4
+
5
+ def chat_mode(client, args):
6
+ """Handle the standard chat mode with a single prompt.
7
+
8
+ Args:
9
+ client: The NGPTClient instance
10
+ args: The parsed command-line arguments
11
+ """
12
+ # Get the prompt
13
+ if args.prompt is None:
14
+ try:
15
+ print("Enter your prompt: ", end='')
16
+ prompt = input()
17
+ except KeyboardInterrupt:
18
+ print("\nInput cancelled by user. Exiting gracefully.")
19
+ sys.exit(130)
20
+ else:
21
+ prompt = args.prompt
22
+
23
+ # Create messages array with preprompt if available
24
+ messages = None
25
+ if args.preprompt:
26
+ messages = [
27
+ {"role": "system", "content": args.preprompt},
28
+ {"role": "user", "content": prompt}
29
+ ]
30
+
31
+ # Set default streaming behavior based on --no-stream and --prettify arguments
32
+ should_stream = not args.no_stream and not args.prettify
33
+
34
+ # If stream-prettify is enabled
35
+ stream_callback = None
36
+ live_display = None
37
+
38
+ if args.stream_prettify:
39
+ should_stream = True # Enable streaming
40
+ # This is the standard mode, not interactive
41
+ live_display, stream_callback = prettify_streaming_markdown(args.renderer)
42
+ if not live_display:
43
+ # Fallback to normal prettify if live display setup failed
44
+ args.prettify = True
45
+ args.stream_prettify = False
46
+ should_stream = False
47
+ print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
48
+
49
+ # If regular prettify is enabled with streaming, inform the user
50
+ if args.prettify and not args.no_stream:
51
+ print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
52
+
53
+ # Start live display if using stream-prettify
54
+ if args.stream_prettify and live_display:
55
+ live_display.start()
56
+
57
+ response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
58
+ temperature=args.temperature, top_p=args.top_p,
59
+ max_tokens=args.max_tokens, messages=messages,
60
+ markdown_format=args.prettify or args.stream_prettify,
61
+ stream_callback=stream_callback)
62
+
63
+ # Stop live display if using stream-prettify
64
+ if args.stream_prettify and live_display:
65
+ live_display.stop()
66
+
67
+ # Handle non-stream response or regular prettify
68
+ if (args.no_stream or args.prettify) and response:
69
+ if args.prettify:
70
+ prettify_markdown(response, args.renderer)
71
+ else:
72
+ print(response)