ai-coding-assistant 0.5.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.
Files changed (89) hide show
  1. ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
  2. ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
  3. ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
  4. ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
  5. ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
  6. coding_assistant/__init__.py +3 -0
  7. coding_assistant/__main__.py +19 -0
  8. coding_assistant/cli/__init__.py +1 -0
  9. coding_assistant/cli/app.py +158 -0
  10. coding_assistant/cli/commands/__init__.py +19 -0
  11. coding_assistant/cli/commands/ask.py +178 -0
  12. coding_assistant/cli/commands/config.py +438 -0
  13. coding_assistant/cli/commands/diagram.py +267 -0
  14. coding_assistant/cli/commands/document.py +410 -0
  15. coding_assistant/cli/commands/explain.py +192 -0
  16. coding_assistant/cli/commands/fix.py +249 -0
  17. coding_assistant/cli/commands/index.py +162 -0
  18. coding_assistant/cli/commands/refactor.py +245 -0
  19. coding_assistant/cli/commands/search.py +182 -0
  20. coding_assistant/cli/commands/serve_docs.py +128 -0
  21. coding_assistant/cli/repl.py +381 -0
  22. coding_assistant/cli/theme.py +90 -0
  23. coding_assistant/codebase/__init__.py +1 -0
  24. coding_assistant/codebase/crawler.py +93 -0
  25. coding_assistant/codebase/parser.py +266 -0
  26. coding_assistant/config/__init__.py +25 -0
  27. coding_assistant/config/config_manager.py +615 -0
  28. coding_assistant/config/settings.py +82 -0
  29. coding_assistant/context/__init__.py +19 -0
  30. coding_assistant/context/chunker.py +443 -0
  31. coding_assistant/context/enhanced_retriever.py +322 -0
  32. coding_assistant/context/hybrid_search.py +311 -0
  33. coding_assistant/context/ranker.py +355 -0
  34. coding_assistant/context/retriever.py +119 -0
  35. coding_assistant/context/window.py +362 -0
  36. coding_assistant/documentation/__init__.py +23 -0
  37. coding_assistant/documentation/agents/__init__.py +27 -0
  38. coding_assistant/documentation/agents/coordinator.py +510 -0
  39. coding_assistant/documentation/agents/module_documenter.py +111 -0
  40. coding_assistant/documentation/agents/synthesizer.py +139 -0
  41. coding_assistant/documentation/agents/task_delegator.py +100 -0
  42. coding_assistant/documentation/decomposition/__init__.py +21 -0
  43. coding_assistant/documentation/decomposition/context_preserver.py +477 -0
  44. coding_assistant/documentation/decomposition/module_detector.py +302 -0
  45. coding_assistant/documentation/decomposition/partitioner.py +621 -0
  46. coding_assistant/documentation/generators/__init__.py +14 -0
  47. coding_assistant/documentation/generators/dataflow_generator.py +440 -0
  48. coding_assistant/documentation/generators/diagram_generator.py +511 -0
  49. coding_assistant/documentation/graph/__init__.py +13 -0
  50. coding_assistant/documentation/graph/dependency_builder.py +468 -0
  51. coding_assistant/documentation/graph/module_analyzer.py +475 -0
  52. coding_assistant/documentation/writers/__init__.py +11 -0
  53. coding_assistant/documentation/writers/markdown_writer.py +322 -0
  54. coding_assistant/embeddings/__init__.py +0 -0
  55. coding_assistant/embeddings/generator.py +89 -0
  56. coding_assistant/embeddings/store.py +187 -0
  57. coding_assistant/exceptions/__init__.py +50 -0
  58. coding_assistant/exceptions/base.py +110 -0
  59. coding_assistant/exceptions/llm.py +249 -0
  60. coding_assistant/exceptions/recovery.py +263 -0
  61. coding_assistant/exceptions/storage.py +213 -0
  62. coding_assistant/exceptions/validation.py +230 -0
  63. coding_assistant/llm/__init__.py +1 -0
  64. coding_assistant/llm/client.py +277 -0
  65. coding_assistant/llm/gemini_client.py +181 -0
  66. coding_assistant/llm/groq_client.py +160 -0
  67. coding_assistant/llm/prompts.py +98 -0
  68. coding_assistant/llm/together_client.py +160 -0
  69. coding_assistant/operations/__init__.py +13 -0
  70. coding_assistant/operations/differ.py +369 -0
  71. coding_assistant/operations/generator.py +347 -0
  72. coding_assistant/operations/linter.py +430 -0
  73. coding_assistant/operations/validator.py +406 -0
  74. coding_assistant/storage/__init__.py +9 -0
  75. coding_assistant/storage/database.py +363 -0
  76. coding_assistant/storage/session.py +231 -0
  77. coding_assistant/utils/__init__.py +31 -0
  78. coding_assistant/utils/cache.py +477 -0
  79. coding_assistant/utils/hardware.py +132 -0
  80. coding_assistant/utils/keystore.py +206 -0
  81. coding_assistant/utils/logger.py +32 -0
  82. coding_assistant/utils/progress.py +311 -0
  83. coding_assistant/validation/__init__.py +13 -0
  84. coding_assistant/validation/files.py +305 -0
  85. coding_assistant/validation/inputs.py +335 -0
  86. coding_assistant/validation/params.py +280 -0
  87. coding_assistant/validation/sanitizers.py +243 -0
  88. coding_assistant/vcs/__init__.py +5 -0
  89. coding_assistant/vcs/git.py +269 -0
