ngpt 2.13.0__py3-none-any.whl → 2.14.1__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/args.py CHANGED
@@ -88,6 +88,19 @@ def setup_argument_parser():
88
88
  global_group.add_argument('--renderer', choices=['auto', 'rich', 'glow'], default='auto',
89
89
  help='Select which markdown renderer to use with --prettify (auto, rich, or glow)')
90
90
 
91
+ # GitCommit message options
92
+ gitcommsg_group = parser.add_argument_group('Git Commit Message Options')
93
+ gitcommsg_group.add_argument('-m', '--message-context',
94
+ help='Context to guide AI generation (e.g., file types, commit type)')
95
+ gitcommsg_group.add_argument('-r', '--recursive-chunk', action='store_true',
96
+ help='Process large diffs in chunks with recursive analysis if needed')
97
+ gitcommsg_group.add_argument('--diff', metavar='FILE', nargs='?', const=True,
98
+ help='Use diff from specified file instead of staged changes. If used without a path, uses the path from CLI config.')
99
+ gitcommsg_group.add_argument('--chunk-size', type=int, default=200,
100
+ help='Number of lines per chunk when chunking is enabled (default: 200)')
101
+ gitcommsg_group.add_argument('--max-depth', type=int, default=3,
102
+ help='Maximum recursion depth for recursive chunking (default: 3)')
103
+
91
104
  # Mode flags (mutually exclusive)
92
105
  mode_group = parser.add_argument_group('Modes (mutually exclusive)')
93
106
  mode_exclusive_group = mode_group.add_mutually_exclusive_group()
@@ -103,6 +116,8 @@ def setup_argument_parser():
103
116
  help='Read from stdin and use content with prompt. Use {} in prompt as placeholder for stdin content')
104
117
  mode_exclusive_group.add_argument('--rewrite', action='store_true',
105
118
  help='Rewrite text from stdin to be more natural while preserving tone and meaning')
119
+ mode_exclusive_group.add_argument('--gitcommsg', action='store_true',
120
+ help='Generate AI-powered git commit messages from staged changes or diff file')
106
121
 
107
122
  return parser
108
123
 
ngpt/cli/main.py CHANGED
@@ -24,6 +24,7 @@ from .modes.code import code_mode
24
24
  from .modes.shell import shell_mode
25
25
  from .modes.text import text_mode
26
26
  from .modes.rewrite import rewrite_mode
27
+ from .modes.gitcommsg import gitcommsg_mode
27
28
  from .args import parse_args, validate_args, handle_cli_config_args, setup_argument_parser, validate_markdown_renderer
28
29
 
29
30
  def show_cli_config_help():
@@ -45,7 +46,8 @@ def show_cli_config_help():
45
46
  "code": [],
46
47
  "interactive": [],
47
48
  "text": [],
48
- "shell": []
49
+ "shell": [],
50
+ "gitcommsg": [] # Add gitcommsg context
49
51
  }
50
52
 
51
53
  for option, meta in CLI_CONFIG_OPTIONS.items():
@@ -70,7 +72,8 @@ def show_cli_config_help():
70
72
  ("code", "Code generation mode"),
71
73
  ("interactive", "Interactive mode"),
72
74
  ("text", "Text mode"),
73
- ("shell", "Shell mode")
75
+ ("shell", "Shell mode"),
76
+ ("gitcommsg", "Git commit message mode") # Add gitcommsg mode
74
77
  ]:
75
78
  if context_groups[mode]:
76
79
  print(f"\n {COLORS['yellow']}Options for {options}:{COLORS['reset']}")
@@ -87,6 +90,8 @@ def show_cli_config_help():
87
90
  print(f" {COLORS['yellow']}ngpt --cli-config set language java{COLORS['reset']} - Set default language to java for code generation")
88
91
  print(f" {COLORS['yellow']}ngpt --cli-config set temperature 0.9{COLORS['reset']} - Set default temperature to 0.9")
89
92
  print(f" {COLORS['yellow']}ngpt --cli-config set no-stream true{COLORS['reset']} - Disable streaming by default")
93
+ print(f" {COLORS['yellow']}ngpt --cli-config set recursive-chunk true{COLORS['reset']} - Enable recursive chunking for git commit messages")
94
+ print(f" {COLORS['yellow']}ngpt --cli-config set diff /path/to/file.diff{COLORS['reset']} - Set default diff file for git commit messages")
90
95
  print(f" {COLORS['yellow']}ngpt --cli-config get temperature{COLORS['reset']} - Check the current temperature setting")
91
96
  print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all current CLI settings")
92
97
  print(f" {COLORS['yellow']}ngpt --cli-config unset language{COLORS['reset']} - Remove language setting")
@@ -227,15 +232,17 @@ def main():
227
232
  # Change log to True to create a temp file
228
233
  args.log = True
229
234
 
230
- # If --log is True, it means it was used without a path value
231
- log_path = None if args.log is True else args.log
232
- logger = create_logger(log_path)
233
- if logger:
234
- logger.open()
235
- print(f"{COLORS['green']}Logging session to: {logger.get_log_path()}{COLORS['reset']}")
236
- # If it's a temporary log file, inform the user
237
- if logger.is_temporary():
238
- print(f"{COLORS['green']}Created temporary log file.{COLORS['reset']}")
235
+ # Skip logger initialization for gitcommsg mode as it creates its own logger
236
+ if not args.gitcommsg:
237
+ # If --log is True, it means it was used without a path value
238
+ log_path = None if args.log is True else args.log
239
+ logger = create_logger(log_path)
240
+ if logger:
241
+ logger.open()
242
+ print(f"{COLORS['green']}Logging session to: {logger.get_log_path()}{COLORS['reset']}")
243
+ # If it's a temporary log file, inform the user
244
+ if logger.is_temporary():
245
+ print(f"{COLORS['green']}Created temporary log file.{COLORS['reset']}")
239
246
 
240
247
  # Priority order for config selection:
241
248
  # 1. Command-line arguments (args.provider, args.config_index)
@@ -461,7 +468,7 @@ def main():
461
468
  return
462
469
 
463
470
  # For interactive mode, we'll allow continuing without a specific prompt
464
- 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 or args.rewrite):
471
+ 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 or args.rewrite or args.gitcommsg):
465
472
  # Simply use the parser's help
466
473
  parser = setup_argument_parser()
467
474
  parser.print_help()
@@ -557,6 +564,13 @@ def main():
557
564
  # Rewrite mode (process stdin)
558
565
  rewrite_mode(client, args, logger=logger)
559
566
 
567
+ elif args.gitcommsg:
568
+ # Apply CLI config for gitcommsg mode
569
+ args = apply_cli_config(args, "gitcommsg")
570
+
571
+ # Git commit message generation mode
572
+ gitcommsg_mode(client, args, logger=logger)
573
+
560
574
  else:
561
575
  # Default to chat mode
562
576
  # Apply CLI config for default chat mode
@@ -3,5 +3,6 @@ from .code import code_mode
3
3
  from .shell import shell_mode
4
4
  from .text import text_mode
5
5
  from .rewrite import rewrite_mode
6
+ from .gitcommsg import gitcommsg_mode
6
7
 
