ngpt 2.13.0__py3-none-any.whl → 2.14.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/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',
98
+ help='Use diff from specified file instead of staged changes')
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():
@@ -227,15 +228,17 @@ def main():
227
228
  # Change log to True to create a temp file
228
229
  args.log = True
229
230
 
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']}")
231
+ # Skip logger initialization for gitcommsg mode as it creates its own logger
232
+ if not args.gitcommsg:
233
+ # If --log is True, it means it was used without a path value
234
+ log_path = None if args.log is True else args.log
235
+ logger = create_logger(log_path)
236
+ if logger:
237
+ logger.open()
238
+ print(f"{COLORS['green']}Logging session to: {logger.get_log_path()}{COLORS['reset']}")
239
+ # If it's a temporary log file, inform the user
240
+ if logger.is_temporary():
241
+ print(f"{COLORS['green']}Created temporary log file.{COLORS['reset']}")
239
242
 
240
243
  # Priority order for config selection:
241
244
  # 1. Command-line arguments (args.provider, args.config_index)
@@ -461,7 +464,7 @@ def main():
461
464
  return
462
465
 
463
466
  # 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):
467
+ 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
468
  # Simply use the parser's help
466
469
  parser = setup_argument_parser()
467
470
  parser.print_help()
@@ -557,6 +560,13 @@ def main():
557
560
  # Rewrite mode (process stdin)
558
561
  rewrite_mode(client, args, logger=logger)
559
562
 
563
+ elif args.gitcommsg:
564
+ # Apply CLI config for gitcommsg mode
565
+ args = apply_cli_config(args, "all")
566
+
567
+ # Git commit message generation mode
568
+ gitcommsg_mode(client, args, logger=logger)
569
+
560
570
  else:
561
571
  # Default to chat mode
