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,381 @@
1
+ """Interactive REPL for the coding assistant."""
2
+
3
+ from typing import List, Dict, Optional
4
+ from pathlib import Path
5
+ import sys
6
+ import random
7
+
8
+ from prompt_toolkit import PromptSession
9
+ from prompt_toolkit.history import FileHistory
10
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
11
+ from prompt_toolkit.styles import Style
12
+ from prompt_toolkit.formatted_text import HTML
13
+ from rich.console import Console
14
+ from rich.markdown import Markdown
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+
18
+ from coding_assistant.config.settings import settings
19
+ from coding_assistant.llm.client import LLMClientFactory
20
+ from coding_assistant.llm.prompts import PromptBuilder
21
+ from coding_assistant.context.enhanced_retriever import EnhancedSemanticRetriever
22
+ from coding_assistant.storage.session import SessionManager
23
+
24
+
25
+ console = Console()
26
+
27
+ # Creative loading messages
28
+ THINKING_MESSAGES = [
29
+ "🧠 Thinking...",
30
+ "🍳 Cooking up a response...",
31
+ "☕ Brewing ideas...",
32
+ "🔮 Consulting the code oracle...",
33
+ "🎯 Targeting the perfect answer...",
34
+ "🚀 Launching neurons...",
35
+ "🎨 Crafting a masterpiece...",
36
+ "🔍 Searching the knowledge base...",
37
+ "⚡ Charging up the response...",
38
+ "🎪 Juggling bits and bytes...",
39
+ "🌟 Manifesting brilliance...",
40
+ "🧪 Mixing up some wisdom...",
41
+ "🎵 Composing a reply...",
42
+ "🏗️ Building the answer...",
43
+ "🎲 Rolling for insight...",
44
+ ]
45
+
46
+
47
+ class AssistantREPL:
48
+ """Interactive REPL for multi-turn conversations."""
49
+
50
+ def __init__(self, project_path: Optional[Path] = None, persist_session: bool = True):
51
+ """Initialize REPL.
52
+
53
+ Args:
54
+ project_path: Path to project (defaults to current directory)
55
+ persist_session: Whether to persist session to database
56
+ """
57
+ self.project_path = project_path or settings.project_path
58
+
59
+ # Setup prompt session with history
60
+ history_file = self.project_path / '.assistant_history'
61
+ self.session = PromptSession(
62
+ history=FileHistory(str(history_file)),
63
+ auto_suggest=AutoSuggestFromHistory(),
64
+ )
65
+
66
+ # Chat history for context
67
+ self.chat_history: List[Dict[str, str]] = []
68
+
69
+ # Initialize components
70
+ self.llm = LLMClientFactory.create_client(settings.llm_provider)
71
+ self.prompt_builder = PromptBuilder()
72
+
73
+ # Try to initialize retriever
74
+ try:
75
+ self.retriever = EnhancedSemanticRetriever(self.project_path)
76
+ self.retriever_available = True
77
+ except Exception:
78
+ self.retriever = None
79
+ self.retriever_available = False
80
+
81
+ # Initialize session persistence
82
+ self.persist_session = persist_session
83
+ self.session_id: Optional[int] = None
84
+ if persist_session:
85
+ db_path = self.project_path / '.assistant' / 'sessions.db'
86
+ self.session_manager = SessionManager(str(db_path))
87
+ # Create new session
88
+ self.session_id = self.session_manager.create_session(
89
+ project_path=str(self.project_path),
90
+ title="Interactive Session"
91
+ )
92
+ else:
93
+ self.session_manager = None
94
+
95
+ # REPL state
96
+ self.running = True
97
+ self.context_chunks: List[Dict] = []
98
+
99
+ def get_prompt_message(self) -> HTML:
100
+ """Get formatted prompt message."""
101
+ return HTML('<ansigreen><b>assistant&gt;&gt;&gt;</b></ansigreen> ')
102
+
103
+ def run(self):
104
+ """Run the REPL loop."""
105
+ # Show welcome message
106
+ self.show_welcome()
107
+
108
+ while self.running:
109
+ try:
110
+ # Get user input
111
+ user_input = self.session.prompt(self.get_prompt_message())
112
+
113
+ # Skip empty input
114
+ if not user_input.strip():
115
+ continue
116
+
117
+ # Handle special commands
118
+ if user_input.startswith('/'):
119
+ self.handle_command(user_input)
120
+ continue
121
+
122
+ # Process regular message
123
+ self.process_message(user_input)
124
+
125
+ except KeyboardInterrupt:
126
+ console.print("\n[dim]Press Ctrl+D to exit[/dim]")
127
+ continue
128
+
129
+ except EOFError:
130
+ # Ctrl+D pressed
131
+ self.handle_exit()
132
+ break
133
+
134
+ except Exception as e:
135
+ console.print(f"\n[red]Error: {e}[/red]")
136
+ if settings.verbose:
137
+ import traceback
138
+ traceback.print_exc()
139
+
140
+ def show_welcome(self):
141
+ """Show welcome message."""
142
+ welcome_text = """[bold cyan]AI Coding Assistant - Interactive Mode[/bold cyan]
143
+
144
+ [bold]Available Commands:[/bold]
145
+ /help Show this help message
146
+ /clear Clear screen
147
+ /history Show conversation history
148
+ /context Show current context
149
+ /reset Reset conversation
150
+ /exit Exit REPL
151
+
152
+ [dim]Type your questions or requests. Press Ctrl+D to exit.[/dim]
153
+ """
154
+ console.print(Panel(welcome_text, border_style="cyan"))
155
+ console.print()
156
+
157
+ # Show retriever status
158
+ if not self.retriever_available:
159
+ console.print("[yellow]⚠️ Codebase not indexed. Run 'assistant index' for better context.[/yellow]\n")
160
+
161
+ def process_message(self, user_input: str):
162
+ """Process a user message.
163
+
164
+ Args:
165
+ user_input: User's message
166
+ """
167
+ # Add user message to history
168
+ self.chat_history.append({
169
+ 'role': 'user',
170
+ 'content': user_input
171
+ })
172
+
173
+ # Persist user message if session persistence enabled
174
+ user_message_id = None
175
+ if self.persist_session and self.session_manager and self.session_id:
176
+ user_message_id = self.session_manager.add_message(
177
+ self.session_id,
178
+ 'user',
179
+ user_input
180
+ )
181
+
182
+ # Retrieve context if available
183
+ if self.retriever_available and self.retriever:
184
+ try:
185
+ with console.status("[bold green]🔍 Searching codebase...[/bold green]", spinner="dots"):
186
+ results = self.retriever.retrieve(
187
+ query=user_input,
188
+ k=5,
189
+ use_hybrid=True,
190
+ use_ranking=True
191
+ )
192
+ self.context_chunks = results
193
+
194
+ if settings.verbose:
195
+ console.print(f"[dim]✓ Retrieved {len(results)} relevant chunks[/dim]")
196
+
197
+ except Exception as e:
198
+ if settings.verbose:
199
+ console.print(f"[yellow]Context retrieval failed: {e}[/yellow]")
200
+ self.context_chunks = []
201
+
202
+ # Build prompt with context
203
+ if self.context_chunks:
204
+ # Convert chunks to file_contents format
205
+ file_contents = []
206
+ for chunk in self.context_chunks:
207
+ file_contents.append({
208
+ 'path': f"{chunk['path']}:{chunk['start_line']}-{chunk['end_line']}",
209
+ 'content': chunk['content'],
210
+ 'language': chunk.get('language', 'python')
211
+ })
212
+
213
+ # Build prompt with context
214
+ system_msg = self.prompt_builder.build_system_prompt()
215
+ user_msg = self.prompt_builder.build_ask_prompt(user_input, file_contents)[1]['content']
216
+
217
+ messages = [
218
+ {'role': 'system', 'content': system_msg},
219
+ *self.chat_history[:-1], # Previous conversation
220
+ {'role': 'user', 'content': user_msg} # Current with context
221
+ ]
222
+ else:
223
+ # No context, just use chat history
224
+ messages = [
225
+ {'role': 'system', 'content': self.prompt_builder.build_system_prompt()},
226
+ *self.chat_history
227
+ ]
228
+
229
+ # Generate response
230
+ console.print()
231
+
232
+ # Show creative thinking message
233
+ thinking_msg = random.choice(THINKING_MESSAGES)
234
+
235
+ try:
236
+ response_text = ""
237
+ first_chunk = True
238
+ status_running = False
239
+
240
+ # Create status context
241
+ status = console.status(f"[bold cyan]{thinking_msg}[/bold cyan]", spinner="dots")
242
+ status.start()
243
+ status_running = True
244
+
245
+ try:
246
+ for chunk in self.llm.generate(messages, stream=True):
247
+ # Stop spinner on first chunk and start printing
248
+ if first_chunk:
249
+ status.stop()
250
+ status_running = False
251
+ first_chunk = False
252
+
253
+ console.print(chunk, end="")
254
+ response_text += chunk
255
+ finally:
256
+ # Ensure status is stopped
257
+ if status_running:
258
+ status.stop()
259
+
260
+ console.print("\n")
261
+
262
+ # Add assistant response to history
263
+ self.chat_history.append({
264
+ 'role': 'assistant',
265
+ 'content': response_text
266
+ })
267
+
268
+ # Persist assistant message if session persistence enabled
269
+ assistant_message_id = None
270
+ if self.persist_session and self.session_manager and self.session_id:
271
+ assistant_message_id = self.session_manager.add_message(
272
+ self.session_id,
273
+ 'assistant',
274
+ response_text
275
+ )
276
+
277
+ # Save context snapshot if we retrieved context
278
+ if self.context_chunks and assistant_message_id:
279
+ self.session_manager.save_context_snapshot(
280
+ self.session_id,
281
+ assistant_message_id,
282
+ self.context_chunks
283
+ )
284
+
285
+ except Exception as e:
286
+ console.print(f"\n[red]Error generating response: {e}[/red]\n")
287
+ # Remove user message from history since we couldn't respond
288
+ self.chat_history.pop()
289
+
290
+ def handle_command(self, command: str):
291
+ """Handle special REPL commands.
292
+
293
+ Args:
294
+ command: Command string (starts with /)
295
+ """
296
+ parts = command.split(maxsplit=1)
297
+ cmd = parts[0].lower()
298
+ args = parts[1] if len(parts) > 1 else ""
299
+
300
+ handlers = {
301
+ '/help': self.cmd_help,
302
+ '/clear': self.cmd_clear,
303
+ '/history': self.cmd_history,
304
+ '/context': self.cmd_context,
305
+ '/reset': self.cmd_reset,
306
+ '/exit': self.handle_exit,
307
+ '/quit': self.handle_exit,
308
+ }
309
+
310
+ handler = handlers.get(cmd)
311
+ if handler:
312
+ handler(args)
313
+ else:
314
+ console.print(f"[red]Unknown command: {cmd}[/red]")
315
+ console.print("[dim]Type /help for available commands[/dim]\n")
316
+
317
+ def cmd_help(self, args: str = ""):
318
+ """Show help message."""
319
+ self.show_welcome()
320
+
321
+ def cmd_clear(self, args: str = ""):
322
+ """Clear the screen."""
323
+ console.clear()
324
+ console.print("[dim]Screen cleared[/dim]\n")
325
+
326
+ def cmd_history(self, args: str = ""):
327
+ """Show conversation history."""
328
+ if not self.chat_history:
329
+ console.print("[yellow]No conversation history yet.[/yellow]\n")
330
+ return
331
+
332
+ console.print("\n[bold cyan]Conversation History[/bold cyan]\n")
333
+ for i, msg in enumerate(self.chat_history, 1):
334
+ role = msg['role']
335
+ content = msg['content'][:100] + "..." if len(msg['content']) > 100 else msg['content']
336
+
337
+ if role == 'user':
338
+ console.print(f"[bold green]{i}. You:[/bold green] {content}")
339
+ else:
340
+ console.print(f"[bold blue]{i}. Assistant:[/bold blue] {content}")
341
+ console.print()
342
+
343
+ def cmd_context(self, args: str = ""):
344
+ """Show current context chunks."""
345
+ if not self.context_chunks:
346
+ console.print("[yellow]No context retrieved yet.[/yellow]\n")
347
+ return
348
+
349
+ console.print(f"\n[bold cyan]Current Context ({len(self.context_chunks)} chunks)[/bold cyan]\n")
350
+ for i, chunk in enumerate(self.context_chunks, 1):
351
+ console.print(f"[bold]{i}. {chunk['path']}:{chunk['start_line']}-{chunk['end_line']}[/bold]")
352
+ console.print(f"[dim] Type: {chunk.get('type', 'unknown')} | Name: {chunk.get('name', '-')}[/dim]")
353
+ console.print()
354
+
355
+ def cmd_reset(self, args: str = ""):
356
+ """Reset conversation history."""
357
+ self.chat_history = []
358
+ self.context_chunks = []
359
+ console.print("[green]Conversation reset.[/green]\n")
360
+
361
+ def handle_exit(self, args: str = ""):
362
+ """Exit the REPL."""
363
+ # End session if persistence enabled
364
+ if self.persist_session and self.session_manager and self.session_id:
365
+ self.session_manager.end_session(self.session_id)
366
+ if settings.verbose:
367
+ summary = self.session_manager.get_session_summary(self.session_id)
368
+ console.print(f"\n[dim]Session saved: {summary['total_messages']} messages[/dim]")
369
+
370
+ console.print("\n[cyan]Goodbye![/cyan]\n")
371
+ self.running = False
372
+
373
+
374
+ def start_repl(project_path: Optional[Path] = None):
375
+ """Start the interactive REPL.
376
+
377
+ Args:
378
+ project_path: Path to project
379
+ """
380
+ repl = AssistantREPL(project_path)
381
+ repl.run()
@@ -0,0 +1,90 @@
1
+ """Dark theme configuration for CLI."""
2
+
3
+ from rich.theme import Theme
4
+ from rich.console import Console
5
+
6
+ # Dark theme color palette
7
+ COLORS = {
8
+ # Primary colors
9
+ "primary": "#8B5CF6", # Purple
10
+ "secondary": "#06B6D4", # Cyan
11
+ "accent": "#F59E0B", # Amber
12
+
13
+ # Status colors
14
+ "success": "#10B981", # Green
15
+ "warning": "#F59E0B", # Amber
16
+ "error": "#EF4444", # Red
17
+ "info": "#3B82F6", # Blue
18
+
19
+ # Text colors
20
+ "text": "#E5E7EB", # Light gray
21
+ "text_dim": "#9CA3AF", # Medium gray
22
+ "text_muted": "#6B7280", # Dark gray
23
+
24
+ # UI elements
25
+ "border": "#4B5563", # Gray border
26
+ "background": "#1F2937", # Dark background
27
+ "highlight": "#374151", # Slightly lighter background
28
+ }
29
+
30
+ # Rich theme
31
+ dark_theme = Theme({
32
+ # Semantic styles
33
+ "primary": f"bold {COLORS['primary']}",
34
+ "secondary": f"bold {COLORS['secondary']}",
35
+ "accent": COLORS['accent'],
36
+
37
+ # Status styles
38
+ "success": f"bold {COLORS['success']}",
39
+ "warning": f"bold {COLORS['warning']}",
40
+ "error": f"bold {COLORS['error']}",
41
+ "info": COLORS['info'],
42
+
43
+ # Component styles
44
+ "header": f"bold {COLORS['primary']}",
45
+ "subheader": f"bold {COLORS['secondary']}",
46
+ "label": f"bold {COLORS['text']}",
47
+ "value": COLORS['success'],
48
+ "dim": f"dim {COLORS['text_dim']}",
49
+ "muted": COLORS['text_muted'],
50
+
51
+ # Question/Answer styles
52
+ "question": f"bold {COLORS['primary']}",
53
+ "answer": COLORS['text'],
54
+ "code": f"{COLORS['secondary']}",
55
+
56
+ # Provider styles
57
+ "provider": f"bold {COLORS['accent']}",
58
+
59
+ # Progress styles
60
+ "spinner": COLORS['secondary'],
61
+ "progress": COLORS['primary'],
62
+ })
63
+
64
+ # Styled console
65
+ styled_console = Console(theme=dark_theme)
66
+
67
+
68
+ def get_console() -> Console:
69
+ """Get themed console instance."""
70
+ return styled_console
71
+
72
+
73
+ # Emoji/icon mapping
74
+ ICONS = {
75
+ "success": "✓",
76
+ "error": "✗",
77
+ "warning": "⚠",
78
+ "info": "ℹ",
79
+ "question": "💭",
80
+ "answer": "💡",
81
+ "code": "📝",
82
+ "search": "🔍",
83
+ "loading": "⏳",
84
+ "rocket": "🚀",
85
+ "sparkles": "✨",
86
+ "gear": "⚙",
87
+ "file": "📄",
88
+ "folder": "📁",
89
+ "link": "🔗",
90
+ }
@@ -0,0 +1 @@
1
+ """Codebase intelligence module."""
@@ -0,0 +1,93 @@
1
+ """File system crawler to discover code files."""
2
+ from pathlib import Path
3
+ from typing import List, Dict, Set
4
+ import pathspec
5
+
6
+
7
+ class CodebaseCrawler:
8
+ """Crawl the codebase and find relevant files."""
9
+
10
+ # Common patterns to ignore
11
+ DEFAULT_IGNORE_PATTERNS = [
12
+ '.git/',
13
+ '__pycache__/',
14
+ '*.pyc',
15
+ '.venv/',
16
+ 'venv/',
17
+ 'node_modules/',
18
+ '.env',
19
+ '*.log',
20
+ 'dist/',
21
+ 'build/',
22
+ '.pytest_cache/',
23
+ '.mypy_cache/',
24
+ '.ruff_cache/',
25
+ '*.egg-info/',
26
+ ]
27
+
28
+ # Code file extensions to include
29
+ CODE_EXTENSIONS = {
30
+ '.py', '.js', '.ts', '.jsx', '.tsx',
31
+ '.java', '.go', '.rs', '.c', '.cpp', '.h', '.hpp',
32
+ '.rb', '.php', '.swift', '.kt', '.scala',
33
+ '.md', '.txt', '.yaml', '.yml', '.json', '.toml'
34
+ }
35
+
36
+ def __init__(self, root_path: Path):
37
+ self.root_path = root_path.resolve()
38
+ self.gitignore_spec = self._load_gitignore()
39
+
40
+ def _load_gitignore(self) -> pathspec.PathSpec:
41
+ """Load .gitignore patterns."""
42
+ gitignore_path = self.root_path / '.gitignore'
43
+ patterns = list(self.DEFAULT_IGNORE_PATTERNS)
44
+
45
+ if gitignore_path.exists():
46
+ with open(gitignore_path, 'r') as f:
47
+ patterns.extend(line.strip() for line in f if line.strip() and not line.startswith('#'))
48
+
49
+ return pathspec.PathSpec.from_lines('gitwildmatch', patterns)
50
+
51
+ def scan(self, max_files: int = 100) -> List[Dict[str, str]]:
52
+ """Scan the codebase and return file information."""
53
+ files = []
54
+
55
+ for file_path in self.root_path.rglob('*'):
56
+ # Skip if not a file
57
+ if not file_path.is_file():
58
+ continue
59
+
60
+ # Skip if ignored by gitignore
61
+ relative_path = file_path.relative_to(self.root_path)
62
+ if self.gitignore_spec.match_file(str(relative_path)):
63
+ continue
64
+
65
+ # Skip if not a code file
66
+ if file_path.suffix not in self.CODE_EXTENSIONS:
67
+ continue
68
+
69
+ # Skip large files (> 1MB)
70
+ if file_path.stat().st_size > 1_000_000:
71
+ continue
72
+
73
+ files.append({
74
+ 'path': str(relative_path),
75
+ 'absolute_path': str(file_path),
76
+ 'extension': file_path.suffix,
77
+ 'size': file_path.stat().st_size,
78
+ })
79
+
80
+ # Limit number of files
81
+ if len(files) >= max_files:
82
+ break
83
+
84
+ return files
85
+
86
+ def read_file(self, file_path: str) -> str:
87
+ """Read a file's contents."""
88
+ try:
89
+ full_path = self.root_path / file_path
90
+ with open(full_path, 'r', encoding='utf-8') as f:
91
+ return f.read()
92
+ except Exception as e:
93
+ return f"Error reading file: {e}"