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.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- 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>>></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}"
|