ngpt 2.7.2__py3-none-any.whl → 2.8.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.
ngpt/__init__.py CHANGED
@@ -3,8 +3,19 @@ __version__ = get_version("ngpt")
3
3
 
4
4
  from .client import NGPTClient
5
5
  from .config import load_config, get_config_path, get_config_dir
6
+ from .cli_config import (
7
+ load_cli_config,
8
+ set_cli_config_option,
9
+ get_cli_config_option,
10
+ unset_cli_config_option,
11
+ apply_cli_config
12
+ )
6
13
 
7
- __all__ = ["NGPTClient", "__version__", "load_config", "get_config_path", "get_config_dir"]
14
+ __all__ = [
15
+ "NGPTClient", "__version__", "load_config", "get_config_path", "get_config_dir",
16
+ "load_cli_config", "set_cli_config_option", "get_cli_config_option",
17
+ "unset_cli_config_option", "apply_cli_config"
18
+ ]
8
19
 
9
20
  # Import cli last to avoid circular imports
10
21
  from .cli import main
ngpt/cli.py CHANGED
@@ -3,6 +3,14 @@ import sys
3
3
  import os
4
4
  from .client import NGPTClient
5
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
+ )
6
14
  from . import __version__
7
15
 
8
16
  # Try to import markdown rendering libraries