562
572
  # 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,730 @@
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
+
12
+ def get_diff_content(diff_file=None):
13
+ """Get git diff content from file or git staged changes.
14
+
15
+ Args:
16
+ diff_file: Path to a diff file to use instead of git staged changes
17
+
18
+ Returns:
19
+ str: Content of the diff, or None if no diff is available
20
+ """
21
+ if diff_file:
22
+ try:
23
+ with open(diff_file, 'r') as f:
24
+ content = f.read()
25
+ return content
26
+ except Exception as e:
27
+ print(f"{COLORS['yellow']}Error reading diff file: {str(e)}{COLORS['reset']}")
28
+ return None
29
+
30
+ # No diff file specified, get staged changes from git
31
+ try:
32
+ result = subprocess.run(
33
+ ["git", "diff", "--staged"],
34
+ capture_output=True,
35
+ text=True
36
+ )
37
+
38
+ if result.returncode != 0:
39
+ raise Exception(f"Git command failed: {result.stderr}")
40
+
41
+ # Check if there are staged changes
42
+ if not result.stdout.strip():
43
+ print(f"{COLORS['yellow']}No staged changes found. Stage changes with 'git add' first.{COLORS['reset']}")
44
+ return None
45
+
46
+ return result.stdout
47
+ except Exception as e:
48
+ print(f"{COLORS['yellow']}Error getting git diff: {str(e)}{COLORS['reset']}")
49
+ return None
50
+
51
+ def split_into_chunks(content, chunk_size=200):
52
+ """Split content into chunks of specified size.
53
+
54
+ Args:
55
+ content: The content to split into chunks
56
+ chunk_size: Maximum number of lines per chunk
57
+
58
+ Returns:
59
+ list: List of content chunks
60
+ """
61
+ lines = content.splitlines()
62
+ chunks = []
63
+
64
+ for i in range(0, len(lines), chunk_size):
65
+ chunk = lines[i:i+chunk_size]
66
+ chunks.append("\n".join(chunk))
67
+
68
+ return chunks
69
+
70
+ def process_context(context):
71
+ """Process context string to extract directives and filters.
72
+
73
+ Args:
74
+ context: The context string provided with -m/--message-context
75
+
76
+ Returns:
77
+ dict: Extracted context data
78
+ """
79
+ context_data = {
80
+ "file_type_filter": None,
81
+ "commit_type": None,
82
+ "focus": None,
83
+ "exclusions": [],
84
+ "raw_context": context
85
+ }
86
+
87
+ if not context:
88
+ return context_data
89
+
90
+ # Extract commit type directive (e.g., "type:feat")
91
+ if "type:" in context:
92
+ match = re.search(r"type:(\w+)", context)
93
+ if match:
94
+ context_data["commit_type"] = match.group(1)
95
+
96
+ # Extract file type filters
97
+ file_type_keywords = ["html", "css", "javascript", "python", "js", "py", "ui", "api", "config"]
98
+ for keyword in file_type_keywords:
99
+ if keyword in context.lower():
100
+ context_data["file_type_filter"] = keyword
101
+ break
102
+
103
+ # Process focus/exclusion directives
104
+ if "focus on" in context.lower() or "only mention" in context.lower():
105
+ focus_match = re.search(r"focus(?:\s+on)?\s+(\w+)", context.lower())
106
+ if focus_match:
107
+ context_data["focus"] = focus_match.group(1)
108
+
109
+ if any(x in context.lower() for x in ["ignore", "don't include", "exclude"]):
110
+ exclusion_matches = re.findall(r"(?:ignore|don't include|exclude)\s+(\w+)", context.lower())
111
+ context_data["exclusions"] = exclusion_matches
112
+
113
+ return context_data
114
+
115
+ def create_system_prompt(context_data=None):
116
+ """Create system prompt based on context data.
117
+
118
+ Args:
119
+ context_data: The processed context data
120
+
121
+ Returns:
122
+ str: System prompt for the AI
123
+ """
124
+ 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.
125
+
126
+ FORMAT:
127
+ type[(scope)]: <concise summary> (max 50 chars)
128
+
129
+ - [type] <specific change 1> (filename:function/method/line)
130
+ - [type] <specific change 2> (filename:function/method/line)
131
+ - [type] <additional changes...>
132
+
133
+ COMMIT TYPES:
134
+ - feat: New user-facing features
135
+ - fix: Bug fixes or error corrections
136
+ - refactor: Code restructuring (no behavior change)
137
+ - style: Formatting/whitespace changes only
138
+ - docs: Documentation only
139
+ - test: Test-related changes
140
+ - perf: Performance improvements
141
+ - build: Build system changes
142
+ - ci: CI/CD pipeline changes
143
+ - chore: Routine maintenance tasks
144
+ - revert: Reverting previous changes
145
+ - add: New files without user-facing features
146
+ - remove: Removing files/code
147
+ - update: Changes to existing functionality
148
+ - security: Security-related changes
149
+ - config: Configuration changes
150
+ - ui: User interface changes
151
+ - api: API-related changes
152
+
153
+ RULES:
154
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
155
+ 2. NEVER invent or assume changes not directly visible in the code
156
+ 3. EVERY bullet point MUST reference specific files/functions/lines
157
+ 4. Include ALL significant changes (do not skip any important modifications)
158
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
159
+ 6. Keep summary line under 50 characters (mandatory)
160
+ 7. Use appropriate type tags for each change (main summary and each bullet)
161
+ 8. ONLY describe code that was actually changed
162
+ 9. Focus on technical specifics, avoid general statements
163
+ 10. Include proper technical details (method names, component identifiers, etc.)
164
+ 11. When all changes are to the same file, mention it once in the summary"""
165
+
166
+ if not context_data:
167
+ return base_prompt
168
+
169
+ # Add file type filtering instructions
170
+ if context_data.get("file_type_filter"):
171
+ file_type = context_data["file_type_filter"]
172
+ file_type_prompt = f"""
173
+
174
+ CRITICAL FILE TYPE FILTERING:
175
+ You MUST INCLUDE ONLY changes to {file_type} files or files related to {file_type}.
176
+ You MUST EXCLUDE ALL other files completely from your output.
177
+ This is a strict filter - no exceptions allowed."""
178
+ base_prompt += file_type_prompt
179
+
180
+ # Add commit type directive
181
+ if context_data.get("commit_type"):
182
+ commit_type = context_data["commit_type"]
183
+ commit_type_prompt = f"""
184
+
185
+ CRITICAL COMMIT TYPE DIRECTIVE:
186
+ You MUST use exactly "{commit_type}:" as the commit type prefix.
187
+ This takes highest priority over any other commit type you might determine.
188
+ Do not override this commit type based on your own analysis."""
189
+ base_prompt += commit_type_prompt
190
+
191
+ # Add focus/exclusion directives
192
+ if context_data.get("focus"):
193
+ focus = context_data["focus"]
194
+ focus_prompt = f"""
195
+
196
+ FOCUS DIRECTIVE:
197
+ Focus exclusively on changes related to {focus}.
198
+ Exclude everything else from your analysis."""
199
+ base_prompt += focus_prompt
200
+
201
+ if context_data.get("exclusions"):
202
+ exclusions = ", ".join(context_data["exclusions"])
203
+ exclusion_prompt = f"""
204
+
205
+ EXCLUSION DIRECTIVE:
206
+ Completely ignore and exclude any mentions of: {exclusions}."""
207
+ base_prompt += exclusion_prompt
208
+
209
+ return base_prompt
210
+
211
+ def create_chunk_prompt(chunk):
212
+ """Create prompt for processing a single diff chunk.
213
+
214
+ Args:
215
+ chunk: The diff chunk to process
216
+
217
+ Returns:
218
+ str: Prompt for the AI
219
+ """
220
+ return f"""Analyze this PARTIAL git diff and create a detailed technical summary with this EXACT format:
221
+
222
+ [FILES]: Comma-separated list of affected files with full paths
223
+
224
+ [CHANGES]:
225
+ - Technical detail 1 (include specific function/method names and line numbers)
226
+ - Technical detail 2 (be precise about exactly what code was added/modified/removed)
227
+ - Additional technical details (include ALL significant changes in this chunk)
228
+
229
+ [IMPACT]: Brief technical description of what the changes accomplish
230
+
231
+ CRITICALLY IMPORTANT: Be extremely specific with technical details.
232
+ ALWAYS identify exact function names, method names, class names, and line numbers where possible.
233
+ Use format 'filename:function_name()' or 'filename:line_number' when referencing code locations.
234
+ Be precise and factual - only describe code that actually changed.
235
+
236
+ Diff chunk:
237
+
238
+ {chunk}"""
239
+
240
+ def create_rechunk_prompt(combined_analysis, depth):
241
+ """Create prompt for re-chunking process.
242
+
243
+ Args:
244
+ combined_analysis: The combined analysis to re-chunk
245
+ depth: Current recursion depth
246
+
247
+ Returns:
248
+ str: Prompt for the AI
249
+ """
250
+ return f"""IMPORTANT: You are analyzing SUMMARIES of git changes, not raw git diff.
251
+
252
+ You are in a re-chunking process (depth: {depth}) where the input is already summarized changes.
253
+ Create a TERSE summary of these summaries focusing ONLY ON TECHNICAL CHANGES:
254
+
255
+ [CHANGES]:
256
+ - Technical change 1 (specific file and function)
257
+ - Technical change 2 (specific file and function)
258
+ - Additional relevant changes
259
+
260
+ DO NOT ask for raw git diff. These summaries are all you need to work with.
261
+ Keep your response FACTUAL and SPECIFIC to what's in the summaries.
262
+
263
+ Section to summarize:
264
+
265
+ {combined_analysis}"""
266
+
267
+ def create_combine_prompt(partial_analyses):
268
+ """Create prompt for combining partial analyses.
269
+
270
+ Args:
271
+ partial_analyses: List of partial analyses to combine
272
+
273
+ Returns:
274
+ str: Prompt for the AI
275
+ """
276
+ all_analyses = "\n\n".join(partial_analyses)
277
+
278
+ return f"""===CRITICAL INSTRUCTION===
279
+ You are working with ANALYZED SUMMARIES of git changes, NOT raw git diff.
280
+ The raw git diff has ALREADY been processed into these summaries.
281
+ DO NOT ask for or expect to see the original git diff.
282
+
283
+ TASK: Synthesize these partial analyses into a complete conventional commit message:
284
+
285
+ {all_analyses}
286
+
287
+ Create a CONVENTIONAL COMMIT MESSAGE with:
288
+ 1. First line: "type[(scope)]: brief summary" (50 chars max)
289
+ - Include scope ONLY if you are 100% confident about the affected area
290
+ - Omit scope if changes affect multiple areas or scope is unclear
291
+ 2. ⚠️ ONE BLANK LINE IS MANDATORY - NEVER SKIP THIS STEP ⚠️
292
+ - This blank line MUST be present in EVERY commit message
293
+ - The blank line separates the summary from the detailed changes
294
+ - Without this blank line, the commit message format is invalid
295
+ 3. Bullet points with specific changes, each with appropriate [type] tag
296
+ 4. Reference files in EACH bullet point with function names or line numbers
297
+
298
+ FILENAME & FUNCTION HANDLING RULES:
299
+ - Include SPECIFIC function names, method names, or line numbers when available
300
+ - Format as filename:function() or filename:line_number
301
+ - Use short relative paths for files
302
+ - Group related changes to the same file when appropriate
303
+ - Avoid breaking long filenames across lines
304
+
305
+ STRICTLY follow this format with NO EXPLANATION or additional commentary.
306
+ DO NOT mention insufficient information or ask for the original diff."""
307
+
308
+ def create_final_prompt(diff_content):
309
+ """Create prompt for direct processing without chunking.
310
+
311
+ Args:
312
+ diff_content: The full diff content
313
+
314
+ Returns:
315
+ str: Prompt for the AI
316
+ """
317
+ return f"""Analyze ONLY the exact changes in this git diff and create a precise, factual commit message.
318
+
319
+ FORMAT:
320
+ type[(scope)]: <concise summary> (max 50 chars)
321
+
322
+ - [type] <specific change 1> (filename:function/method/line)
323
+ - [type] <specific change 2> (filename:function/method/line)
324
+ - [type] <additional changes...>
325
+
326
+ RULES FOR FILENAMES:
327
+ 1. Use short relative paths when possible
328
+ 2. For multiple changes to the same file, consider grouping them
329
+ 3. Abbreviate long paths when they're repeated (e.g., 'commit.zsh' instead of full path)
330
+ 4. Avoid breaking filenames across lines
331
+ 5. Only include function names when they add clarity
332
+
333
+ COMMIT TYPES:
334
+ - feat: New user-facing features
335
+ - fix: Bug fixes or error corrections
336
+ - refactor: Code restructuring (no behavior change)
337
+ - style: Formatting/whitespace changes only
338
+ - docs: Documentation only
339
+ - test: Test-related changes
340
+ - perf: Performance improvements
341
+ - build: Build system changes
342
+ - ci: CI/CD pipeline changes
343
+ - chore: Routine maintenance tasks
344
+ - revert: Reverting previous changes
345
+ - add: New files without user-facing features
346
+ - remove: Removing files/code
347
+ - update: Changes to existing functionality
348
+ - security: Security-related changes
349
+ - config: Configuration changes
350
+ - ui: User interface changes
351
+ - api: API-related changes
352
+
353
+ RULES:
354
+ 1. BE 100% FACTUAL - Mention ONLY code explicitly shown in the diff
355
+ 2. NEVER invent or assume changes not directly visible in the code
356
+ 3. EVERY bullet point MUST reference specific files/functions/lines
357
+ 4. Include ALL significant changes (do not skip any important modifications)
358
+ 5. If unsure about a change's purpose, describe WHAT changed, not WHY
359
+ 6. Keep summary line under 50 characters (mandatory)
360
+ 7. Use appropriate type tags for each change (main summary and each bullet)
361
+ 8. ONLY describe code that was actually changed
362
+ 9. Focus on technical specifics, avoid general statements
363
+ 10. Include proper technical details (method names, component identifiers, etc.)
364
+ 11. When all changes are to the same file, mention it once in the summary
365
+
366
+ Git diff to process:
367
+
368
+ {diff_content}"""
369
+
370
+ def handle_api_call(client, prompt, system_prompt=None, logger=None, max_retries=3):
371
+ """Handle API call with retries and error handling.
372
+
373
+ Args:
374
+ client: The NGPTClient instance
375
+ prompt: The prompt to send to the API
376
+ system_prompt: Optional system prompt
377
+ logger: Optional logger instance
378
+ max_retries: Maximum number of retries on error
379
+
380
+ Returns:
381
+ str: Response from the API
382
+ """
383
+ if logger:
384
+ # Enhanced logging of full prompt and system prompt
385
+ logger.log_prompt("DEBUG", system_prompt, prompt)
386
+
387
+ retry_count = 0
388
+ wait_seconds = 5
389
+
390
+ while True:
391
+ try:
392
+ # Create messages array with system prompt if available
393
+ messages = None
394
+ if system_prompt:
395
+ messages = [
396
+ {"role": "system", "content": system_prompt},
397
+ {"role": "user", "content": prompt}
398
+ ]
399
+ else:
400
+ messages = [
401
+ {"role": "user", "content": prompt}
402
+ ]
403
+
404
+ response = client.chat(
405
+ prompt=prompt,
406
+ stream=False,
407
+ markdown_format=False,
408
+ messages=messages
409
+ )
410
+
411
+ if logger:
412
+ # Log full response
413
+ logger.log_response("DEBUG", response)
414
+
415
+ return response
416
+
417
+ except Exception as e:
418
+ retry_count += 1
419
+ error_msg = f"Error (attempt {retry_count}/{max_retries}): {str(e)}"
420
+
421
+ if logger:
422
+ logger.error(error_msg)
423
+
424
+ if retry_count >= max_retries:
425
+ raise Exception(f"Failed after {max_retries} retries: {str(e)}")
426
+
427
+ print(f"{COLORS['yellow']}{error_msg}{COLORS['reset']}")
428
+ print(f"{COLORS['yellow']}Retrying in {wait_seconds} seconds...{COLORS['reset']}")
429
+
430
+ # Create a spinner effect for waiting
431
+ spinner = "⣾⣽⣻⢿⡿⣟⣯⣷"
432
+ for _ in range(wait_seconds * 5):
433
+ for char in spinner:
434
+ sys.stdout.write(f"\r{COLORS['yellow']}Waiting... {char}{COLORS['reset']}")
435
+ sys.stdout.flush()
436
+ time.sleep(0.2)
437
+
438
+ print("\r" + " " * 20 + "\r", end="")
439
+
440
+ # Exponential backoff
441
+ wait_seconds *= 2
442
+
443
+ def process_with_chunking(client, diff_content, context_data, chunk_size=200, recursive=False, max_depth=3, logger=None):
444
+ """Process diff with chunking to handle large diffs.
445
+
446
+ Args:
447
+ client: The NGPTClient instance
448
+ diff_content: The diff content to process
449
+ context_data: The processed context data
450
+ chunk_size: Maximum number of lines per chunk
451
+ recursive: Whether to use recursive chunking
452
+ max_depth: Maximum recursion depth
453
+ logger: Optional logger instance
454
+
455
+ Returns:
456
+ str: Generated commit message
457
+ """
458
+ # Create system prompt
459
+ system_prompt = create_system_prompt(context_data)
460
+
461
+ # Log initial diff content
462
+ if logger:
463
+ logger.log_diff("DEBUG", diff_content)
464
+
465
+ # Split diff into chunks
466
+ chunks = split_into_chunks(diff_content, chunk_size)
467
+ chunk_count = len(chunks)
468
+
469
+ if logger:
470
+ logger.info(f"Processing {chunk_count} chunks of {chunk_size} lines each")
471
+
472
+ print(f"{COLORS['green']}Processing diff in {chunk_count} chunks...{COLORS['reset']}")
473
+
474
+ # Process each chunk
475
+ partial_analyses = []
476
+ for i, chunk in enumerate(chunks):
477
+ print(f"\n{COLORS['cyan']}[Chunk {i+1}/{chunk_count}]{COLORS['reset']}")
478
+
479
+ # Log chunk content
480
+ if logger:
481
+ logger.log_chunks("DEBUG", i+1, chunk_count, chunk)
482
+
483
+ # Create chunk prompt
484
+ chunk_prompt = create_chunk_prompt(chunk)
485
+
486
+ # Log chunk template
487
+ if logger:
488
+ logger.log_template("DEBUG", "CHUNK", chunk_prompt)
489
+
490
+ # Process chunk
491
+ print(f"{COLORS['yellow']}Analyzing changes...{COLORS['reset']}")
492
+ try:
493
+ result = handle_api_call(client, chunk_prompt, system_prompt, logger)
494
+ partial_analyses.append(result)
495
+ print(f"{COLORS['green']}✓ Chunk {i+1} processed{COLORS['reset']}")
496
+ except Exception as e:
497
+ print(f"{COLORS['red']}Error processing chunk {i+1}: {str(e)}{COLORS['reset']}")
498
+ if logger:
499
+ logger.error(f"Error processing chunk {i+1}: {str(e)}")
500
+ return None
501
+
502
+ # Rate limit protection between chunks
503
+ if i < chunk_count - 1:
504
+ print(f"{COLORS['yellow']}Waiting to avoid rate limits...{COLORS['reset']}")
505
+ time.sleep(5)
506
+
507
+ # Combine partial analyses
508
+ print(f"\n{COLORS['cyan']}Combining analyses from {len(partial_analyses)} chunks...{COLORS['reset']}")
509
+
510
+ # Log partial analyses
511
+ if logger:
512
+ combined_analyses = "\n\n".join(partial_analyses)
513
+ logger.log_content("DEBUG", "PARTIAL_ANALYSES", combined_analyses)
514
+
515
+ # Check if we need to use recursive chunking
516
+ combined_analyses = "\n\n".join(partial_analyses)
517
+ combined_line_count = len(combined_analyses.splitlines())
518
+
519
+ if recursive and combined_line_count > 50 and max_depth > 0:
520
+ # Use recursive chunking
521
+ return recursive_process(client, combined_analyses, context_data, max_depth, logger)
522
+ else:
523
+ # Use direct combination
524
+ combine_prompt = create_combine_prompt(partial_analyses)
525
+
526
+ # Log combine template
527
+ if logger:
528
+ logger.log_template("DEBUG", "COMBINE", combine_prompt)
529
+
530
+ try:
531
+ result = handle_api_call(client, combine_prompt, system_prompt, logger)
532
+ return result
533
+ except Exception as e:
534
+ print(f"{COLORS['red']}Error combining analyses: {str(e)}{COLORS['reset']}")
535
+ if logger:
536
+ logger.error(f"Error combining analyses: {str(e)}")
537
+ return None
538
+
539
+ def recursive_process(client, combined_analysis, context_data, max_depth, logger=None, current_depth=1):
540
+ """Process large analysis results recursively.
541
+
542
+ Args:
543
+ client: The NGPTClient instance
544
+ combined_analysis: The combined analysis to process
545
+ context_data: The processed context data
546
+ max_depth: Maximum recursion depth
547
+ logger: Optional logger instance
548
+ current_depth: Current recursion depth
549
+
550
+ Returns:
551
+ str: Generated commit message
552
+ """
553
+ system_prompt = create_system_prompt(context_data)
554
+
555
+ print(f"\n{COLORS['cyan']}Recursive chunking level {current_depth}/{max_depth}...{COLORS['reset']}")
556
+
557
+ if logger:
558
+ logger.info(f"Starting recursive chunking at depth {current_depth}/{max_depth}")
559
+ logger.debug(f"Combined analysis size: {len(combined_analysis.splitlines())} lines")
560
+ logger.log_content("DEBUG", f"COMBINED_ANALYSIS_DEPTH_{current_depth}", combined_analysis)
561
+
562
+ # Create rechunk prompt
563
+ rechunk_prompt = create_rechunk_prompt(combined_analysis, current_depth)
564
+
565
+ # Log rechunk template
566
+ if logger:
567
+ logger.log_template("DEBUG", f"RECHUNK_DEPTH_{current_depth}", rechunk_prompt)
568
+
569
+ # Process rechunk
570
+ try:
571
+ result = handle_api_call(client, rechunk_prompt, system_prompt, logger)
572
+
573
+ # Check if further recursive chunking is needed
574
+ result_line_count = len(result.splitlines())
575
+
576
+ if result_line_count > 50 and current_depth < max_depth:
577
+ # Need another level of chunking
578
+ print(f"{COLORS['yellow']}Result still too large ({result_line_count} lines), continuing recursion...{COLORS['reset']}")
579
+ if logger:
580
+ logger.info(f"Result still too large ({result_line_count} lines), depth {current_depth}/{max_depth}")
581
+
582
+ return recursive_process(client, result, context_data, max_depth, logger, current_depth + 1)
583
+ else:
584
+ # Final processing
585
+ print(f"{COLORS['green']}Recursion complete, generating final commit message...{COLORS['reset']}")
586
+
587
+ # Create final combine prompt
588
+ final_prompt = f"""Create a CONVENTIONAL COMMIT MESSAGE based on these analyzed git changes:
589
+
590
+ {result}
591
+
592
+ FORMAT:
593
+ type[(scope)]: <concise summary> (max 50 chars)
594
+
595
+ - [type] <specific change 1> (filename:function/method/line)
596
+ - [type] <specific change 2> (filename:function/method/line)
597
+ - [type] <additional changes...>
598
+
599
+ RULES:
600
+ 1. First line must be under 50 characters
601
+ 2. Include a blank line after the first line
602
+ 3. Each bullet must include specific file references
603
+ 4. BE SPECIFIC - mention technical details and function names
604
+
605
+ DO NOT include any explanation or commentary outside the commit message format."""
606
+
607
+ # Log final template
608
+ if logger:
609
+ logger.log_template("DEBUG", "FINAL", final_prompt)
610
+
611
+ return handle_api_call(client, final_prompt, system_prompt, logger)
612
+ except Exception as e:
613
+ print(f"{COLORS['red']}Error in recursive processing: {str(e)}{COLORS['reset']}")
614
+ if logger:
615
+ logger.error(f"Error in recursive processing at depth {current_depth}: {str(e)}")
616
+ return None
617
+
618
+ def gitcommsg_mode(client, args, logger=None):
619
+ """Handle the Git commit message generation mode.
620
+
621
+ Args:
622
+ client: The NGPTClient instance
623
+ args: The parsed command line arguments
624
+ logger: Optional logger instance
625
+ """
626
+ # Set up logging if requested
627
+ custom_logger = None
628
+ log_path = None
629
+
630
+ if args.log:
631
+ custom_logger = create_gitcommsg_logger(args.log)
632
+
633
+ # Use both loggers if they exist
634
+ active_logger = logger if logger else custom_logger
635
+
636
+ if active_logger:
637
+ active_logger.info("Starting gitcommsg mode")
638
+ active_logger.debug(f"Args: {args}")
639
+
640
+ try:
641
+ # Get diff content
642
+ diff_content = get_diff_content(args.diff)
643
+
644
+ if not diff_content:
645
+ print(f"{COLORS['red']}No diff content available. Exiting.{COLORS['reset']}")
646
+ return
647
+
648
+ # Log the diff content
649
+ if active_logger:
650
+ active_logger.log_diff("DEBUG", diff_content)
651
+
652
+ # Process context if provided
653
+ context_data = None
654
+ if args.message_context:
655
+ context_data = process_context(args.message_context)
656
+ if active_logger:
657
+ active_logger.debug(f"Processed context: {context_data}")
658
+ active_logger.log_content("DEBUG", "CONTEXT_DATA", str(context_data))
659
+
660
+ # Create system prompt
661
+ system_prompt = create_system_prompt(context_data)
662
+
663
+ # Log system prompt
664
+ if active_logger:
665
+ active_logger.log_template("DEBUG", "SYSTEM", system_prompt)
666
+
667
+ print(f"\n{COLORS['green']}Generating commit message...{COLORS['reset']}")
668
+
669
+ # Process based on chunking options
670
+ result = None
671
+ if args.chunk_size:
672
+ chunk_size = args.chunk_size
673
+ if active_logger:
674
+ active_logger.info(f"Using chunk size: {chunk_size}")
675
+
676
+ if args.recursive_chunk:
677
+ # Use chunking with recursive processing
678
+ if active_logger:
679
+ active_logger.info(f"Using recursive chunking with max_depth: {args.max_depth}")
680
+
681
+ result = process_with_chunking(
682
+ client,
683
+ diff_content,
684
+ context_data,
685
+ chunk_size=args.chunk_size,
686
+ recursive=True,
687
+ max_depth=args.max_depth,
688
+ logger=active_logger
689
+ )
690
+ else:
691
+ # Direct processing without chunking
692
+ if active_logger:
693
+ active_logger.info("Processing without chunking")
694
+
695
+ prompt = create_final_prompt(diff_content)
696
+
697
+ # Log final template
698
+ if active_logger:
699
+ active_logger.log_template("DEBUG", "DIRECT_PROCESSING", prompt)
700
+
701
+ result = handle_api_call(client, prompt, system_prompt, active_logger)
702
+
703
+ if not result:
704
+ print(f"{COLORS['red']}Failed to generate commit message.{COLORS['reset']}")
705
+ return
706
+
707
+ # Display the result
708
+ print(f"\n{COLORS['green']}✨ Generated Commit Message:{COLORS['reset']}\n")
709
+ print(result)
710
+
711
+ # Log the result
712
+ if active_logger:
713
+ active_logger.info("Generated commit message successfully")
714
+ active_logger.log_content("INFO", "FINAL_COMMIT_MESSAGE", result)
715
+
716
+ # Try to copy to clipboard
717
+ try:
718
+ import pyperclip
719
+ pyperclip.copy(result)
720
+ print(f"\n{COLORS['green']}(Copied to clipboard){COLORS['reset']}")
721
+ if active_logger:
722
+ active_logger.info("Commit message copied to clipboard")
723
+ except ImportError:
724
+ if active_logger:
725
+ active_logger.debug("pyperclip not available, couldn't copy to clipboard")
726
+
727
+ except Exception as e:
728
+ print(f"{COLORS['red']}Error: {str(e)}{COLORS['reset']}")
729
+ if active_logger:
730
+ 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.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
@@ -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=87b35nG7LFWPwewiICQrgilGIdD_uwqpkgo1DN3xOZY,11073
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=hFX7Nn9NaRwa6uRp09fnPDzfmbkbbWZNczSLCUZPtLU,28488
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=Su7-e2w5_3-ilgjfo_x055HFC7HROwiyT_jkb667gCM,26637
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.0.dist-info/METADATA,sha256=kvuDMT94vqncE_r5Nlw75ijljRMOBtdWRZ_p3bDsP8k,22573
24
+ ngpt-2.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ ngpt-2.14.0.dist-info/entry_points.txt,sha256=SqAAvLhMrsEpkIr4YFRdUeyuXQ9o0IBCeYgE6AVojoI,44
26
+ ngpt-2.14.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
27
+ ngpt-2.14.0.dist-info/RECORD,,
File without changes