7
- __all__ = ['chat_mode', 'code_mode', 'shell_mode', 'text_mode', 'rewrite_mode']
8
+ __all__ = ['chat_mode', 'code_mode', 'shell_mode', 'text_mode', 'rewrite_mode', 'gitcommsg_mode']
@@ -0,0 +1,758 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import tempfile
5
+ import time
6
+ import subprocess
7
+ from datetime import datetime
8
+ import logging
9
+ from ..formatters import COLORS
10
+ from ...utils.log import create_gitcommsg_logger
11
+ from ...utils.cli_config import get_cli_config_option
12
+
13
+ def get_diff_content(diff_file=None):
14
+ """Get git diff content from file or git staged changes.
15
+
16
+ Args:
17
+ diff_file: Path to a diff file to use instead of git staged changes
18
+
19
+ Returns:
20
+ str: Content of the diff, or None if no diff is available
21
+ """
22
+ if diff_file:
23
+ try:
24
+ with open(diff_file, 'r') as f:
25
+ content = f.read()
26
+ return content
27
+ except Exception as e:
28
+ print(f"{COLORS['yellow']}Error reading diff file: {str(e)}{COLORS['reset']}")
29
+ return None
30
+
31
+ # No diff file specified, get staged changes from git
32
+ try:
33
+ result = subprocess.run(
34
+ ["git", "diff", "--staged"],
35
+ capture_output=True,
36
+ text=True
37
+ )
38
+
39
+ if result.returncode != 0:
40
+ raise Exception(f"Git command failed: {result.stderr}")
41
+
42
+ # Check if there are staged changes
43
+ if not result.stdout.strip():
44
+ print(f"{COLORS['yellow']}No staged changes found. Stage changes with 'git add' first.{COLORS['reset']}")
45
+ return None
46
+
47
+ return result.stdout
48
+ except Exception as e:
49
+ print(f"{COLORS['yellow']}Error getting git diff: {str(e)}{COLORS['reset']}")
50
+ return None
51
+
52
+ def split_into_chunks(content, chunk_size=200):
53
+ """Split content into chunks of specified size.
54
+
55
+ Args:
56
+ content: The content to split into chunks
57
+ chunk_size: Maximum number of lines per chunk
58
+
59
+ Returns:
60
+ list: List of content chunks
61
+ """
62
+ lines = content.splitlines()
63
+ chunks = []
64
+
65
+ for i in range(0, len(lines), chunk_size):
66
+ chunk = lines[i:i+chunk_size]
67
+ chunks.append("\n".join(chunk))
68
+
69
+ return chunks
70
+
71
+ def process_context(context):
72
+ """Process context string to extract directives and filters.
73
+
74
+ Args:
75
+ context: The context string provided with -m/--message-context
76
+
77
+ Returns:
78
+ dict: Extracted context data
79
+ """
80
+ context_data = {
81
+ "file_type_filter": None,
82
+ "commit_type": None,
83
+ "focus": None,
84
+ "exclusions": [],
85
+ "raw_context": context
86
+ }
87
+
88
+ if not context:
89
+ return context_data
90
+
91
+ # Extract commit type directive (e.g., "type:feat")
92
+ if "type:" in context:
93
+ match = re.search(r"type:(\w+)", context)
94
+ if match:
95
+ context_data["commit_type"] = match.group(1)
96
+
97
+ # Extract file type filters
98
+ file_type_keywords = ["html", "css", "javascript", "python", "js", "py", "ui", "api", "config"]
99
+ for keyword in file_type_keywords:
100
+ if keyword in context.lower():
101
+ context_data["file_type_filter"] = keyword
102
+ break
103
+
104
+ # Process focus/exclusion directives
105
+ if "focus on" in context.lower() or "only mention" in context.lower():
106
+ focus_match = re.search(r"focus(?:\s+on)?\s+(\w+)", context.lower())
107
+ if focus_match:
108
+ context_data["focus"] = focus_match.group(1)
109
+
110
+ if any(x in context.lower() for x in ["ignore", "don't include", "exclude"]):
111
+ exclusion_matches = re.findall(r"(?:ignore|don't include|exclude)\s+(\w+)", context.lower())
112
+ context_data["exclusions"] = exclusion_matches
113
+
114
+ return context_data
115
+
116
+ def create_system_prompt(context_data=None):
117
+ """Create system prompt based on context data.
118
+
119
+ Args:
120
+ context_data: The processed context data
121
+
122
+ Returns:
123
+ str: System prompt for the AI
124
+ """
125
+ base_prompt = """You are an expert Git commit message writer. Your task is to analyze the git diff and create a precise, factual commit message following the conventional commit format.
126
+
127
+ FORMAT:
128
+ type[(scope)]: <concise summary> (max 50 chars)
129
+
130
+ - [type] <specific change 1> (filename:function/method/line)
131
+ - [type] <specific change 2> (filename:function/method/line)
132
+ - [type] <additional changes...>
133
+
134
+ COMMIT TYPES:
135
+ - feat: New user-facing features
136
+ - fix: Bug fixes or error corrections
137
+ - refactor: Code restructuring (no behavior change)
138
+ - style: Formatting/whitespace changes only
139
+ - docs: Documentation only
140
+ - test: Test-related changes
141
+ - perf: Performance improvements
142
+ - build: Build system changes
143
+ - ci: CI/CD pipeline changes
144
+ - chore: Routine maintenance tasks
145
+ - revert: Reverting previous changes
146
+ - add: New files without user-facing features
147
+ - remove: Removing files/code
148
+ - update: Changes to existing functionality
149
+ - security: Security-related changes
150
+ - config: Configuration changes
151
+ - ui: User interface changes
152
+ - api: API-related changes
153
+
154
+ RULES:
155
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
156
+ 2. NEVER invent or assume changes not directly visible in the code
157
+ 3. EVERY bullet point MUST reference specific files/functions/lines
158
+ 4. Include ALL significant changes (do not skip any important modifications)
159
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
160
+ 6. Keep summary line under 50 characters (mandatory)
161
+ 7. Use appropriate type tags for each change (main summary and each bullet)
162
+ 8. ONLY describe code that was actually changed
163
+ 9. Focus on technical specifics, avoid general statements
164
+ 10. Include proper technical details (method names, component identifiers, etc.)
165
+ 11. When all changes are to the same file, mention it once in the summary"""
166
+
167
+ if not context_data:
168
+ return base_prompt
169
+
170
+ # Add file type filtering instructions
171
+ if context_data.get("file_type_filter"):
172
+ file_type = context_data["file_type_filter"]
173
+ file_type_prompt = f"""
174
+
175
+ CRITICAL FILE TYPE FILTERING:
176
+ You MUST INCLUDE ONLY changes to {file_type} files or files related to {file_type}.
177
+ You MUST EXCLUDE ALL other files completely from your output.
178
+ This is a strict filter - no exceptions allowed."""
179
+ base_prompt += file_type_prompt
180
+
181
+ # Add commit type directive
182
+ if context_data.get("commit_type"):
183
+ commit_type = context_data["commit_type"]
184
+ commit_type_prompt = f"""
185
+
186
+ CRITICAL COMMIT TYPE DIRECTIVE:
187
+ You MUST use exactly "{commit_type}:" as the commit type prefix.
188
+ This takes highest priority over any other commit type you might determine.
189
+ Do not override this commit type based on your own analysis."""
190
+ base_prompt += commit_type_prompt
191
+
192
+ # Add focus/exclusion directives
193
+ if context_data.get("focus"):
194
+ focus = context_data["focus"]
195
+ focus_prompt = f"""
196
+
197
+ FOCUS DIRECTIVE:
198
+ Focus exclusively on changes related to {focus}.
199
+ Exclude everything else from your analysis."""
200
+ base_prompt += focus_prompt
201
+
202
+ if context_data.get("exclusions"):
203
+ exclusions = ", ".join(context_data["exclusions"])
204
+ exclusion_prompt = f"""
205
+
206
+ EXCLUSION DIRECTIVE:
207
+ Completely ignore and exclude any mentions of: {exclusions}."""
208
+ base_prompt += exclusion_prompt
209
+
210
+ return base_prompt
211
+
212
+ def create_chunk_prompt(chunk):
213
+ """Create prompt for processing a single diff chunk.
214
+
215
+ Args:
216
+ chunk: The diff chunk to process
217
+
218
+ Returns:
219
+ str: Prompt for the AI
220
+ """
221
+ return f"""Analyze this PARTIAL git diff and create a detailed technical summary with this EXACT format:
222
+
223
+ [FILES]: Comma-separated list of affected files with full paths
224
+
225
+ [CHANGES]:
226
+ - Technical detail 1 (include specific function/method names and line numbers)
227
+ - Technical detail 2 (be precise about exactly what code was added/modified/removed)
228
+ - Additional technical details (include ALL significant changes in this chunk)
229
+
230
+ [IMPACT]: Brief technical description of what the changes accomplish
231
+
232
+ CRITICALLY IMPORTANT: Be extremely specific with technical details.
233
+ ALWAYS identify exact function names, method names, class names, and line numbers where possible.
234
+ Use format 'filename:function_name()' or 'filename:line_number' when referencing code locations.
235
+ Be precise and factual - only describe code that actually changed.
236
+
237
+ Diff chunk:
238
+
239
+ {chunk}"""
240
+
241
+ def create_rechunk_prompt(combined_analysis, depth):
242
+ """Create prompt for re-chunking process.
243
+
244
+ Args:
245
+ combined_analysis: The combined analysis to re-chunk
246
+ depth: Current recursion depth
247
+
248
+ Returns:
249
+ str: Prompt for the AI
250
+ """
251
+ return f"""IMPORTANT: You are analyzing SUMMARIES of git changes, not raw git diff.
252
+
253
+ You are in a re-chunking process (depth: {depth}) where the input is already summarized changes.
254
+ Create a TERSE summary of these summaries focusing ONLY ON TECHNICAL CHANGES:
255
+
256
+ [CHANGES]:
257
+ - Technical change 1 (specific file and function)
258
+ - Technical change 2 (specific file and function)
259
+ - Additional relevant changes
260
+
261
+ DO NOT ask for raw git diff. These summaries are all you need to work with.
262
+ Keep your response FACTUAL and SPECIFIC to what's in the summaries.
263
+
264
+ Section to summarize:
265
+
266
+ {combined_analysis}"""
267
+
268
+ def create_combine_prompt(partial_analyses):
269
+ """Create prompt for combining partial analyses.
270
+
271
+ Args:
272
+ partial_analyses: List of partial analyses to combine
273
+
274
+ Returns:
275
+ str: Prompt for the AI
276
+ """
277
+ all_analyses = "\n\n".join(partial_analyses)
278
+
279
+ return f"""===CRITICAL INSTRUCTION===
280
+ You are working with ANALYZED SUMMARIES of git changes, NOT raw git diff.
281
+ The raw git diff has ALREADY been processed into these summaries.
282
+ DO NOT ask for or expect to see the original git diff.
283
+
284
+ TASK: Synthesize these partial analyses into a complete conventional commit message:
285
+
286
+ {all_analyses}
287
+
288
+ Create a CONVENTIONAL COMMIT MESSAGE with:
289
+ 1. First line: "type[(scope)]: brief summary" (50 chars max)
290
+ - Include scope ONLY if you are 100% confident about the affected area
291
+ - Omit scope if changes affect multiple areas or scope is unclear
292
+ 2. ⚠️ ONE BLANK LINE IS MANDATORY - NEVER SKIP THIS STEP ⚠️
293
+ - This blank line MUST be present in EVERY commit message
294
+ - The blank line separates the summary from the detailed changes
295
+ - Without this blank line, the commit message format is invalid
296
+ 3. Bullet points with specific changes, each with appropriate [type] tag
297
+ 4. Reference files in EACH bullet point with function names or line numbers
298
+
299
+ FILENAME & FUNCTION HANDLING RULES:
300
+ - Include SPECIFIC function names, method names, or line numbers when available
301
+ - Format as filename:function() or filename:line_number
302
+ - Use short relative paths for files
303
+ - Group related changes to the same file when appropriate
304
+ - Avoid breaking long filenames across lines
305
+
306
+ STRICTLY follow this format with NO EXPLANATION or additional commentary.
307
+ DO NOT mention insufficient information or ask for the original diff."""
308
+
309
+ def create_final_prompt(diff_content):
310
+ """Create prompt for direct processing without chunking.
311
+
312
+ Args:
313
+ diff_content: The full diff content
314
+
315
+ Returns:
316
+ str: Prompt for the AI
317
+ """
318
+ return f"""Analyze ONLY the exact changes in this git diff and create a precise, factual commit message.
319
+
320
+ FORMAT:
321
+ type[(scope)]: <concise summary> (max 50 chars)
322
+
323
+ - [type] <specific change 1> (filename:function/method/line)
324
+ - [type] <specific change 2> (filename:function/method/line)
325
+ - [type] <additional changes...>
326
+
327
+ RULES FOR FILENAMES:
328
+ 1. Use short relative paths when possible
329
+ 2. For multiple changes to the same file, consider grouping them
330
+ 3. Abbreviate long paths when they're repeated (e.g., 'commit.zsh' instead of full path)
331
+ 4. Avoid breaking filenames across lines
332
+ 5. Only include function names when they add clarity
333
+
334
+ COMMIT TYPES:
335
+ - feat: New user-facing features
336
+ - fix: Bug fixes or error corrections
337
+ - refactor: Code restructuring (no behavior change)
338
+ - style: Formatting/whitespace changes only
339
+ - docs: Documentation only
340
+ - test: Test-related changes
341
+ - perf: Performance improvements
342
+ - build: Build system changes
343
+ - ci: CI/CD pipeline changes
344
+ - chore: Routine maintenance tasks
345
+ - revert: Reverting previous changes
346
+ - add: New files without user-facing features
347
+ - remove: Removing files/code
348
+ - update: Changes to existing functionality
349
+ - security: Security-related changes
350
+ - config: Configuration changes
351
+ - ui: User interface changes
352
+ - api: API-related changes
353
+
354
+ RULES:
355
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
356
+ 2. NEVER invent or assume changes not directly visible in the code
357
+ 3. EVERY bullet point MUST reference specific files/functions/lines
358
+ 4. Include ALL significant changes (do not skip any important modifications)
359
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
360
+ 6. Keep summary line under 50 characters (mandatory)
361
+ 7. Use appropriate type tags for each change (main summary and each bullet)
362
+ 8. ONLY describe code that was actually changed
363
+ 9. Focus on technical specifics, avoid general statements
364
+ 10. Include proper technical details (method names, component identifiers, etc.)
365
+ 11. When all changes are to the same file, mention it once in the summary
366
+
367
+ Git diff to process:
368
+
369
+ {diff_content}"""
370
+
371
+ def handle_api_call(client, prompt, system_prompt=None, logger=None, max_retries=3):
372
+ """Handle API call with retries and error handling.
373
+
374
+ Args:
375
+ client: The NGPTClient instance
376
+ prompt: The prompt to send to the API
377
+ system_prompt: Optional system prompt
378
+ logger: Optional logger instance
379
+ max_retries: Maximum number of retries on error
380
+
381
+ Returns:
382
+ str: Response from the API
383
+ """
384
+ if logger:
385
+ # Enhanced logging of full prompt and system prompt
386
+ logger.log_prompt("DEBUG", system_prompt, prompt)
387
+
388
+ retry_count = 0
389
+ wait_seconds = 5
390
+
391
+ while True:
392
+ try:
393
+ # Create messages array with system prompt if available
394
+ messages = None
395
+ if system_prompt:
396
+ messages = [
397
+ {"role": "system", "content": system_prompt},
398
+ {"role": "user", "content": prompt}
399
+ ]
400
+ else:
401
+ messages = [
402
+ {"role": "user", "content": prompt}
403
+ ]
404
+
405
+ response = client.chat(
406
+ prompt=prompt,
407
+ stream=False,
408
+ markdown_format=False,
409
+ messages=messages
410
+ )
411
+
412
+ if logger:
413
+ # Log full response
414
+ logger.log_response("DEBUG", response)
415
+
416
+ return response
417
+
418
+ except Exception as e:
419
+ retry_count += 1
420
+ error_msg = f"Error (attempt {retry_count}/{max_retries}): {str(e)}"
421
+
422
+ if logger:
423
+ logger.error(error_msg)
424
+
425
+ if retry_count >= max_retries:
426
+ raise Exception(f"Failed after {max_retries} retries: {str(e)}")
427
+
428
+ print(f"{COLORS['yellow']}{error_msg}{COLORS['reset']}")
429
+ print(f"{COLORS['yellow']}Retrying in {wait_seconds} seconds...{COLORS['reset']}")
430
+
431
+ # Create a spinner effect for waiting
432
+ spinner = "⣾⣽⣻⢿⡿⣟⣯⣷"
433
+ for _ in range(wait_seconds * 5):
434
+ for char in spinner:
435
+ sys.stdout.write(f"\r{COLORS['yellow']}Waiting... {char}{COLORS['reset']}")
436
+ sys.stdout.flush()
437
+ time.sleep(0.2)
438
+
439
+ print("\r" + " " * 20 + "\r", end="")
440
+
441
+ # Exponential backoff
442
+ wait_seconds *= 2
443
+
444
+ def process_with_chunking(client, diff_content, context_data, chunk_size=200, recursive=False, max_depth=3, logger=None):
445
+ """Process diff with chunking to handle large diffs.
446
+
447
+ Args:
448
+ client: The NGPTClient instance
449
+ diff_content: The diff content to process
450
+ context_data: The processed context data
451
+ chunk_size: Maximum number of lines per chunk
452
+ recursive: Whether to use recursive chunking
453
+ max_depth: Maximum recursion depth
454
+ logger: Optional logger instance
455
+
456
+ Returns:
457
+ str: Generated commit message
458
+ """
459
+ # Create system prompt
460
+ system_prompt = create_system_prompt(context_data)
461
+
462
+ # Log initial diff content
463
+ if logger:
464
+ logger.log_diff("DEBUG", diff_content)
465
+
466
+ # Split diff into chunks
467
+ chunks = split_into_chunks(diff_content, chunk_size)
468
+ chunk_count = len(chunks)
469
+
470
+ if logger:
471
+ logger.info(f"Processing {chunk_count} chunks of {chunk_size} lines each")
472
+
473
+ print(f"{COLORS['green']}Processing diff in {chunk_count} chunks...{COLORS['reset']}")
474
+
475
+ # Process each chunk
476
+ partial_analyses = []
477
+ for i, chunk in enumerate(chunks):
478
+ print(f"\n{COLORS['cyan']}[Chunk {i+1}/{chunk_count}]{COLORS['reset']}")
479
+
480
+ # Log chunk content
481
+ if logger:
482
+ logger.log_chunks("DEBUG", i+1, chunk_count, chunk)
483
+
484
+ # Create chunk prompt
485
+ chunk_prompt = create_chunk_prompt(chunk)
486
+
487
+ # Log chunk template
488
+ if logger:
489
+ logger.log_template("DEBUG", "CHUNK", chunk_prompt)
490
+
491
+ # Process chunk
492
+ print(f"{COLORS['yellow']}Analyzing changes...{COLORS['reset']}")
493
+ try:
494
+ result = handle_api_call(client, chunk_prompt, system_prompt, logger)
495
+ partial_analyses.append(result)
496
+ print(f"{COLORS['green']}✓ Chunk {i+1} processed{COLORS['reset']}")
497
+ except Exception as e:
498
+ print(f"{COLORS['red']}Error processing chunk {i+1}: {str(e)}{COLORS['reset']}")
499
+ if logger:
500
+ logger.error(f"Error processing chunk {i+1}: {str(e)}")
501
+ return None
502
+
503
+ # Rate limit protection between chunks
504
+ if i < chunk_count - 1:
505
+ print(f"{COLORS['yellow']}Waiting to avoid rate limits...{COLORS['reset']}")
506
+ time.sleep(5)
507
+
508
+ # Combine partial analyses
509
+ print(f"\n{COLORS['cyan']}Combining analyses from {len(partial_analyses)} chunks...{COLORS['reset']}")
510
+
511
+ # Log partial analyses
512
+ if logger:
513
+ combined_analyses = "\n\n".join(partial_analyses)
514
+ logger.log_content("DEBUG", "PARTIAL_ANALYSES", combined_analyses)
515
+
516
+ # Check if we need to use recursive chunking
517
+ combined_analyses = "\n\n".join(partial_analyses)
518
+ combined_line_count = len(combined_analyses.splitlines())
519
+
520
+ if recursive and combined_line_count > 50 and max_depth > 0:
521
+ # Use recursive chunking
522
+ return recursive_process(client, combined_analyses, context_data, max_depth, logger)
523
+ else:
524
+ # Use direct combination
525
+ combine_prompt = create_combine_prompt(partial_analyses)
526
+
527
+ # Log combine template
528
+ if logger:
529
+ logger.log_template("DEBUG", "COMBINE", combine_prompt)
530
+
531
+ try:
532
+ result = handle_api_call(client, combine_prompt, system_prompt, logger)
533
+ return result
534
+ except Exception as e:
535
+ print(f"{COLORS['red']}Error combining analyses: {str(e)}{COLORS['reset']}")
536
+ if logger:
537
+ logger.error(f"Error combining analyses: {str(e)}")
538
+ return None
539
+
540
+ def recursive_process(client, combined_analysis, context_data, max_depth, logger=None, current_depth=1):
541
+ """Process large analysis results recursively.
542
+
543
+ Args:
544
+ client: The NGPTClient instance
545
+ combined_analysis: The combined analysis to process
546
+ context_data: The processed context data
547
+ max_depth: Maximum recursion depth
548
+ logger: Optional logger instance
549
+ current_depth: Current recursion depth
550
+
551
+ Returns:
552
+ str: Generated commit message
553
+ """
554
+ system_prompt = create_system_prompt(context_data)
555
+
556
+ print(f"\n{COLORS['cyan']}Recursive chunking level {current_depth}/{max_depth}...{COLORS['reset']}")
557
+
558
+ if logger:
559
+ logger.info(f"Starting recursive chunking at depth {current_depth}/{max_depth}")
560
+ logger.debug(f"Combined analysis size: {len(combined_analysis.splitlines())} lines")
561
+ logger.log_content("DEBUG", f"COMBINED_ANALYSIS_DEPTH_{current_depth}", combined_analysis)
562
+
563
+ # Create rechunk prompt
564
+ rechunk_prompt = create_rechunk_prompt(combined_analysis, current_depth)
565
+
566
+ # Log rechunk template
567
+ if logger:
568
+ logger.log_template("DEBUG", f"RECHUNK_DEPTH_{current_depth}", rechunk_prompt)
569
+
570
+ # Process rechunk
571
+ try:
572
+ result = handle_api_call(client, rechunk_prompt, system_prompt, logger)
573
+
574
+ # Check if further recursive chunking is needed
575
+ result_line_count = len(result.splitlines())
576
+
577
+ if result_line_count > 50 and current_depth < max_depth:
578
+ # Need another level of chunking
579
+ print(f"{COLORS['yellow']}Result still too large ({result_line_count} lines), continuing recursion...{COLORS['reset']}")
580
+ if logger:
581
+ logger.info(f"Result still too large ({result_line_count} lines), depth {current_depth}/{max_depth}")
582
+
583
+ return recursive_process(client, result, context_data, max_depth, logger, current_depth + 1)
584
+ else:
585
+ # Final processing
586
+ print(f"{COLORS['green']}Recursion complete, generating final commit message...{COLORS['reset']}")
587
+
588
+ # Create final combine prompt
589
+ final_prompt = f"""Create a CONVENTIONAL COMMIT MESSAGE based on these analyzed git changes:
590
+
591
+ {result}
592
+
593
+ FORMAT:
594
+ type[(scope)]: <concise summary> (max 50 chars)
595
+
596
+ - [type] <specific change 1> (filename:function/method/line)
597
+ - [type] <specific change 2> (filename:function/method/line)
598
+ - [type] <additional changes...>
599
+
600
+ RULES:
601
+ 1. First line must be under 50 characters
602
+ 2. Include a blank line after the first line
603
+ 3. Each bullet must include specific file references
604
+ 4. BE SPECIFIC - mention technical details and function names
605
+
606
+ DO NOT include any explanation or commentary outside the commit message format."""
607
+
608
+ # Log final template
609
+ if logger:
610
+ logger.log_template("DEBUG", "FINAL", final_prompt)
611
+
612
+ return handle_api_call(client, final_prompt, system_prompt, logger)
613
+ except Exception as e:
614
+ print(f"{COLORS['red']}Error in recursive processing: {str(e)}{COLORS['reset']}")
615
+ if logger:
616
+ logger.error(f"Error in recursive processing at depth {current_depth}: {str(e)}")
617
+ return None
618
+
619
+ def gitcommsg_mode(client, args, logger=None):
620
+ """Handle the Git commit message generation mode.
621
+
622
+ Args:
623
+ client: The NGPTClient instance
624
+ args: The parsed command line arguments
625
+ logger: Optional logger instance
626
+ """
627
+ # Set up logging if requested
628
+ custom_logger = None
629
+ log_path = None
630
+
631
+ if args.log:
632
+ custom_logger = create_gitcommsg_logger(args.log)
633
+
634
+ # Use both loggers if they exist
635
+ active_logger = logger if logger else custom_logger
636
+
637
+ if active_logger:
638
+ active_logger.info("Starting gitcommsg mode")
639
+ active_logger.debug(f"Args: {args}")
640
+
641
+ try:
642
+ # Check if --diff was explicitly passed on the command line
643
+ diff_option_provided = '--diff' in sys.argv
644
+ diff_path_provided = diff_option_provided and args.diff is not None and args.diff is not True
645
+
646
+ # If --diff wasn't explicitly provided on the command line, don't use the config value
647
+ if not diff_option_provided:
648
+ # Even if diff is in CLI config, don't use it unless --diff flag is provided
649
+ diff_file = None
650
+ if active_logger:
651
+ active_logger.info("Not using diff file from CLI config because --diff flag was not provided")
652
+ else:
653
+ # --diff flag was provided on command line
654
+ if args.diff is True:
655
+ # --diff flag was used without a path, use the value from CLI config
656
+ success, config_diff = get_cli_config_option("diff")
657
+ diff_file = config_diff if success and config_diff else None
658
+ if active_logger:
659
+ if diff_file:
660
+ active_logger.info(f"Using diff file from CLI config: {diff_file}")
661
+ else:
662
+ active_logger.info("No diff file found in CLI config")
663
+ else:
664
+ # --diff flag was used with an explicit path
665
+ diff_file = args.diff
666
+ if active_logger:
667
+ active_logger.info(f"Using explicitly provided diff file: {diff_file}")
668
+
669
+ # Get diff content
670
+ diff_content = get_diff_content(diff_file)
671
+
672
+ if not diff_content:
673
+ print(f"{COLORS['red']}No diff content available. Exiting.{COLORS['reset']}")
674
+ return
675
+
676
+ # Log the diff content
677
+ if active_logger:
678
+ active_logger.log_diff("DEBUG", diff_content)
679
+
680
+ # Process context if provided
681
+ context_data = None
682
+ if args.message_context:
683
+ context_data = process_context(args.message_context)
684
+ if active_logger:
685
+ active_logger.debug(f"Processed context: {context_data}")
686
+ active_logger.log_content("DEBUG", "CONTEXT_DATA", str(context_data))
687
+
688
+ # Create system prompt
689
+ system_prompt = create_system_prompt(context_data)
690
+
691
+ # Log system prompt
692
+ if active_logger:
693
+ active_logger.log_template("DEBUG", "SYSTEM", system_prompt)
694
+
695
+ print(f"\n{COLORS['green']}Generating commit message...{COLORS['reset']}")
696
+
697
+ # Process based on chunking options
698
+ result = None
699
+ if args.chunk_size:
700
+ chunk_size = args.chunk_size
701
+ if active_logger:
702
+ active_logger.info(f"Using chunk size: {chunk_size}")
703
+
704
+ if args.recursive_chunk:
705
+ # Use chunking with recursive processing
706
+ if active_logger:
707
+ active_logger.info(f"Using recursive chunking with max_depth: {args.max_depth}")
708
+
709
+ result = process_with_chunking(
710
+ client,
711
+ diff_content,
712
+ context_data,
713
+ chunk_size=args.chunk_size,
714
+ recursive=True,
715
+ max_depth=args.max_depth,
716
+ logger=active_logger
717
+ )
718
+ else:
719
+ # Direct processing without chunking
720
+ if active_logger:
721
+ active_logger.info("Processing without chunking")
722
+
723
+ prompt = create_final_prompt(diff_content)
724
+
725
+ # Log final template
726
+ if active_logger:
727
+ active_logger.log_template("DEBUG", "DIRECT_PROCESSING", prompt)
728
+
729
+ result = handle_api_call(client, prompt, system_prompt, active_logger)
730
+
731
+ if not result:
732
+ print(f"{COLORS['red']}Failed to generate commit message.{COLORS['reset']}")
733
+ return
734
+
735
+ # Display the result
736
+ print(f"\n{COLORS['green']}✨ Generated Commit Message:{COLORS['reset']}\n")
737
+ print(result)
738
+
739
+ # Log the result
740
+ if active_logger:
741
+ active_logger.info("Generated commit message successfully")
742
+ active_logger.log_content("INFO", "FINAL_COMMIT_MESSAGE", result)
743
+
744
+ # Try to copy to clipboard
745
+ try:
746
+ import pyperclip
747
+ pyperclip.copy(result)
748
+ print(f"\n{COLORS['green']}(Copied to clipboard){COLORS['reset']}")
749
+ if active_logger:
750
+ active_logger.info("Commit message copied to clipboard")
751
+ except ImportError:
752
+ if active_logger:
753
+ active_logger.debug("pyperclip not available, couldn't copy to clipboard")
754
+
755
+ except Exception as e:
756
+ print(f"{COLORS['red']}Error: {str(e)}{COLORS['reset']}")
757
+ if active_logger:
758
+ active_logger.error(f"Error in gitcommsg mode: {str(e)}", exc_info=True)
ngpt/utils/cli_config.py CHANGED
@@ -19,6 +19,12 @@ CLI_CONFIG_OPTIONS = {
19
19
  "renderer": {"type": "str", "default": "auto", "context": ["all"]},
20
20
  "config-index": {"type": "int", "default": 0, "context": ["all"], "exclusive": ["provider"]},
21
21
  "web-search": {"type": "bool", "default": False, "context": ["all"]},
22
+ # GitCommit message options
23
+ "message-context": {"type": "str", "default": None, "context": ["gitcommsg"]},
24
+ "recursive-chunk": {"type": "bool", "default": False, "context": ["gitcommsg"]},
25
+ "diff": {"type": "str", "default": None, "context": ["gitcommsg"]},
26
+ "chunk-size": {"type": "int", "default": 200, "context": ["gitcommsg"]},
27
+ "max-depth": {"type": "int", "default": 3, "context": ["gitcommsg"]},
22
28
  }