@@ -844,6 +852,143 @@ def prettify_streaming_markdown(renderer='rich', is_interactive=False, header_te
844
852
  print(f"{COLORS['yellow']}Error setting up Rich streaming display: {str(e)}{COLORS['reset']}")
845
853
  return None, None
846
854
 
855
+ def show_cli_config_help():
856
+ """Display help information about CLI configuration."""
857
+ print(f"\n{COLORS['green']}{COLORS['bold']}CLI Configuration Help:{COLORS['reset']}")
858
+ print(f" {COLORS['cyan']}Command syntax:{COLORS['reset']}")
859
+ print(f" {COLORS['yellow']}ngpt --cli-config set OPTION VALUE{COLORS['reset']} - Set a default value for OPTION")
860
+ print(f" {COLORS['yellow']}ngpt --cli-config get OPTION{COLORS['reset']} - Get the current value of OPTION")
861
+ print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all CLI configuration settings")
862
+ print(f" {COLORS['yellow']}ngpt --cli-config unset OPTION{COLORS['reset']} - Remove OPTION from configuration")
863
+ print(f" {COLORS['yellow']}ngpt --cli-config list{COLORS['reset']} - List all available options")
864
+
865
+ print(f"\n {COLORS['cyan']}Available options:{COLORS['reset']}")
866
+
867
+ # Group options by context
868
+ context_groups = {
869
+ "all": [],
870
+ "code": [],
871
+ "interactive": [],
872
+ "text": [],
873
+ "shell": []
874
+ }
875
+
876
+ for option, meta in CLI_CONFIG_OPTIONS.items():
877
+ for context in meta["context"]:
878
+ if context in context_groups:
879
+ if context == "all":
880
+ context_groups[context].append(option)
881
+ break
882
+ else:
883
+ context_groups[context].append(option)
884
+
885
+ # Print general options (available in all contexts)
886
+ print(f" {COLORS['yellow']}General options (all modes):{COLORS['reset']}")
887
+ for option in sorted(context_groups["all"]):
888
+ meta = CLI_CONFIG_OPTIONS[option]
889
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
890
+ exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
891
+ print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
892
+
893
+ # Print mode-specific options
894
+ for mode, options in [
895
+ ("code", "Code generation mode"),
896
+ ("interactive", "Interactive mode"),
897
+ ("text", "Text mode"),
898
+ ("shell", "Shell mode")
899
+ ]:
900
+ if context_groups[mode]:
901
+ print(f"\n {COLORS['yellow']}Options for {options}:{COLORS['reset']}")
902
+ for option in sorted(context_groups[mode]):
903
+ # Skip if already listed in general options
904
+ if option in context_groups["all"]:
905
+ continue
906
+ meta = CLI_CONFIG_OPTIONS[option]
907
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
908
+ exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
909
+ print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
910
+
911
+ print(f"\n {COLORS['cyan']}Example usage:{COLORS['reset']}")
912
+ print(f" {COLORS['yellow']}ngpt --cli-config set language java{COLORS['reset']} - Set default language to java for code generation")
913
+ print(f" {COLORS['yellow']}ngpt --cli-config set temperature 0.9{COLORS['reset']} - Set default temperature to 0.9")
914
+ print(f" {COLORS['yellow']}ngpt --cli-config set no-stream true{COLORS['reset']} - Disable streaming by default")
915
+ print(f" {COLORS['yellow']}ngpt --cli-config unset language{COLORS['reset']} - Remove language setting")
916
+
917
+ print(f"\n {COLORS['cyan']}Notes:{COLORS['reset']}")
918
+ print(f" - CLI configuration is stored in {COLORS['yellow']}~/.config/ngpt/ngpt-cli.conf{COLORS['reset']} (or equivalent for your OS)")
919
+ print(f" - Settings are applied based on context (e.g., language only applies to code generation mode)")
920
+ print(f" - Command-line arguments always override CLI configuration")
921
+ print(f" - Some options are mutually exclusive and will not be applied together")
922
+
923
+ def handle_cli_config(action, option=None, value=None):
924
+ """Handle CLI configuration commands."""
925
+ if action == "list":
926
+ # List all available options
927
+ print(f"{COLORS['green']}{COLORS['bold']}Available CLI configuration options:{COLORS['reset']}")
928
+ for option in list_cli_config_options():
929
+ meta = CLI_CONFIG_OPTIONS[option]
930
+ default = f"(default: {meta['default']})" if meta['default'] is not None else ""
931
+ contexts = ', '.join(meta['context'])
932
+ if "all" in meta['context']:
933
+ contexts = "all modes"
934
+ print(f" {COLORS['cyan']}{option}{COLORS['reset']} - {meta['type']} {default} - Available in: {contexts}")
935
+ return
936
+
937
+ if action == "get":
938
+ if option is None:
939
+ # Get all options
940
+ success, config = get_cli_config_option()
941
+ if success and config:
942
+ print(f"{COLORS['green']}{COLORS['bold']}Current CLI configuration:{COLORS['reset']}")
943
+ for opt, val in config.items():
944
+ if opt in CLI_CONFIG_OPTIONS:
945
+ print(f" {COLORS['cyan']}{opt}{COLORS['reset']} = {val}")
946
+ else:
947
+ print(f" {COLORS['yellow']}{opt}{COLORS['reset']} = {val} (unknown option)")
948
+ else:
949
+ print(f"{COLORS['yellow']}No CLI configuration set. Use 'ngpt --cli-config set OPTION VALUE' to set options.{COLORS['reset']}")
950
+ else:
951
+ # Get specific option
952
+ success, result = get_cli_config_option(option)
953
+ if success:
954
+ if result is None:
955
+ print(f"{COLORS['cyan']}{option}{COLORS['reset']} is not set (default: {CLI_CONFIG_OPTIONS.get(option, {}).get('default', 'N/A')})")
956
+ else:
957
+ print(f"{COLORS['cyan']}{option}{COLORS['reset']} = {result}")
958
+ else:
959
+ print(f"{COLORS['yellow']}{result}{COLORS['reset']}")
960
+ return
961
+
962
+ if action == "set":
963
+ if option is None or value is None:
964
+ print(f"{COLORS['yellow']}Error: Both OPTION and VALUE are required for 'set' command.{COLORS['reset']}")
965
+ print(f"Usage: ngpt --cli-config set OPTION VALUE")
966
+ return
967
+
968
+ success, message = set_cli_config_option(option, value)
969
+ if success:
970
+ print(f"{COLORS['green']}{message}{COLORS['reset']}")
971
+ else:
972
+ print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
973
+ return
974
+
975
+ if action == "unset":
976
+ if option is None:
977
+ print(f"{COLORS['yellow']}Error: OPTION is required for 'unset' command.{COLORS['reset']}")
978
+ print(f"Usage: ngpt --cli-config unset OPTION")
979
+ return
980
+
981
+ success, message = unset_cli_config_option(option)
982
+ if success:
983
+ print(f"{COLORS['green']}{message}{COLORS['reset']}")
984
+ else:
985
+ print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
986
+ return
987
+
988
+ # If we get here, the action is not recognized
989
+ print(f"{COLORS['yellow']}Error: Unknown action '{action}'. Use 'set', 'get', 'unset', or 'list'.{COLORS['reset']}")
990
+ show_cli_config_help()
991
+
847
992
  def main():
848
993
  # Colorize description - use a shorter description to avoid line wrapping issues
849
994
  description = f"{COLORS['cyan']}{COLORS['bold']}nGPT{COLORS['reset']} - Interact with AI language models via OpenAI-compatible APIs"
@@ -921,8 +1066,30 @@ def main():
921
1066
  # Prompt argument
922
1067
  parser.add_argument('prompt', nargs='?', default=None, help='The prompt to send')
923
1068
 
1069
+ # Add CLI configuration command
1070
+ config_group.add_argument('--cli-config', nargs='*', metavar='COMMAND',
1071
+ help='Manage CLI configuration (set, get, unset, list)')
1072
+
924
1073
  args = parser.parse_args()
925
1074
 
1075
+ # Handle CLI configuration command
1076
+ if args.cli_config is not None:
1077
+ # Show help if no arguments or "help" argument
1078
+ if len(args.cli_config) == 0 or (len(args.cli_config) > 0 and args.cli_config[0].lower() == "help"):
1079
+ show_cli_config_help()
1080
+ return
1081
+
1082
+ action = args.cli_config[0].lower()
1083
+ option = args.cli_config[1] if len(args.cli_config) > 1 else None
1084
+ value = args.cli_config[2] if len(args.cli_config) > 2 else None
1085
+
1086
+ if action in ("set", "get", "unset", "list"):
1087
+ handle_cli_config(action, option, value)
1088
+ return
1089
+ else:
1090
+ show_cli_config_help()
1091
+ return
1092
+
926
1093
  # Validate --all usage
927
1094
  if args.all and not args.show_config:
928
1095
  parser.error("--all can only be used with --show-config")
@@ -932,8 +1099,28 @@ def main():
932
1099
  show_available_renderers()
933
1100
  return
934
1101
 
935
- # Check for mutual exclusivity between --config-index and --provider
936
- if args.config_index != 0 and args.provider:
1102
+ # Load CLI configuration early
1103
+ from .cli_config import load_cli_config
1104
+ cli_config = load_cli_config()
1105
+
1106
+ # Priority order for config selection:
1107
+ # 1. Command-line arguments (args.provider, args.config_index)
1108
+ # 2. CLI configuration (cli_config["provider"], cli_config["config-index"])
1109
+ # 3. Default values (None, 0)
1110
+
1111
+ # Get provider/config-index from CLI config if not specified in args
1112
+ effective_provider = args.provider
1113
+ effective_config_index = args.config_index
1114
+
1115
+ # Only apply CLI config for provider/config-index if not explicitly set on command line
1116
+ if not effective_provider and 'provider' in cli_config and '--provider' not in sys.argv:
1117
+ effective_provider = cli_config['provider']
1118
+
1119
+ if effective_config_index == 0 and 'config-index' in cli_config and '--config-index' not in sys.argv:
1120
+ effective_config_index = cli_config['config-index']
1121
+
1122
+ # Check for mutual exclusivity between provider and config-index
1123
+ if effective_config_index != 0 and effective_provider:
937
1124
  parser.error("--config-index and --provider cannot be used together")
938
1125
 
939
1126
  # Handle interactive configuration mode
@@ -943,22 +1130,22 @@ def main():
943
1130
  # Handle configuration removal if --remove flag is present
944
1131
  if args.remove:
945
1132
  # Validate that config_index is explicitly provided
946
- if '--config-index' not in sys.argv and not args.provider:
1133
+ if '--config-index' not in sys.argv and not effective_provider:
947
1134
  parser.error("--remove requires explicitly specifying --config-index or --provider")
948
1135
 
949
1136
  # Show config details before asking for confirmation
950
1137
  configs = load_configs(str(config_path))
951
1138
 
952
1139
  # Determine the config index to remove
953
- config_index = args.config_index
954
- if args.provider:
1140
+ config_index = effective_config_index
1141
+ if effective_provider:
955
1142
  # Find config index by provider name
956
- matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
1143
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
957
1144
  if not matching_configs:
958
- print(f"Error: No configuration found for provider '{args.provider}'")
1145
+ print(f"Error: No configuration found for provider '{effective_provider}'")
959
1146
  return
960
1147
  elif len(matching_configs) > 1:
961
- print(f"Multiple configurations found for provider '{args.provider}':")
1148
+ print(f"Multiple configurations found for provider '{effective_provider}':")
962
1149
  for i, idx in enumerate(matching_configs):
963
1150
  print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
964
1151
 
@@ -1008,15 +1195,15 @@ def main():
1008
1195
  config_index = None
1009
1196
 
1010
1197
  # Determine if we're editing an existing config or creating a new one
1011
- if args.provider:
1198
+ if effective_provider:
1012
1199
  # Find config by provider name
1013
1200
  configs = load_configs(str(config_path))
1014
- matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
1201
+ matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
1015
1202
 
1016
1203
  if not matching_configs:
1017
- print(f"No configuration found for provider '{args.provider}'. Creating a new configuration.")
1204
+ print(f"No configuration found for provider '{effective_provider}'. Creating a new configuration.")
1018
1205
  elif len(matching_configs) > 1:
1019
- print(f"Multiple configurations found for provider '{args.provider}':")
1206
+ print(f"Multiple configurations found for provider '{effective_provider}':")
1020
1207
  for i, idx in enumerate(matching_configs):
1021
1208
  print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
1022
1209
 
@@ -1032,14 +1219,14 @@ def main():
1032
1219
  config_index = matching_configs[0]
1033
1220
 
1034
1221
  print(f"Editing existing configuration at index {config_index}")
1035
- elif args.config_index != 0 or '--config-index' in sys.argv:
1222
+ elif effective_config_index != 0 or '--config-index' in sys.argv:
1036
1223
  # Check if the index is valid
1037
1224
  configs = load_configs(str(config_path))
1038
- if args.config_index >= 0 and args.config_index < len(configs):
1039
- config_index = args.config_index
1225
+ if effective_config_index >= 0 and effective_config_index < len(configs):
1226
+ config_index = effective_config_index
1040
1227
  print(f"Editing existing configuration at index {config_index}")
1041
1228
  else:
1042
- print(f"Configuration index {args.config_index} is out of range. Creating a new configuration.")
1229
+ print(f"Configuration index {effective_config_index} is out of range. Creating a new configuration.")
1043
1230
  else:
1044
1231
  # Creating a new config
1045
1232
  configs = load_configs(str(config_path))
@@ -1048,12 +1235,10 @@ def main():
1048
1235
  add_config_entry(config_path, config_index)
1049
1236
  return
1050
1237
 
1051
- # Load configuration using the specified index or provider (needed for active config display)
1052
- active_config = load_config(args.config, args.config_index, args.provider)
1238
+ # Load configuration using the effective provider/config-index
1239
+ active_config = load_config(args.config, effective_config_index, effective_provider)
1053
1240
 
1054
1241
  # Command-line arguments override config settings for active config display
1055
- # This part is kept to ensure the active config display reflects potential overrides,
1056
- # even though the overrides don't affect the stored configurations displayed with --all.
1057
1242
  if args.api_key:
1058
1243
  active_config["api_key"] = args.api_key
1059
1244
  if args.base_url:
@@ -1070,9 +1255,9 @@ def main():
1070
1255
  print(f"Total configurations: {len(configs)}")
1071
1256
 
1072
1257
  # Determine active configuration and display identifier
1073
- active_identifier = f"index {args.config_index}"
1074
- if args.provider:
1075
- active_identifier = f"provider '{args.provider}'"
1258
+ active_identifier = f"index {effective_config_index}"
1259
+ if effective_provider:
1260
+ active_identifier = f"provider '{effective_provider}'"
1076
1261
  print(f"Active configuration: {active_identifier}")
1077
1262
 
1078
1263
  if args.all:
@@ -1081,8 +1266,8 @@ def main():
1081
1266
  for i, cfg in enumerate(configs):
1082
1267
  provider = cfg.get('provider', 'N/A')
1083
1268
  active_str = '(Active)' if (
1084
- (args.provider and provider.lower() == args.provider.lower()) or
1085
- (not args.provider and i == args.config_index)
1269
+ (effective_provider and provider.lower() == effective_provider.lower()) or
1270
+ (not effective_provider and i == effective_config_index)
1086
1271
  ) else ''
1087
1272
  print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
1088
1273
  print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
@@ -1112,8 +1297,8 @@ def main():
1112
1297
  provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
1113
1298
 
1114
1299
  active_marker = "*" if (
1115
- (args.provider and provider.lower() == args.provider.lower()) or
1116
- (not args.provider and i == args.config_index)
1300
+ (effective_provider and provider.lower() == effective_provider.lower()) or
1301
+ (not effective_provider and i == effective_config_index)
1117
1302
  ) else " "
1118
1303
  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]'})")
