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 +15 -0
- ngpt/cli/main.py +20 -10
- ngpt/cli/modes/__init__.py +2 -1
- ngpt/cli/modes/gitcommsg.py +730 -0
- ngpt/utils/cli_config.py +6 -0
- ngpt/utils/log.py +305 -5
- {ngpt-2.13.0.dist-info → ngpt-2.14.0.dist-info}/METADATA +20 -1
- {ngpt-2.13.0.dist-info → ngpt-2.14.0.dist-info}/RECORD +11 -10
- {ngpt-2.13.0.dist-info → ngpt-2.14.0.dist-info}/WHEEL +0 -0
- {ngpt-2.13.0.dist-info → ngpt-2.14.0.dist-info}/entry_points.txt +0 -0
- {ngpt-2.13.0.dist-info → ngpt-2.14.0.dist-info}/licenses/LICENSE +0 -0
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
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
logger
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
ngpt/cli/modes/__init__.py
CHANGED
@@ -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
|
-
#
|
9
|
+
# Define colors locally to avoid circular imports
|
8
10
|
COLORS = {
|
9
|
-
"
|
11
|
+
"reset": "\033[0m",
|
12
|
+
"bold": "\033[1m",
|
13
|
+
"cyan": "\033[36m",
|
14
|
+
"green": "\033[32m",
|
10
15
|
"yellow": "\033[33m",
|
11
|
-
"
|
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.
|
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=
|
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=
|
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=
|
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=
|
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=
|
22
|
-
ngpt-2.
|
23
|
-
ngpt-2.
|
24
|
-
ngpt-2.
|
25
|
-
ngpt-2.
|
26
|
-
ngpt-2.
|
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
|
File without changes
|
File without changes
|