23
29
 
24
30
  def get_cli_config_dir() -> Path:
ngpt/utils/log.py CHANGED
@@ -1,14 +1,24 @@
1
1
  import os
2
2
  import sys
3
3
  import datetime
4
+ import logging
5
+ import tempfile
4
6
  from pathlib import Path
5
- from typing import Optional, TextIO, Dict, Any
7
+ from typing import Optional, TextIO, Dict, Any, Union
6
8
 
7
- # Simple color definitions for fallback message
9
+ # Define colors locally to avoid circular imports
8
10
  COLORS = {
9
- "green": "\033[32m",
11
+ "reset": "\033[0m",
12
+ "bold": "\033[1m",
13
+ "cyan": "\033[36m",
14
+ "green": "\033[32m",
10
15
  "yellow": "\033[33m",
11
- "reset": "\033[0m"
16
+ "red": "\033[31m",
17
+ "blue": "\033[34m",
18
+ "magenta": "\033[35m",
19
+ "gray": "\033[90m",
20
+ "bg_blue": "\033[44m",
21
+ "bg_cyan": "\033[46m"
12
22
  }
13
23
 
14
24
  class Logger:
@@ -165,6 +175,281 @@ class Logger:
165
175
  """
166
176
  return self.is_temp
167
177
 
178
+ # Add standard logging methods to be compatible with gitcommsg mode
179
+ def info(self, message: str):
180
+ """Log an info message."""
181
+ self.log("INFO", message)
182
+
183
+ def debug(self, message: str):
184
+ """Log a debug message."""
185
+ self.log("DEBUG", message)
186
+
187
+ def warning(self, message: str):
188
+ """Log a warning message."""
189
+ self.log("WARNING", message)
190
+
191
+ def error(self, message: str, exc_info=False):
192
+ """Log an error message."""
193
+ if exc_info:
194
+ import traceback
195
+ message += "\n" + traceback.format_exc()
196
+ self.log("ERROR", message)
197
+
198
+
199
+ class GitCommsgLogger:
200
+ """Specialized logger for gitcommsg mode with standard logging methods."""
201
+
202
+ def __init__(self, log_path: Optional[str] = None):
203
+ """
204
+ Initialize the gitcommsg logger.
205
+
206
+ Args:
207
+ log_path: Optional path to the log file. If None, a temporary file will be created.
208
+ """
209
+ self.log_path = log_path
210
+ self.logger = None
211
+ self.is_temp = False
212
+ self.command_args = sys.argv
213
+
214
+ # Create a temporary log file if no path provided
215
+ if self.log_path is True or self.log_path is None:
216
+ timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
217
+ if sys.platform == "win32":
218
+ temp_dir = os.environ.get("TEMP", "")
219
+ self.log_path = os.path.join(temp_dir, f"ngpt_gitcommsg_{timestamp}.log")
220
+ else:
221
+ self.log_path = f"/tmp/ngpt_gitcommsg_{timestamp}.log"
222
+ self.is_temp = True
223
+
224
+ def setup(self):
225
+ """Set up the logger."""
226
+ # Set up Python's standard logging module
227
+ self.logger = logging.getLogger("gitcommsg")
228
+ self.logger.setLevel(logging.DEBUG)
229
+
230
+ # Clear any existing handlers
231
+ if self.logger.handlers:
232
+ for handler in self.logger.handlers:
233
+ self.logger.removeHandler(handler)
234
+
235
+ # Create file handler
236
+ try:
237
+ # Ensure the directory exists
238
+ log_dir = os.path.dirname(self.log_path)
239
+ if log_dir and not os.path.exists(log_dir):
240
+ os.makedirs(log_dir, exist_ok=True)
241
+
242
+ file_handler = logging.FileHandler(self.log_path)
243
+ file_handler.setLevel(logging.DEBUG)
244
+
245
+ # Create formatter
246
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
247
+ file_handler.setFormatter(formatter)
248
+
249
+ # Add handler to logger
250
+ self.logger.addHandler(file_handler)
251
+
252
+ print(f"{COLORS['green']}Logging enabled. Log file: {self.log_path}{COLORS['reset']}")
253
+ self.logger.info("GitCommitMsg mode started")
254
+ self.logger.info(f"Command: {' '.join(self.command_args)}")
255
+
256
+ return True
257
+ except Exception as e:
258
+ print(f"{COLORS['yellow']}Error setting up logger: {str(e)}{COLORS['reset']}")
259
+ return False
260
+
261
+ def info(self, message: str):
262
+ """Log an info message."""
263
+ if self.logger:
264
+ self.logger.info(message)
265
+
266
+ def debug(self, message: str):
267
+ """Log a debug message."""
268
+ if self.logger:
269
+ self.logger.debug(message)
270
+
271
+ def warning(self, message: str):
272
+ """Log a warning message."""
273
+ if self.logger:
274
+ self.logger.warning(message)
275
+
276
+ def error(self, message: str, exc_info=False):
277
+ """Log an error message."""
278
+ if self.logger:
279
+ self.logger.error(message, exc_info=exc_info)
280
+
281
+ def get_log_path(self) -> str:
282
+ """
283
+ Get the path to the log file.
284
+
285
+ Returns:
286
+ str: Path to the log file
287
+ """
288
+ return self.log_path
289
+
290
+ def is_temporary(self) -> bool:
291
+ """
292
+ Check if the log file is temporary.
293
+
294
+ Returns:
295
+ bool: True if the log file is temporary
296
+ """
297
+ return self.is_temp
298
+
299
+ def log_file_contents(self, level: str, description: str, filepath: str):
300
+ """
301
+ Log the contents of a file.
302
+
303
+ Args:
304
+ level: Log level (DEBUG, INFO, etc.)
305
+ description: Description of the file contents
306
+ filepath: Path to the file
307
+ """
308
+ if not self.logger or not os.path.isfile(filepath):
309
+ return
310
+
311
+ try:
312
+ # Start marker with description
313
+ self.logger.log(
314
+ getattr(logging, level.upper()),
315
+ f"===== BEGIN {description}: {filepath} ====="
316
+ )
317
+
318
+ # Read and log file contents
319
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
320
+ content = f.read()
321
+ for line in content.split('\n'):
322
+ self.logger.log(
323
+ getattr(logging, level.upper()),
324
+ line
325
+ )
326
+
327
+ # End marker
328
+ self.logger.log(
329
+ getattr(logging, level.upper()),
330
+ f"===== END {description}: {filepath} ====="
331
+ )
332
+ except Exception as e:
333
+ self.logger.error(f"Error logging file contents: {str(e)}")
334
+
335
+ def log_content(self, level: str, description: str, content: str):
336
+ """
337
+ Log the provided content.
338
+
339
+ Args:
340
+ level: Log level (DEBUG, INFO, etc.)
341
+ description: Description of the content
342
+ content: The content to log
343
+ """
344
+ if not self.logger:
345
+ return
346
+
347
+ try:
348
+ # Create a temporary file for the content
349
+ fd, temp_path = tempfile.mkstemp(prefix="ngpt_log_", suffix=".txt")
350
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
351
+ f.write(content)
352
+
353
+ # Log the content from the file
354
+ self.log_file_contents(level, description, temp_path)
355
+
356
+ # Clean up the temporary file
357
+ try:
358
+ os.unlink(temp_path)
359
+ except Exception:
360
+ pass
361
+ except Exception as e:
362
+ if self.logger:
363
+ self.logger.error(f"Error logging content: {str(e)}")
364
+
365
+ def log_prompt(self, level: str, system_prompt: Optional[str], user_prompt: str):
366
+ """
367
+ Log AI prompt information with detailed content.
368
+
369
+ Args:
370
+ level: Log level (DEBUG, INFO, etc.)
371
+ system_prompt: Optional system prompt
372
+ user_prompt: User prompt
373
+ """
374
+ if not self.logger:
375
+ return
376
+
377
+ # Log summary
378
+ self.logger.log(
379
+ getattr(logging, level.upper()),
380
+ "AI Request:"
381
+ )
382
+
383
+ # Log system prompt if provided
384
+ if system_prompt:
385
+ self.log_content(level, "SYSTEM_PROMPT", system_prompt)
386
+
387
+ # Log user prompt
388
+ self.log_content(level, "USER_PROMPT", user_prompt)
389
+
390
+ def log_response(self, level: str, response: str):
391
+ """
392
+ Log AI response with full content.
393
+
394
+ Args:
395
+ level: Log level (DEBUG, INFO, etc.)
396
+ response: The AI response
397
+ """
398
+ if not self.logger:
399
+ return
400
+
401
+ # Log response
402
+ self.log_content(level, "AI_RESPONSE", response)
403
+
404
+ def log_diff(self, level: str, diff_content: str):
405
+ """
406
+ Log git diff content.
407
+
408
+ Args:
409
+ level: Log level (DEBUG, INFO, etc.)
410
+ diff_content: The git diff content
411
+ """
412
+ if not self.logger:
413
+ return
414
+
415
+ # Log diff content
416
+ self.log_content(level, "GIT_DIFF", diff_content)
417
+
418
+ def log_chunks(self, level: str, chunk_number: int, total_chunks: int, chunk_content: str):
419
+ """
420
+ Log chunk content for processing.
421
+
422
+ Args:
423
+ level: Log level (DEBUG, INFO, etc.)
424
+ chunk_number: Current chunk number
425
+ total_chunks: Total number of chunks
426
+ chunk_content: Content of the chunk
427
+ """
428
+ if not self.logger:
429
+ return
430
+
431
+ # Log chunk content
432
+ self.log_content(
433
+ level,
434
+ f"CHUNK_{chunk_number}_OF_{total_chunks}",
435
+ chunk_content
436
+ )
437
+
438
+ def log_template(self, level: str, template_type: str, template: str):
439
+ """
440
+ Log prompt template.
441
+
442
+ Args:
443
+ level: Log level (DEBUG, INFO, etc.)
444
+ template_type: Type of template (INITIAL, CHUNK, COMBINE, etc.)
445
+ template: Template content
446
+ """
447
+ if not self.logger:
448
+ return
449
+
450
+ # Log template
451
+ self.log_content(level, f"{template_type}_TEMPLATE", template)
452
+
168
453
 
169
454
  def create_logger(log_path: Optional[str] = None) -> Logger:
170
455
  """