1119
1304
 
@@ -1173,6 +1358,9 @@ def main():
1173
1358
 
1174
1359
  # Handle modes
1175
1360
  if args.interactive:
1361
+ # Apply CLI config for interactive mode
1362
+ args = apply_cli_config(args, "interactive")
1363
+
1176
1364
  # Interactive chat mode
1177
1365
  interactive_chat_session(
1178
1366
  client,
@@ -1188,6 +1376,9 @@ def main():
1188
1376
  stream_prettify=args.stream_prettify
1189
1377
  )
1190
1378
  elif args.shell:
1379
+ # Apply CLI config for shell mode
1380
+ args = apply_cli_config(args, "shell")
1381
+
1191
1382
  if args.prompt is None:
1192
1383
  try:
1193
1384
  print("Enter shell command description: ", end='')
@@ -1226,6 +1417,9 @@ def main():
1226
1417
  print(f"\nError:\n{e.stderr}")
1227
1418
 
1228
1419
  elif args.code:
1420
+ # Apply CLI config for code mode
1421
+ args = apply_cli_config(args, "code")
1422
+
1229
1423
  if args.prompt is None:
1230
1424
  try:
1231
1425
  print("Enter code description: ", end='')
@@ -1236,30 +1430,55 @@ def main():
1236
1430
  else:
1237
1431
  prompt = args.prompt
1238
1432
 
1239
- # Setup for stream-prettify with code generation
1433
+ # Setup for streaming and prettify logic
1240
1434
  stream_callback = None
1241
1435
  live_display = None
1242
- should_stream = False
1243
-
1436
+ should_stream = True # Default to streaming
1437
+ use_stream_prettify = False
1438
+ use_regular_prettify = False
1439
+
1440
+ # Determine final behavior based on flag priority
1244
1441
  if args.stream_prettify:
1245
- should_stream = True # Enable streaming
1246
- # This is the code generation mode, not interactive
1247
- live_display, stream_callback = prettify_streaming_markdown(args.renderer)
1248
- if not live_display:
1249
- # Fallback to normal prettify if live display setup failed
1250
- args.prettify = True
1251
- args.stream_prettify = False
1442
+ # Highest priority: stream-prettify
1443
+ if has_markdown_renderer('rich'):
1444
+ should_stream = True
1445
+ use_stream_prettify = True
1446
+ live_display, stream_callback = prettify_streaming_markdown(args.renderer)
1447
+ if not live_display:
1448
+ # Fallback if live display fails
1449
+ use_stream_prettify = False
1450
+ use_regular_prettify = True
1451
+ should_stream = False
1452
+ print(f"{COLORS['yellow']}Live display setup failed. Falling back to regular prettify mode.{COLORS['reset']}")
1453
+ else:
1454
+ # Rich not available for stream-prettify
1455
+ print(f"{COLORS['yellow']}Warning: Rich is not available for --stream-prettify. Install with: pip install \"ngpt[full]\".{COLORS['reset']}")
1456
+ print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
1457
+ should_stream = True
1458
+ use_stream_prettify = False
1459
+ elif args.no_stream:
1460
+ # Second priority: no-stream
1461
+ should_stream = False
1462
+ use_regular_prettify = False # No prettify if no streaming
1463
+ elif args.prettify:
1464
+ # Third priority: prettify (requires disabling stream)
1465
+ if has_markdown_renderer(args.renderer):
1252
1466
  should_stream = False