@@ -0,0 +1,98 @@
1
+ """Prompt templates for the LLM."""
2
+ from typing import List, Dict
3
+
4
+
5
+ class PromptBuilder:
6
+ """Build prompts for different tasks."""
7
+
8
+ @staticmethod
9
+ def build_system_prompt() -> str:
10
+ """Build the system prompt for the assistant with FAANG/MAANG senior engineer capabilities."""
11
+ return """You are an expert AI coding assistant with the capabilities of a FAANG/MAANG Senior Software Engineer (L5/E5/SDE III level).
12
+
13
+ Your Core Capabilities:
14
+
15
+ **Architecture & System Design:**
16
+ - Design scalable, performant, and reliable software architectures
17
+ - Make informed trade-offs between different architectural approaches
18
+ - Address scalability, performance, security, and maintainability concerns
19
+ - Design distributed systems and microservices
20
+ - Handle real-world architectural challenges (caching, load balancing, data consistency)
21
+
22
+ **Code Quality & Best Practices:**
23
+ - Write clean, maintainable, production-quality code
24
+ - Apply SOLID principles and design patterns appropriately
25
+ - Optimize code for performance and efficiency
26
+ - Ensure code follows industry best practices and standards
27
+ - Implement comprehensive error handling and logging
28
+
29
+ **Code Review & Mentorship:**
30
+ - Conduct thorough code reviews with constructive feedback
31
+ - Identify bugs, security vulnerabilities, and performance issues
32
+ - Suggest improvements and refactoring opportunities
33
+ - Explain complex concepts clearly for knowledge sharing
34
+ - Guide junior developers toward better solutions
35
+
36
+ **Problem Solving & Algorithms:**
37
+ - Optimize solutions using data structures and algorithms
38
+ - Analyze time and space complexity
39
+ - Solve complex technical challenges systematically
40
+ - Debug issues efficiently using structured approaches
41
+
42
+ **Technical Expertise:**
43
+ - Deep knowledge of software development tools and processes
44
+ - Experience with modern tech stacks and frameworks
45
+ - Understanding of CI/CD, testing, and deployment pipelines
46
+ - Knowledge of cloud platforms and containerization
47
+ - Familiarity with databases, caching, and message queues
48
+
49
+ **Communication & Collaboration:**
50
+ - Explain technical decisions and trade-offs clearly
51
+ - Document code and architectural decisions
52
+ - Cite specific files and line numbers when referencing code
53
+ - Provide actionable, step-by-step guidance
54
+ - Ask clarifying questions when requirements are ambiguous
55
+
56
+ Guidelines:
57
+ - Use the provided codebase context to give accurate, context-aware answers
58
+ - Suggest production-ready solutions, not just quick hacks
59
+ - Consider long-term maintainability and team productivity
60
+ - Point out potential issues before they become problems
61
+ - If uncertain, acknowledge limitations rather than guessing
62
+ - Provide code examples following the project's existing patterns
63
+ - Think like a senior engineer: balance perfectionism with pragmatism"""
64
+
65
+ @staticmethod
66
+ def build_ask_prompt(question: str, file_contents: List[Dict[str, str]]) -> List[Dict[str, str]]:
67
+ """Build a prompt for the 'ask' command."""
68
+
69
+ system_prompt = """You are an expert coding assistant with access to the user's codebase.
70
+
71
+ Your capabilities:
72
+ - Explain code functionality
73
+ - Answer questions about the codebase
74
+ - Provide insights about code structure and patterns
75
+
76
+ Guidelines:
77
+ - Use the provided code context to give accurate answers
78
+ - Cite specific files when referencing code
79
+ - If you're not sure, say so rather than guessing
80
+ """
81
+
82
+ # Build context section
83
+ context = "# Codebase Context:\n\n"
84
+ if file_contents:
85
+ for file_info in file_contents:
86
+ context += f"## File: {file_info['path']}\n"
87
+ context += f"```{file_info.get('language', 'text')}\n"
88
+ context += f"{file_info['content']}\n```\n\n"
89
+ else:
90
+ context += "No files found in the current directory.\n\n"
91
+
92
+ # User question
93
+ user_message = f"{context}\n# User Question:\n{question}"
94
+
95
+ return [
96
+ {"role": "system", "content": system_prompt},
97
+ {"role": "user", "content": user_message}
98
+ ]
@@ -0,0 +1,160 @@
1
+ """Together AI LLM client implementation."""
2
+
3
+ from typing import List, Dict, Iterator, Optional
4
+ import requests
5
+ import json
6
+ from coding_assistant.llm.client import BaseLLMClient
7
+ from coding_assistant.exceptions.llm import (
8
+ LLMConnectionError,
9
+ LLMResponseError,
10
+ LLMTimeoutError
11
+ )
12
+
13
+
14
+ class TogetherClient(BaseLLMClient):
15
+ """Together AI cloud LLM client (OpenAI-compatible API)."""
16
+
17
+ def __init__(
18
+ self,
19
+ api_key: Optional[str] = None,
20
+ model: str = "Qwen/Qwen2.5-Coder-32B-Instruct",
21
+ base_url: str = "https://api.together.xyz/v1"
22
+ ):
23
+ """
24
+ Initialize Together AI client.
25
+
26
+ Args:
27
+ api_key: Together AI API key
28
+ model: Model name (default: Qwen/Qwen2.5-Coder-32B-Instruct)
29
+ base_url: API base URL
30
+ """
31
+ self.api_key = api_key
32
+ self.model = model
33
+ self.base_url = base_url
34
+
35
+ def generate(self, messages: List[Dict[str, str]], stream: bool = True) -> Iterator[str]:
36
+ """
37
+ Generate response from Together AI.
38
+
39
+ Args:
40
+ messages: List of message dicts with 'role' and 'content'
41
+ stream: Whether to stream the response
42
+
43
+ Yields:
44
+ Response chunks
45
+ """
46
+ if not self.api_key:
47
+ raise LLMConnectionError(
48
+ provider="together",
49
+ endpoint=self.base_url,
50
+ reason="API key not set"
51
+ )
52
+
53
+ headers = {
54
+ "Authorization": f"Bearer {self.api_key}",
55
+ "Content-Type": "application/json"
56
+ }
57
+
58
+ payload = {
59
+ "model": self.model,
60
+ "messages": messages,
61
+ "stream": stream
62
+ }
63
+
64
+ try:
65
+ response = requests.post(
66
+ f"{self.base_url}/chat/completions",
67
+ headers=headers,
68
+ json=payload,
69
+ stream=stream,
70
+ timeout=120
71
+ )
72
+
73
+ # Check for errors
74
+ if response.status_code == 401 or response.status_code == 403:
75
+ raise LLMResponseError(
76
+ message="Authentication failed. Check your API key.",
77
+ provider="together",
78
+ status_code=response.status_code,
79
+ response_text=response.text
80
+ )
81
+ elif response.status_code == 429:
82
+ raise LLMResponseError(
83
+ message="Rate limit exceeded. Please wait and try again.",
84
+ provider="together",
85
+ status_code=429,
86
+ response_text=response.text
87
+ )
88
+ elif response.status_code >= 400:
89
+ raise LLMResponseError(
90
+ message=f"API error: {response.status_code}",
91
+ provider="together",
92
+ status_code=response.status_code,
93
+ response_text=response.text
94
+ )
95
+
96
+ if stream:
97
+ # Parse Server-Sent Events (SSE)
98
+ for line in response.iter_lines():
99
+ if line:
100
+ line_str = line.decode('utf-8')
101
+ # SSE format: "data: {json}"
102
+ if line_str.startswith('data: '):
103
+ data_str = line_str[6:] # Remove "data: " prefix
104
+
105
+ # Check for end of stream
106
+ if data_str.strip() == '[DONE]':
107
+ break
108
+
109
+ try:
110
+ chunk = json.loads(data_str)
111
+ # Extract content from delta
112
+ if 'choices' in chunk and len(chunk['choices']) > 0:
113
+ delta = chunk['choices'][0].get('delta', {})
114
+ content = delta.get('content', '')
115
+ if content:
116
+ yield content
117
+ except json.JSONDecodeError:
118
+ continue
119
+ else:
120
+ # Non-streaming response
121
+ result = response.json()
122
+ if 'choices' in result and len(result['choices']) > 0:
123
+ content = result['choices'][0]['message']['content']
124
+ yield content
125
+
126
+ except requests.exceptions.Timeout:
127
+ raise LLMTimeoutError(
128
+ provider="together",
129
+ timeout_seconds=120
130
+ )
131
+ except requests.exceptions.ConnectionError as e:
132
+ raise LLMConnectionError(
133
+ provider="together",
134
+ endpoint=self.base_url,
135
+ reason=str(e)
136
+ )
137
+ except (LLMConnectionError, LLMResponseError, LLMTimeoutError):
138
+ # Re-raise our custom exceptions
139
+ raise
140
+ except Exception as e:
141
+ raise LLMConnectionError(
142
+ provider="together",
143
+ endpoint=self.base_url,
144
+ reason=f"Unexpected error: {str(e)}"
145
+ )
146
+
147
+ def is_available(self) -> bool:
148
+ """
149
+ Check if Together AI is available.
150
+
151
+ Returns:
152
+ True if API key is set
153
+ """
154
+ # Basic check: API key is set
155
+ if not self.api_key:
156
+ return False
157
+
158
+ # Could optionally validate with API call here
159
+ # but keeping it simple for now to avoid unnecessary API calls
160
+ return True
@@ -0,0 +1,13 @@
1
+ """Code operations module for generation, validation, and modification."""
2
+
3
+ from coding_assistant.operations.generator import CodeGenerator
4
+ from coding_assistant.operations.validator import SyntaxValidator
5
+ from coding_assistant.operations.differ import DiffGenerator
6
+ from coding_assistant.operations.linter import LinterIntegration
7
+
8
+ __all__ = [
9
+ 'CodeGenerator',
10
+ 'SyntaxValidator',
11
+ 'DiffGenerator',
12
+ 'LinterIntegration',
13
+ ]
@@ -0,0 +1,369 @@
1
+ """Generate and display code diffs."""
2
+
3
+ import difflib
4
+ from typing import List, Optional, Tuple
5
+ from pathlib import Path
6
+ from rich.console import Console
7
+ from rich.syntax import Syntax
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+
13
+ class DiffGenerator:
14
+ """Generate and display code differences."""
15
+
16
+ def __init__(self):
17
+ """Initialize the diff generator."""
18
+ self.console = Console()
19
+
20
+ def generate_diff(self, original: str, modified: str,
21
+ filepath: Optional[str] = None,
22
+ context_lines: int = 3) -> str:
23
+ """
24
+ Generate a unified diff between two code versions.
25
+
26
+ Args:
27
+ original: The original code
28
+ modified: The modified code
29
+ filepath: Optional file path for diff headers
30
+ context_lines: Number of context lines to include
31
+
32
+ Returns:
33
+ Unified diff as a string
34
+ """
35
+ original_lines = original.splitlines(keepends=True)
36
+ modified_lines = modified.splitlines(keepends=True)
37
+
38
+ filename = filepath or 'file'
39
+
40
+ diff = difflib.unified_diff(
41
+ original_lines,
42
+ modified_lines,
43
+ fromfile=f"a/{filename}",
44
+ tofile=f"b/{filename}",
45
+ lineterm='',
46
+ n=context_lines
47
+ )
48
+
49
+ return ''.join(diff)
50
+
51
+ def generate_side_by_side_diff(self, original: str, modified: str) -> List[Tuple[str, str, str]]:
52
+ """
53
+ Generate a side-by-side diff.
54
+
55
+ Args:
56
+ original: The original code
57
+ modified: The modified code
58
+
59
+ Returns:
60
+ List of (status, original_line, modified_line) tuples
61
+ status can be: 'unchanged', 'modified', 'added', 'removed'
62
+ """
63
+ original_lines = original.splitlines()
64
+ modified_lines = modified.splitlines()
65
+
66
+ matcher = difflib.SequenceMatcher(None, original_lines, modified_lines)
67
+ result = []
68
+
69
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
70
+ if tag == 'equal':
71
+ for i in range(i1, i2):
72
+ result.append(('unchanged', original_lines[i], modified_lines[j1 + (i - i1)]))
73
+ elif tag == 'replace':
74
+ # Lines were modified
75
+ for i in range(i1, i2):
76
+ old_line = original_lines[i] if i < len(original_lines) else ''
77
+ new_line = modified_lines[j1 + (i - i1)] if (j1 + (i - i1)) < len(modified_lines) else ''
78
+ result.append(('modified', old_line, new_line))
79
+ elif tag == 'delete':
80
+ for i in range(i1, i2):
81
+ result.append(('removed', original_lines[i], ''))
82
+ elif tag == 'insert':
83
+ for j in range(j1, j2):
84
+ result.append(('added', '', modified_lines[j]))
85
+
86
+ return result
87
+
88
+ def display_diff(self, diff_text: str, language: str = 'diff',
89
+ title: Optional[str] = None):
90
+ """
91
+ Display a unified diff in the terminal with syntax highlighting.
92
+
93
+ Args:
94
+ diff_text: The unified diff text
95
+ language: Language for syntax highlighting (default: diff)
96
+ title: Optional title for the diff panel
97
+ """
98
+ if not diff_text.strip():
99
+ self.console.print("[yellow]No changes detected[/yellow]")
100
+ return
101
+
102
+ syntax = Syntax(
103
+ diff_text,
104
+ 'diff',
105
+ theme='monokai',
106
+ line_numbers=False,
107
+ word_wrap=False
108
+ )
109
+
110
+ if title:
111
+ self.console.print(Panel(syntax, title=title, border_style="blue"))
112
+ else:
113
+ self.console.print(syntax)
114
+
115
+ def display_side_by_side(self, original: str, modified: str,
116
+ title: Optional[str] = None,
117
+ language: str = 'python'):
118
+ """
119
+ Display a side-by-side diff with color coding.
120
+
121
+ Args:
122
+ original: The original code
123
+ modified: The modified code
124
+ title: Optional title
125
+ language: Programming language for context
126
+ """
127
+ diff_lines = self.generate_side_by_side_diff(original, modified)
128
+
129
+ table = Table(
130
+ title=title,
131
+ show_header=True,
132
+ header_style="bold magenta",
133
+ border_style="blue",
134
+ show_lines=True
135
+ )
136
+
137
+ table.add_column("", width=3) # Status indicator
138
+ table.add_column("Line", width=5)
139
+ table.add_column("Original", style="dim")
140
+ table.add_column("Modified", style="dim")
141
+
142
+ line_num = 1
143
+ for status, old_line, new_line in diff_lines:
144
+ if status == 'unchanged':
145
+ icon = " "
146
+ style = "dim"
147
+ elif status == 'modified':
148
+ icon = "~"
149
+ style = "yellow"
150
+ elif status == 'added':
151
+ icon = "+"
152
+ style = "green"
153
+ elif status == 'removed':
154
+ icon = "-"
155
+ style = "red"
156
+
157
+ table.add_row(
158
+ f"[{style}]{icon}[/{style}]",
159
+ str(line_num),
160
+ old_line[:80], # Truncate long lines
161
+ new_line[:80],
162
+ style=style
163
+ )
164
+ line_num += 1
165
+
166
+ self.console.print(table)
167
+
168
+ def display_summary(self, diff_text: str):
169
+ """
170
+ Display a summary of changes.
171
+
172
+ Args:
173
+ diff_text: The unified diff text
174
+ """
175
+ lines = diff_text.split('\n')
176
+
177
+ added = sum(1 for line in lines if line.startswith('+') and not line.startswith('+++'))
178
+ removed = sum(1 for line in lines if line.startswith('-') and not line.startswith('---'))
179
+ modified = min(added, removed)
180
+ net_added = added - modified
181
+ net_removed = removed - modified
182
+
183
+ summary = Text()
184
+ summary.append("Changes: ", style="bold")
185
+
186
+ if modified > 0:
187
+ summary.append(f"{modified} modified ", style="yellow")
188
+ if net_added > 0:
189
+ summary.append(f"{net_added} added ", style="green")
190
+ if net_removed > 0:
191
+ summary.append(f"{net_removed} removed ", style="red")
192
+
193
+ if not any([modified, net_added, net_removed]):
194
+ summary.append("No changes", style="dim")
195
+
196
+ self.console.print(summary)
197
+
198
+ def generate_patch(self, original: str, modified: str,
199
+ filepath: str) -> str:
200
+ """
201
+ Generate a patch file that can be applied with `patch` command.
202
+
203
+ Args:
204
+ original: The original code
205
+ modified: The modified code
206
+ filepath: The file path for the patch
207
+
208
+ Returns:
209
+ Patch content as string
210
+ """
211
+ return self.generate_diff(original, modified, filepath, context_lines=3)
212
+
213
+ def apply_patch(self, original: str, patch: str) -> Optional[str]:
214
+ """
215
+ Apply a patch to original content.
216
+
217
+ Args:
218
+ original: The original code
219
+ patch: The unified diff patch
220
+
221
+ Returns:
222
+ Modified content if successful, None otherwise
223
+ """
224
+ try:
225
+ original_lines = original.splitlines(keepends=True)
226
+ patch_lines = patch.splitlines(keepends=True)
227
+
228
+ # Use difflib to apply patch
229
+ # This is a simple implementation
230
+ # For production, consider using a proper patch library
231
+
232
+ result_lines = original_lines.copy()
233
+
234
+ # Parse patch and apply
235
+ # This is simplified - full patch parsing is complex
236
+ # For now, just return None to indicate manual application needed
237
+
238
+ return None # TODO: Implement proper patch application
239
+
240
+ except Exception:
241
+ return None
242
+
243
+ def compare_files(self, file1_path: str, file2_path: str,
244
+ display: bool = True) -> str:
245
+ """
246
+ Compare two files and generate a diff.
247
+
248
+ Args:
249
+ file1_path: Path to first file
250
+ file2_path: Path to second file
251
+ display: Whether to display the diff
252
+
253
+ Returns:
254
+ Unified diff as string
255
+ """
256
+ path1 = Path(file1_path)
257
+ path2 = Path(file2_path)
258
+
259
+ if not path1.exists():
260
+ raise FileNotFoundError(f"File not found: {file1_path}")
261
+ if not path2.exists():
262
+ raise FileNotFoundError(f"File not found: {file2_path}")
263
+
264
+ content1 = path1.read_text(encoding='utf-8')
265
+ content2 = path2.read_text(encoding='utf-8')
266
+
267
+ diff = self.generate_diff(content1, content2, file1_path)
268
+
269
+ if display:
270
+ self.display_diff(diff, title=f"{file1_path} vs {file2_path}")
271
+
272
+ return diff
273
+
274
+ def get_change_stats(self, diff_text: str) -> dict:
275
+ """
276
+ Get statistics about changes in a diff.
277
+
278
+ Args:
279
+ diff_text: The unified diff text
280
+
281
+ Returns:
282
+ Dict with stats: lines_added, lines_removed, files_changed, etc.
283
+ """
284
+ lines = diff_text.split('\n')
285
+
286
+ stats = {
287
+ 'lines_added': 0,
288
+ 'lines_removed': 0,
289
+ 'lines_modified': 0,
290
+ 'files_changed': 0,
291
+ 'hunks': 0
292
+ }
293
+
294
+ for line in lines:
295
+ if line.startswith('+++'):
296
+ stats['files_changed'] += 1
297
+ elif line.startswith('@@'):
298
+ stats['hunks'] += 1
299
+ elif line.startswith('+') and not line.startswith('+++'):
300
+ stats['lines_added'] += 1
301
+ elif line.startswith('-') and not line.startswith('---'):
302
+ stats['lines_removed'] += 1
303
+
304
+ stats['lines_modified'] = min(stats['lines_added'], stats['lines_removed'])
305
+ stats['net_change'] = stats['lines_added'] - stats['lines_removed']
306
+
307
+ return stats
308
+
309
+ def highlight_changes(self, original: str, modified: str,
310
+ language: str = 'python') -> Tuple[str, str]:
311
+ """
312
+ Highlight specific character-level changes between lines.
313
+
314
+ Args:
315
+ original: Original code line
316
+ modified: Modified code line
317
+ language: Programming language
318
+
319
+ Returns:
320
+ Tuple of (highlighted_original, highlighted_modified)
321
+ """
322
+ # Use SequenceMatcher for character-level diff
323
+ matcher = difflib.SequenceMatcher(None, original, modified)
324
+
325
+ orig_highlighted = []
326
+ mod_highlighted = []
327
+
328
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
329
+ if tag == 'equal':
330
+ orig_highlighted.append(original[i1:i2])
331
+ mod_highlighted.append(modified[j1:j2])
332
+ elif tag == 'replace':
333
+ orig_highlighted.append(f"[red]{original[i1:i2]}[/red]")
334
+ mod_highlighted.append(f"[green]{modified[j1:j2]}[/green]")
335
+ elif tag == 'delete':
336
+ orig_highlighted.append(f"[red]{original[i1:i2]}[/red]")
337
+ elif tag == 'insert':
338
+ mod_highlighted.append(f"[green]{modified[j1:j2]}[/green]")
339
+
340
+ return ''.join(orig_highlighted), ''.join(mod_highlighted)
341
+
342
+ def format_diff_for_display(self, diff_text: str) -> str:
343
+ """
344
+ Format diff text for better terminal display.
345
+
346
+ Args:
347
+ diff_text: Raw unified diff
348
+
349
+ Returns:
350
+ Formatted diff with colors
351
+ """
352
+ lines = diff_text.split('\n')
353
+ formatted = []
354
+
355
+ for line in lines:
356
+ if line.startswith('+++'):
357
+ formatted.append(f"[blue]{line}[/blue]")
358
+ elif line.startswith('---'):
359
+ formatted.append(f"[blue]{line}[/blue]")
360
+ elif line.startswith('@@'):
361
+ formatted.append(f"[cyan]{line}[/cyan]")
362
+ elif line.startswith('+'):
363
+ formatted.append(f"[green]{line}[/green]")
364
+ elif line.startswith('-'):
365
+ formatted.append(f"[red]{line}[/red]")
366
+ else:
367
+ formatted.append(line)
368
+
369
+ return '\n'.join(formatted)