@@ -176,4 +461,19 @@ def create_logger(log_path: Optional[str] = None) -> Logger:
176
461
  Returns:
177
462
  Logger: Logger instance
178
463
  """
179
- return Logger(log_path)
464
+ return Logger(log_path)
465
+
466
+
467
+ def create_gitcommsg_logger(log_path: Optional[str] = None) -> GitCommsgLogger:
468
+ """
469
+ Create a gitcommsg logger instance.
470
+
471
+ Args:
472
+ log_path: Optional path to the log file
473
+
474
+ Returns:
475
+ GitCommsgLogger: GitCommsgLogger instance
476
+ """
477
+ logger = GitCommsgLogger(log_path)
478
+ logger.setup()
479
+ return logger
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ngpt
3
- Version: 2.13.0
3
+ Version: 2.14.1
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
@@ -110,6 +110,24 @@ echo "your text" | ngpt --rewrite
110
110
  # Rewrite text from a command-line argument
111
111
  ngpt --rewrite "your text to rewrite"
112
112
 
113
+ # Rewrite text from a file
114
+ cat file.txt | ngpt --rewrite
115
+
116
+ # Generate AI-powered git commit messages for staged changes
117
+ ngpt --gitcommsg
118
+
119
+ # Generate commit message with context
120
+ ngpt --gitcommsg -m "type:feat"
121
+
122
+ # Process large diffs in chunks with recursive analysis
123
+ ngpt --gitcommsg -r
124
+
125
+ # Process a diff file instead of staged changes
126
+ ngpt --gitcommsg --diff /path/to/changes.diff
127
+
128
+ # Generate a commit message with logging for debugging
129
+ ngpt --gitcommsg --log commit_log.txt
130
+
113
131
  # Use interactive multiline editor to enter text to rewrite
114
132
  ngpt --rewrite
115
133
 
@@ -156,6 +174,7 @@ For more examples and detailed usage, visit the [CLI Usage Guide](https://nazdri
156
174
  - 🧠 **Text Rewriting**: Improve text quality while maintaining original tone and meaning
157
175
  - 🧩 **Clean Code Generation**: Output code without markdown or explanations
158
176
  - 📝 **Rich Multiline Editor**: Interactive multiline text input with syntax highlighting and intuitive controls
177
+ - 📑 **Git Commit Messages**: AI-powered generation of conventional, detailed commit messages from git diffs
159
178
  - 🎭 **System Prompts**: Customize model behavior with custom system prompts
160
179
  - 📃 **Conversation Logging**: Save your conversations to text files for later reference
161
180
  - 🧰 **CLI Components**: Reusable components for building custom AI-powered command-line tools
@@ -2,25 +2,26 @@ ngpt/__init__.py,sha256=kpKhViLakwMdHZkuLht2vWcjt0uD_5gR33gvMhfXr6w,664
2
2
  ngpt/__main__.py,sha256=j3eFYPOtCCFBOGh7NK5IWEnADnTMMSEB9GLyIDoW724,66
3
3
  ngpt/client.py,sha256=rLgDPmJe8_yi13-XUiHJ45z54rJVrupxWmeb-fQZGF4,15129
4
4
  ngpt/cli/__init__.py,sha256=hebbDSMGiOd43YNnQP67uzr67Ue6rZPwm2czynr5iZY,43
5
- ngpt/cli/args.py,sha256=1yuVgmBXPu3HrCkVtWBfJG5he90ij84UjPbY11TppPk,9965
5
+ ngpt/cli/args.py,sha256=fTX3ozqMT5fCcxlyrPQEEHeWxSpgDvdWSM4AtdIAIE8,11152
6
6
  ngpt/cli/config_manager.py,sha256=NQQcWnjUppAAd0s0p9YAf8EyKS1ex5-0EB4DvKdB4dk,3662
7
7
  ngpt/cli/formatters.py,sha256=HBYGlx_7eoAKyzfy0Vq5L0yn8yVKjngqYBukMmXCcz0,9401
8
8
  ngpt/cli/interactive.py,sha256=DZFbExcXd7RylkpBiZBhiI6N8FBaT0m_lBes0Pvhi48,10894
9
- ngpt/cli/main.py,sha256=E51XQFIT2NGC1nV1FTQ0GAy-Q6fm730vDXhArqJDwRU,28019
9
+ ngpt/cli/main.py,sha256=6GO4r9e9su7FFukj9JeVmJt1bJsqPOJBj6xo3iyMZXU,28911
10
10
  ngpt/cli/renderers.py,sha256=gJ3WdVvCGkNxrLEkLCh6gk9HBFMK8y7an6CsEkqt2Z8,10535
11
11
  ngpt/cli/ui.py,sha256=iMinm_QdsmwrEUpb7CBRexyyBqf4sviFI9M3E8D-hhA,5303
12
- ngpt/cli/modes/__init__.py,sha256=i1ZKe7QsKkQ1g8Ip-NmyltZRQQI6nwXRq35bsmER8BU,229
12
+ ngpt/cli/modes/__init__.py,sha256=R3aO662RIzWEOvr3moTrEI8Tpg0zDDyMGGh1-OxiRgM,285
13
13
  ngpt/cli/modes/chat.py,sha256=4a5EgM_5A1zCSrLrjgQMDnBwIHd1Rnu5_BjSKSm7p24,4255
14
14
  ngpt/cli/modes/code.py,sha256=RjOAj7BDO5vLUdIPkUfPtyIkI_W6qEHsZvYh-sIdVaM,4293
15
+ ngpt/cli/modes/gitcommsg.py,sha256=eme_E1JFOndh1yWP90ryYcrsx6K5VLQqPoTIJDKwCsQ,28164
15
16
  ngpt/cli/modes/rewrite.py,sha256=Zb0PFvWRKXs4xJCF3GEdYc-LSmy6qRszz8-QJuldHc0,8595
16
17
  ngpt/cli/modes/shell.py,sha256=lF9f7w-0bl_FdZl-WJnZuV736BKrWQtrwoKr3ejPXFE,2682
17
18
  ngpt/cli/modes/text.py,sha256=ncYnfLFMdTPuHiOvAaHNiOWhox6GF6S-2fTwMIrAz-g,3140
18
19
  ngpt/utils/__init__.py,sha256=E46suk2-QgYBI0Qrs6WXOajOUOebF3ETAFY7ah8DTWs,942
19
- ngpt/utils/cli_config.py,sha256=A1TgO1rkRs6zgfNwIw5v8-N5jxqrHMZ4B6r3w48egi8,10687
20
+ ngpt/utils/cli_config.py,sha256=b7cXTxbRA-tQWgaehP_uRm_L8-677elPUXk290uzsTs,11110
20
21
  ngpt/utils/config.py,sha256=WYOk_b1eiYjo6hpV3pfXr2RjqhOnmKqwZwKid1T41I4,10363
21
- ngpt/utils/log.py,sha256=3AJiry9vUbo9Rzzrgj6-NMM4lCbgoZhrGcXvdxMqtrs,6353
22
- ngpt-2.13.0.dist-info/METADATA,sha256=KseQZjNtDa7N5uJVZ0XFThyLh7fawca-6mEUHFzMcjY,21987
23
- ngpt-2.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
- ngpt-2.13.0.dist-info/entry_points.txt,sha256=SqAAvLhMrsEpkIr4YFRdUeyuXQ9o0IBCeYgE6AVojoI,44
25
- ngpt-2.13.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
26
- ngpt-2.13.0.dist-info/RECORD,,
22
+ ngpt/utils/log.py,sha256=f1jg2iFo35PAmsarH8FVL_62plq4VXH0Mu2QiP6RJGw,15934
23
+ ngpt-2.14.1.dist-info/METADATA,sha256=MRsUrLvG2mRWtZ7ZxhbTzf1GQ43gY9E_LjVIPIr_NSU,22573
24
+ ngpt-2.14.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ ngpt-2.14.1.dist-info/entry_points.txt,sha256=SqAAvLhMrsEpkIr4YFRdUeyuXQ9o0IBCeYgE6AVojoI,44
26
+ ngpt-2.14.1.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
27
+ ngpt-2.14.1.dist-info/RECORD,,
File without changes