1253
- print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
1254
-
1255
- # If regular prettify is enabled with streaming, inform the user
1256
- if args.prettify and not args.no_stream:
1257
- print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
1467
+ use_regular_prettify = True
1468
+ print(f"{COLORS['yellow']}Note: Streaming disabled to enable regular markdown rendering (--prettify).{COLORS['reset']}")
1469
+ else:
1470
+ # Renderer not available for prettify
1471
+ print(f"{COLORS['yellow']}Warning: Renderer '{args.renderer}' not available for --prettify.{COLORS['reset']}")
1472
+ show_available_renderers()
1473
+ print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
1474
+ should_stream = True
1475
+ use_regular_prettify = False
1476
+ # else: Default is should_stream = True
1258
1477
 
1259
1478
  print("\nGenerating code...")
1260
1479
 
1261
1480
  # Start live display if using stream-prettify
1262
- if args.stream_prettify and live_display:
1481
+ if use_stream_prettify and live_display:
1263
1482
  live_display.start()
1264
1483
 
1265
1484
  generated_code = client.generate_code(
@@ -1269,23 +1488,29 @@ def main():
1269
1488
  temperature=args.temperature,
1270
1489
  top_p=args.top_p,
1271
1490
  max_tokens=args.max_tokens,
1272
- markdown_format=args.prettify or args.stream_prettify,
1491
+ # Request markdown from API if any prettify option is active
1492
+ markdown_format=use_regular_prettify or use_stream_prettify,
1273
1493
  stream=should_stream,
1274
1494
  stream_callback=stream_callback
1275
1495
  )
1276
1496
 
1277
1497
  # Stop live display if using stream-prettify
1278
- if args.stream_prettify and live_display:
1498
+ if use_stream_prettify and live_display:
1279
1499
  live_display.stop()
1280
1500
 
1281
- if generated_code and not args.stream_prettify:
1282
- if args.prettify:
1501
+ # Print non-streamed output if needed
1502
+ if generated_code and not should_stream:
1503
+ if use_regular_prettify:
1283
1504
  print("\nGenerated code:")
1284
1505
  prettify_markdown(generated_code, args.renderer)
1285
1506
  else:
1507
+ # Should only happen if --no-stream was used without prettify
1286
1508
  print(f"\nGenerated code:\n{generated_code}")
1287
-
1509
+
1288
1510
  elif args.text:
1511
+ # Apply CLI config for text mode
1512
+ args = apply_cli_config(args, "text")
1513
+
1289
1514
  if args.prompt is not None:
1290
1515
  prompt = args.prompt
1291
1516
  else:
@@ -1447,6 +1672,9 @@ def main():
1447
1672
 
1448
1673
  else:
1449
1674
  # Default to chat mode
1675
+ # Apply CLI config for default chat mode
1676
+ args = apply_cli_config(args, "all")
1677
+
1450
1678
  if args.prompt is None:
1451
1679
  try:
1452
1680
  print("Enter your prompt: ", end='')
ngpt/cli_config.py ADDED
@@ -0,0 +1,247 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, Optional, Any, List, Union, Tuple
6
+
7
+ # CLI config options with their types and default values
8
+ CLI_CONFIG_OPTIONS = {
9
+ "language": {"type": "str", "default": "python", "context": ["code"]},
10
+ "provider": {"type": "str", "default": None, "context": ["all"]},
11
+ "temperature": {"type": "float", "default": 0.7, "context": ["all"]},
12
+ "top_p": {"type": "float", "default": 1.0, "context": ["all"]},
13
+ "max_tokens": {"type": "int", "default": None, "context": ["all"]},
14
+ "log": {"type": "str", "default": None, "context": ["interactive", "text"]},
15
+ "preprompt": {"type": "str", "default": None, "context": ["all"]},
16
+ "no-stream": {"type": "bool", "default": False, "context": ["all"], "exclusive": ["prettify", "stream-prettify"]},
17
+ "prettify": {"type": "bool", "default": False, "context": ["all"], "exclusive": ["no-stream", "stream-prettify"]},
18
+ "stream-prettify": {"type": "bool", "default": False, "context": ["all"], "exclusive": ["no-stream", "prettify"]},
19
+ "renderer": {"type": "str", "default": "auto", "context": ["all"]},
20
+ "config-index": {"type": "int", "default": 0, "context": ["all"]},
21
+ "web-search": {"type": "bool", "default": False, "context": ["all"]},
22
+ }
23
+
24
+ def get_cli_config_dir() -> Path:
25
+ """Get the appropriate CLI config directory based on OS."""
26
+ if sys.platform == "win32":
27
+ # Windows
28
+ config_dir = Path(os.environ.get("APPDATA", "")) / "ngpt"
29
+ elif sys.platform == "darwin":
30
+ # macOS
31
+ config_dir = Path.home() / "Library" / "Application Support" / "ngpt"
32
+ else:
33
+ # Linux and other Unix-like systems
34
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
35
+ if xdg_config_home:
36
+ config_dir = Path(xdg_config_home) / "ngpt"
37
+ else:
38
+ config_dir = Path.home() / ".config" / "ngpt"
39
+
40
+ # Ensure the directory exists
41
+ config_dir.mkdir(parents=True, exist_ok=True)
42
+ return config_dir
43
+
44
+ def get_cli_config_path() -> Path:
45
+ """Get the path to the CLI config file."""
46
+ return get_cli_config_dir() / "ngpt-cli.conf"
47
+
48
+ def load_cli_config() -> Dict[str, Any]:
49
+ """Load CLI configuration from the config file."""
50
+ config_path = get_cli_config_path()
51
+
52
+ # Default empty config
53
+ config = {}
54
+
55
+ # Load from config file if it exists
56
+ if config_path.exists():
57
+ try:
58
+ with open(config_path, "r") as f:
59
+ config = json.load(f)
60
+ except (json.JSONDecodeError, IOError) as e:
61
+ print(f"Warning: Could not read CLI config file: {e}", file=sys.stderr)
62
+
63
+ return config
64
+
65
+ def save_cli_config(config: Dict[str, Any]) -> bool:
66
+ """Save CLI configuration to the config file."""
67
+ config_path = get_cli_config_path()
68
+
69
+ try:
70
+ with open(config_path, "w") as f:
71
+ json.dump(config, f, indent=2)
72
+ return True
73
+ except Exception as e:
74
+ print(f"Error saving CLI configuration: {e}", file=sys.stderr)
75
+ return False
76
+
77
+ def set_cli_config_option(option: str, value: Any) -> Tuple[bool, str]:
78
+ """Set a CLI configuration option.
79
+
80
+ Args:
81
+ option: The name of the option to set
82
+ value: The value to set
83
+
84
+ Returns:
85
+ Tuple of (success, message)
86
+ """
87
+ # Check if option is valid
88
+ if option not in CLI_CONFIG_OPTIONS:
89
+ return False, f"Error: Unknown option '{option}'"
90
+
91
+ # Load current config
92
+ config = load_cli_config()
93
+
94
+ # Parse and validate the value based on option type
95
+ option_type = CLI_CONFIG_OPTIONS[option]["type"]
96
+
97
+ try:
98
+ if option_type == "str":
99
+ parsed_value = str(value)
100
+ elif option_type == "int":
101
+ parsed_value = int(value)
102
+ elif option_type == "float":
103
+ parsed_value = float(value)
104
+ elif option_type == "bool":
105
+ if isinstance(value, bool):
106
+ parsed_value = value
107
+ elif value.lower() in ("true", "yes", "1", "t", "y"):
108
+ parsed_value = True
109
+ elif value.lower() in ("false", "no", "0", "f", "n"):
110
+ parsed_value = False
111
+ else:
112
+ return False, f"Error: Invalid boolean value '{value}' for option '{option}'"
113
+ else:
114
+ return False, f"Error: Unsupported option type '{option_type}'"
115
+
116
+ # Handle mutual exclusivity for boolean options
117
+ if option_type == "bool" and "exclusive" in CLI_CONFIG_OPTIONS[option]:
118
+ if parsed_value: # If setting this option to True
119
+ # Set all other exclusive options to False
120
+ for excl_option in CLI_CONFIG_OPTIONS[option]["exclusive"]:
121
+ config[excl_option] = False
122
+ # No special handling needed if setting to False, just update the value
123
+
124
+ # Set the value in the config
125
+ config[option] = parsed_value
126
+
127
+ # Save the config
128
+ if save_cli_config(config):
129
+ return True, f"Successfully set {option}={parsed_value}"
130
+ else:
131
+ return False, "Error saving configuration"
132
+
133
+ except (ValueError, TypeError) as e:
134
+ return False, f"Error: {str(e)}"
135
+
136
+ def get_cli_config_option(option: str = None) -> Tuple[bool, Union[str, Dict[str, Any]]]:
137
+ """Get a CLI configuration option or all options.
138
+
139
+ Args:
140
+ option: The name of the option to get, or None for all options
141
+
142
+ Returns:
143
+ Tuple of (success, value/message)
144
+ """
145
+ # Load current config
146
+ config = load_cli_config()
147
+
148
+ # Return all options if no specific option requested
149
+ if option is None:
150
+ return True, config
151
+
152
+ # Check if option is valid
153
+ if option not in CLI_CONFIG_OPTIONS:
154
+ return False, f"Error: Unknown option '{option}'"
155
+
156
+ # Return the option value if set, otherwise the default
157
+ if option in config:
158
+ return True, config[option]
159
+ else:
160
+ return True, CLI_CONFIG_OPTIONS[option]["default"]
161
+
162
+ def unset_cli_config_option(option: str) -> Tuple[bool, str]:
163
+ """Unset a CLI configuration option.
164
+
165
+ Args:
166
+ option: The name of the option to unset
167
+
168
+ Returns:
169
+ Tuple of (success, message)
170
+ """
171
+ # Check if option is valid
172
+ if option not in CLI_CONFIG_OPTIONS:
173
+ return False, f"Error: Unknown option '{option}'"
174
+
175
+ # Load current config
176
+ config = load_cli_config()
177
+
178
+ # Remove the option if it exists
179
+ if option in config:
180
+ del config[option]
181
+
182
+ # Save the config
183
+ if save_cli_config(config):
184
+ return True, f"Successfully unset {option}"
185
+ else:
186
+ return False, "Error saving configuration"
187
+ else:
188
+ return True, f"Note: Option '{option}' was not set"
189
+
190
+ def apply_cli_config(args: Any, mode: str) -> Any:
191
+ """Apply CLI configuration to args object, respecting context and not overriding explicit args.
192
+
193
+ Args:
194
+ args: The argparse namespace object
195
+ mode: The current mode ('interactive', 'shell', 'code', 'text', or 'all' for default)
196
+
197
+ Returns:
198
+ Updated args object
199
+ """
200
+ # Load CLI config
201
+ cli_config = load_cli_config()
202
+
203
+ # Get command-line arguments provided by the user
204
+ explicit_args = set(arg for arg in sys.argv[1:] if arg.startswith('--'))
205
+
206
+ # For each option in CLI config, check if it should be applied
207
+ for option, value in cli_config.items():
208
+ # Skip if not a valid option
209
+ if option not in CLI_CONFIG_OPTIONS:
210
+ continue
211
+
212
+ # Check context
213
+ option_context = CLI_CONFIG_OPTIONS[option]["context"]
214
+ if "all" not in option_context and mode not in option_context:
215
+ continue
216
+
217
+ # Convert dashes to underscores for argparse compatibility
218
+ arg_name = option.replace("-", "_")
219
+
220
+ # Skip if explicitly set via command line
221
+ # Check common variants like --option
222
+ cli_option = f"--{option}"
223
+ if cli_option in explicit_args:
224
+ continue
225
+
226
+ # Check exclusivity constraints against *explicitly set* args
227
+ # Don't apply a CLI config option if an exclusive option was explicitly set
228
+ if "exclusive" in CLI_CONFIG_OPTIONS[option]:
229
+ skip = False
230
+ for excl_option in CLI_CONFIG_OPTIONS[option]["exclusive"]:
231
+ excl_cli_option = f"--{excl_option}"
232
+ if excl_cli_option in explicit_args:
233
+ skip = True
234
+ break # Skip applying this CLI config value
235
+ if skip:
236
+ continue
237
+
238
+ # Apply the value from CLI config
239
+ # Ensure the attribute exists on args before setting
240
+ if hasattr(args, arg_name):
241
+ setattr(args, arg_name, value)
242
+
243
+ return args
244
+
245
+ def list_cli_config_options() -> List[str]:
246
+ """List all available CLI configuration options."""
247
+ return sorted(CLI_CONFIG_OPTIONS.keys())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ngpt
3
- Version: 2.7.2
3
+ Version: 2.8.0
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
@@ -54,6 +54,7 @@ A lightweight Python CLI and library for interacting with OpenAI-compatible APIs
54
54
  - [Python Library](#as-a-library)
55
55
  - [Configuration](#configuration)
56
56
  - [Command Line Options](#command-line-options)
57
+ - [CLI Configuration](#cli-configuration)
57
58
  - [Interactive Configuration](#interactive-configuration)
58
59
  - [Configuration File](#configuration-file)
59
60
  - [Configuration Priority](#configuration-priority)
@@ -315,6 +316,72 @@ You can configure the client using the following options:
315
316
 
316
317
  For a complete reference of all available options, see the [CLI Usage Guide](https://nazdridoy.github.io/ngpt/usage/cli_usage.html).
317
318
 
319
+ ### CLI Configuration
320
+
321
+ NGPT offers a CLI configuration system that allows you to set default values for command-line options:
322
+
323
+ ```bash
324
+ # Set default options
325
+ ngpt --cli-config set language typescript
326
+ ngpt --cli-config set temperature 0.9
327
+ ngpt --cli-config set prettify true
328
+
329
+ # View current settings
330
+ ngpt --cli-config get
331
+
332
+ # Get a specific setting
333
+ ngpt --cli-config get language
334
+
335
+ # Remove a setting
336
+ ngpt --cli-config unset prettify
337
+
338
+ # List all available options
339
+ ngpt --cli-config list
340
+
341
+ # Show help information
342
+ ngpt --cli-config help
343
+ ```
344
+
345
+ Key features of CLI configuration:
346
+ - **Context-Aware**: Settings are applied based on the current command mode (e.g., `language` only applies in code generation mode `-c`).
347
+ - **Priority**: When determining option values, NGPT uses the following priority order (highest to lowest):
348
+ 1. Command-line arguments
349
+ 2. Environment variables
350
+ 3. CLI configuration (ngpt-cli.conf)
351
+ 4. Main configuration file (ngpt.conf)
352
+ 5. Default values
353
+ - **Mutual Exclusivity**: For options like `no-stream`, `prettify`, and `stream-prettify`, setting one to `True` automatically sets the others to `False` in the configuration file, ensuring consistency.
354
+ - **Smart Selection**: The `provider` setting is used to select which configuration profile to use, offering a persistent way to select your preferred API.
355
+
356
+ Available options include:
357
+ - General options (all modes): `provider`, `temperature`, `top_p`, `max_tokens`, `preprompt`, `renderer`, `config-index`, `web-search`
358
+ - Mode-specific options: `language` (code mode only), `log` (interactive and text modes)
359
+ - Mutually exclusive options: `no-stream`, `prettify`, `stream-prettify`
360
+
361
+ #### Practical Examples
362
+
363
+ ```bash
364
+ # Set Gemini as your default provider
365
+ ngpt --cli-config set provider Gemini
366
+ # Now you can run commands without specifying --provider
367
+ ngpt "Explain quantum computing"
368
+
369
+ # Configure code generation for TypeScript
370
+ ngpt --cli-config set language typescript
371
+ # Now in code mode, TypeScript will be used by default
372
+ ngpt -c "Write a function to sort an array"
373
+
374
+ # Set a higher temperature for more creative responses
375
+ ngpt --cli-config set temperature 0.9
376
+ ```
377
+
378
+ The CLI configuration is stored in:
379
+ - Linux: `~/.config/ngpt/ngpt-cli.conf`
380
+ - macOS: `~/Library/Application Support/ngpt/ngpt-cli.conf`
381
+ - Windows: `%APPDATA%\ngpt\ngpt-cli.conf`
382
+
383
+ For more details, see the [CLI Configuration Guide](https://nazdridoy.github.io/ngpt/usage/cli_config.html).
384
+
318
385
  ### Interactive Configuration
319
386
 
320
387
  The `--config` option without arguments enters interactive configuration mode, allowing you to add or edit configurations:
@@ -387,10 +454,11 @@ For details on the configuration file format and structure, see the [Configurati
387
454
 
388
455
  nGPT determines configuration values in the following order (highest priority first):
389
456
 
390
- 1. Command line arguments (`--api-key`, `--base-url`, `--model`)
457
+ 1. Command line arguments (`--api-key`, `--base-url`, `--model`, etc.)
391
458
  2. Environment variables (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`)
392
- 3. Configuration file (selected by `--config-index`, defaults to index 0)
393
- 4. Default values
459
+ 3. CLI configuration file (`ngpt-cli.conf`, managed with `--cli-config`)
460
+ 4. Main configuration file `ngpt.conf` or `custom-config-file`
461
+ 5. Default values
394
462
 
395
463
  ## Contributing
396
464
 
@@ -0,0 +1,10 @@
1
+ ngpt/__init__.py,sha256=awvycdj3tgcOr0BO81L4XU6DOtnToxFqkPHe1Pyu0Bw,652
2
+ ngpt/cli.py,sha256=8E65ovaJE0OJ3-M6aw_FqCNSy_1-x1h4NwhnsZOlXq4,81580
3
+ ngpt/cli_config.py,sha256=LatikOJTtohbmCfXM6AH44qplbpyAD-x1kMkmaJHRS0,9046
4
+ ngpt/client.py,sha256=Rv-JO8RAmw1v3gdLkwaPe_PEw6p83cejO0YNT_DDjeg,15134
5
+ ngpt/config.py,sha256=WYOk_b1eiYjo6hpV3pfXr2RjqhOnmKqwZwKid1T41I4,10363
6
+ ngpt-2.8.0.dist-info/METADATA,sha256=g6KOwGut4qU6t1fbpsEaH1FPw6_PSDRRT3kZUGqO2ws,17993
7
+ ngpt-2.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ ngpt-2.8.0.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
9
+ ngpt-2.8.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
10
+ ngpt-2.8.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
2
- ngpt/cli.py,sha256=kdEgC4mOml4I3uX_w1SsCZUqlaccdyIQGtox3IjPhbs,70115
3
- ngpt/client.py,sha256=Rv-JO8RAmw1v3gdLkwaPe_PEw6p83cejO0YNT_DDjeg,15134
4
- ngpt/config.py,sha256=WYOk_b1eiYjo6hpV3pfXr2RjqhOnmKqwZwKid1T41I4,10363
5
- ngpt-2.7.2.dist-info/METADATA,sha256=J1epGacDeo1w9bWUUqAQ8LvUI1I-rEtKIxm6TwRA-7U,15452
6
- ngpt-2.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
- ngpt-2.7.2.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
8
- ngpt-2.7.2.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
9
- ngpt-2.7.2.dist-info/RECORD,,
File without changes