cognify-code 0.2.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_code_assistant/__init__.py +14 -0
- ai_code_assistant/agent/__init__.py +63 -0
- ai_code_assistant/agent/code_agent.py +461 -0
- ai_code_assistant/agent/code_generator.py +388 -0
- ai_code_assistant/agent/code_reviewer.py +365 -0
- ai_code_assistant/agent/diff_engine.py +308 -0
- ai_code_assistant/agent/file_manager.py +300 -0
- ai_code_assistant/agent/intent_classifier.py +284 -0
- ai_code_assistant/chat/__init__.py +11 -0
- ai_code_assistant/chat/agent_session.py +156 -0
- ai_code_assistant/chat/session.py +165 -0
- ai_code_assistant/cli.py +1571 -0
- ai_code_assistant/config.py +149 -0
- ai_code_assistant/editor/__init__.py +8 -0
- ai_code_assistant/editor/diff_handler.py +270 -0
- ai_code_assistant/editor/file_editor.py +350 -0
- ai_code_assistant/editor/prompts.py +146 -0
- ai_code_assistant/generator/__init__.py +7 -0
- ai_code_assistant/generator/code_gen.py +265 -0
- ai_code_assistant/generator/prompts.py +114 -0
- ai_code_assistant/git/__init__.py +6 -0
- ai_code_assistant/git/commit_generator.py +130 -0
- ai_code_assistant/git/manager.py +203 -0
- ai_code_assistant/llm.py +111 -0
- ai_code_assistant/providers/__init__.py +23 -0
- ai_code_assistant/providers/base.py +124 -0
- ai_code_assistant/providers/cerebras.py +97 -0
- ai_code_assistant/providers/factory.py +148 -0
- ai_code_assistant/providers/google.py +103 -0
- ai_code_assistant/providers/groq.py +111 -0
- ai_code_assistant/providers/ollama.py +86 -0
- ai_code_assistant/providers/openai.py +114 -0
- ai_code_assistant/providers/openrouter.py +130 -0
- ai_code_assistant/py.typed +0 -0
- ai_code_assistant/refactor/__init__.py +20 -0
- ai_code_assistant/refactor/analyzer.py +189 -0
- ai_code_assistant/refactor/change_plan.py +172 -0
- ai_code_assistant/refactor/multi_file_editor.py +346 -0
- ai_code_assistant/refactor/prompts.py +175 -0
- ai_code_assistant/retrieval/__init__.py +19 -0
- ai_code_assistant/retrieval/chunker.py +215 -0
- ai_code_assistant/retrieval/indexer.py +236 -0
- ai_code_assistant/retrieval/search.py +239 -0
- ai_code_assistant/reviewer/__init__.py +7 -0
- ai_code_assistant/reviewer/analyzer.py +278 -0
- ai_code_assistant/reviewer/prompts.py +113 -0
- ai_code_assistant/utils/__init__.py +18 -0
- ai_code_assistant/utils/file_handler.py +155 -0
- ai_code_assistant/utils/formatters.py +259 -0
- cognify_code-0.2.0.dist-info/METADATA +383 -0
- cognify_code-0.2.0.dist-info/RECORD +55 -0
- cognify_code-0.2.0.dist-info/WHEEL +5 -0
- cognify_code-0.2.0.dist-info/entry_points.txt +3 -0
- cognify_code-0.2.0.dist-info/licenses/LICENSE +22 -0
- cognify_code-0.2.0.dist-info/top_level.txt +1 -0
ai_code_assistant/cli.py
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
"""Command-line interface for AI Code Assistant."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
from ai_code_assistant import __version__
|
|
13
|
+
from ai_code_assistant.config import Config, load_config, get_language_by_extension
|
|
14
|
+
from ai_code_assistant.llm import LLMManager
|
|
15
|
+
from ai_code_assistant.reviewer import CodeAnalyzer
|
|
16
|
+
from ai_code_assistant.generator import CodeGenerator
|
|
17
|
+
from ai_code_assistant.chat import ChatSession
|
|
18
|
+
from ai_code_assistant.editor import FileEditor
|
|
19
|
+
from ai_code_assistant.utils import FileHandler, get_formatter
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_components(config_path: Optional[Path] = None):
|
|
25
|
+
"""Initialize and return all components."""
|
|
26
|
+
config = load_config(config_path)
|
|
27
|
+
llm = LLMManager(config)
|
|
28
|
+
return config, llm
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
@click.version_option(version=__version__, prog_name="ai-assist")
|
|
33
|
+
@click.option("--config", "-c", type=click.Path(exists=True, path_type=Path), help="Config file path")
|
|
34
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def main(ctx, config: Optional[Path], verbose: bool):
|
|
37
|
+
"""AI Code Assistant - Review and generate code using local LLMs."""
|
|
38
|
+
ctx.ensure_object(dict)
|
|
39
|
+
ctx.obj["config_path"] = config
|
|
40
|
+
ctx.obj["verbose"] = verbose
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@main.command()
|
|
44
|
+
@click.argument("files", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
45
|
+
@click.option("--type", "-t", "review_type", default="full",
|
|
46
|
+
type=click.Choice(["full", "quick", "security"]), help="Review type")
|
|
47
|
+
@click.option("--format", "-f", "output_format", default="console",
|
|
48
|
+
type=click.Choice(["console", "markdown", "json"]), help="Output format")
|
|
49
|
+
@click.option("--output", "-o", type=click.Path(path_type=Path), help="Output file path")
|
|
50
|
+
@click.option("--recursive", "-r", is_flag=True, help="Recursively review directories")
|
|
51
|
+
@click.pass_context
|
|
52
|
+
def review(ctx, files: Tuple[Path, ...], review_type: str, output_format: str,
|
|
53
|
+
output: Optional[Path], recursive: bool):
|
|
54
|
+
"""Review code files for issues and improvements."""
|
|
55
|
+
if not files:
|
|
56
|
+
console.print("[red]Error:[/red] No files specified")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
60
|
+
analyzer = CodeAnalyzer(config, llm)
|
|
61
|
+
file_handler = FileHandler(config)
|
|
62
|
+
formatter = get_formatter(output_format, config.output.use_colors)
|
|
63
|
+
|
|
64
|
+
# Collect all files to review
|
|
65
|
+
all_files = []
|
|
66
|
+
for file_path in files:
|
|
67
|
+
if file_path.is_dir():
|
|
68
|
+
all_files.extend(file_handler.find_code_files(file_path, recursive=recursive))
|
|
69
|
+
else:
|
|
70
|
+
all_files.append(file_path)
|
|
71
|
+
|
|
72
|
+
if not all_files:
|
|
73
|
+
console.print("[yellow]No code files found to review[/yellow]")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
console.print(f"\n[bold]Reviewing {len(all_files)} file(s)...[/bold]\n")
|
|
77
|
+
|
|
78
|
+
all_output = []
|
|
79
|
+
|
|
80
|
+
with Progress(
|
|
81
|
+
SpinnerColumn(),
|
|
82
|
+
TextColumn("[progress.description]{task.description}"),
|
|
83
|
+
console=console,
|
|
84
|
+
) as progress:
|
|
85
|
+
task = progress.add_task("Analyzing...", total=len(all_files))
|
|
86
|
+
|
|
87
|
+
for file_path in all_files:
|
|
88
|
+
progress.update(task, description=f"Reviewing {file_path.name}...")
|
|
89
|
+
|
|
90
|
+
result = analyzer.review_file(file_path, review_type=review_type)
|
|
91
|
+
formatted = formatter.format_review(result)
|
|
92
|
+
all_output.append(formatted)
|
|
93
|
+
|
|
94
|
+
progress.advance(task)
|
|
95
|
+
|
|
96
|
+
# Save or display output
|
|
97
|
+
if output:
|
|
98
|
+
combined = "\n\n---\n\n".join(all_output) if output_format != "json" else all_output
|
|
99
|
+
if output_format == "json":
|
|
100
|
+
import json
|
|
101
|
+
combined = json.dumps([json.loads(o) for o in all_output], indent=2)
|
|
102
|
+
output.write_text(combined)
|
|
103
|
+
console.print(f"\n[green]Report saved to:[/green] {output}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@main.command()
|
|
107
|
+
@click.argument("description")
|
|
108
|
+
@click.option("--mode", "-m", default="generic",
|
|
109
|
+
type=click.Choice(["function", "class", "script", "test", "generic"]))
|
|
110
|
+
@click.option("--language", "-l", default="python", help="Target language")
|
|
111
|
+
@click.option("--name", "-n", help="Name for function/class")
|
|
112
|
+
@click.option("--params", "-p", help="Parameters (for function mode)")
|
|
113
|
+
@click.option("--output", "-o", type=click.Path(path_type=Path), help="Output file")
|
|
114
|
+
@click.option("--format", "-f", "output_format", default="console",
|
|
115
|
+
type=click.Choice(["console", "markdown", "json"]))
|
|
116
|
+
@click.option("--source", "-s", type=click.Path(exists=True, path_type=Path),
|
|
117
|
+
help="Source file (for test mode)")
|
|
118
|
+
@click.pass_context
|
|
119
|
+
def generate(ctx, description: str, mode: str, language: str, name: Optional[str],
|
|
120
|
+
params: Optional[str], output: Optional[Path], output_format: str,
|
|
121
|
+
source: Optional[Path]):
|
|
122
|
+
"""Generate code from natural language description."""
|
|
123
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
124
|
+
generator = CodeGenerator(config, llm)
|
|
125
|
+
formatter = get_formatter(output_format, config.output.use_colors)
|
|
126
|
+
|
|
127
|
+
console.print(f"\n[bold]Generating {mode} in {language}...[/bold]\n")
|
|
128
|
+
|
|
129
|
+
with console.status("[bold green]Generating code..."):
|
|
130
|
+
if mode == "function":
|
|
131
|
+
result = generator.generate_function(
|
|
132
|
+
description=description, name=name or "generated_function",
|
|
133
|
+
language=language, parameters=params or "",
|
|
134
|
+
)
|
|
135
|
+
elif mode == "class":
|
|
136
|
+
result = generator.generate_class(
|
|
137
|
+
description=description, name=name or "GeneratedClass", language=language,
|
|
138
|
+
)
|
|
139
|
+
elif mode == "script":
|
|
140
|
+
result = generator.generate_script(
|
|
141
|
+
description=description, requirements=[description], language=language,
|
|
142
|
+
)
|
|
143
|
+
elif mode == "test":
|
|
144
|
+
if not source:
|
|
145
|
+
console.print("[red]Error:[/red] --source required for test mode")
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
source_code = source.read_text()
|
|
148
|
+
result = generator.generate_tests(source_code=source_code, language=language)
|
|
149
|
+
else:
|
|
150
|
+
result = generator.generate(description=description, language=language)
|
|
151
|
+
|
|
152
|
+
formatted = formatter.format_generation(result)
|
|
153
|
+
|
|
154
|
+
if output and result.success:
|
|
155
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
output.write_text(result.code)
|
|
157
|
+
console.print(f"\n[green]Code saved to:[/green] {output}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@main.command()
|
|
161
|
+
@click.option("--context", "-c", multiple=True, type=click.Path(exists=True, path_type=Path),
|
|
162
|
+
help="Files to load as context")
|
|
163
|
+
@click.option("--stream/--no-stream", default=True, help="Stream responses")
|
|
164
|
+
@click.pass_context
|
|
165
|
+
def chat(ctx, context: Tuple[Path, ...], stream: bool):
|
|
166
|
+
"""Start an interactive chat session about code."""
|
|
167
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
168
|
+
session = ChatSession(config, llm)
|
|
169
|
+
|
|
170
|
+
# Load context files
|
|
171
|
+
for file_path in context:
|
|
172
|
+
if session.load_file_context(file_path):
|
|
173
|
+
console.print(f"[dim]Loaded context: {file_path}[/dim]")
|
|
174
|
+
else:
|
|
175
|
+
console.print(f"[yellow]Warning: Could not load {file_path}[/yellow]")
|
|
176
|
+
|
|
177
|
+
console.print(Panel(
|
|
178
|
+
"[bold]AI Code Assistant Chat[/bold]\n\n"
|
|
179
|
+
"Commands:\n"
|
|
180
|
+
" /load <file> - Load a file as context\n"
|
|
181
|
+
" /clear - Clear conversation history\n"
|
|
182
|
+
" /context - Show loaded context files\n"
|
|
183
|
+
" /export - Export conversation to markdown\n"
|
|
184
|
+
" /quit - Exit chat\n",
|
|
185
|
+
title="Interactive Mode",
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
while True:
|
|
189
|
+
try:
|
|
190
|
+
user_input = console.input("\n[bold cyan]You>[/bold cyan] ").strip()
|
|
191
|
+
except (KeyboardInterrupt, EOFError):
|
|
192
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if not user_input:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Handle commands
|
|
199
|
+
if user_input.startswith("/"):
|
|
200
|
+
cmd_parts = user_input[1:].split(maxsplit=1)
|
|
201
|
+
cmd = cmd_parts[0].lower()
|
|
202
|
+
arg = cmd_parts[1] if len(cmd_parts) > 1 else ""
|
|
203
|
+
|
|
204
|
+
if cmd == "quit" or cmd == "exit":
|
|
205
|
+
console.print("[dim]Goodbye![/dim]")
|
|
206
|
+
break
|
|
207
|
+
elif cmd == "clear":
|
|
208
|
+
session.clear_history()
|
|
209
|
+
console.print("[dim]History cleared[/dim]")
|
|
210
|
+
elif cmd == "context":
|
|
211
|
+
if session._code_context:
|
|
212
|
+
for name in session._code_context:
|
|
213
|
+
console.print(f" • {name}")
|
|
214
|
+
else:
|
|
215
|
+
console.print("[dim]No context files loaded[/dim]")
|
|
216
|
+
elif cmd == "load" and arg:
|
|
217
|
+
path = Path(arg)
|
|
218
|
+
if session.load_file_context(path):
|
|
219
|
+
console.print(f"[green]Loaded: {path}[/green]")
|
|
220
|
+
else:
|
|
221
|
+
console.print(f"[red]Could not load: {path}[/red]")
|
|
222
|
+
elif cmd == "export":
|
|
223
|
+
export_path = Path("chat_export.md")
|
|
224
|
+
export_path.write_text(session.export_history())
|
|
225
|
+
console.print(f"[green]Exported to: {export_path}[/green]")
|
|
226
|
+
else:
|
|
227
|
+
console.print(f"[yellow]Unknown command: {cmd}[/yellow]")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# Send message
|
|
231
|
+
console.print("\n[bold green]Assistant>[/bold green] ", end="")
|
|
232
|
+
|
|
233
|
+
if stream:
|
|
234
|
+
for chunk in session.send_message(user_input, stream=True):
|
|
235
|
+
console.print(chunk, end="")
|
|
236
|
+
console.print()
|
|
237
|
+
else:
|
|
238
|
+
response = session.send_message(user_input, stream=False)
|
|
239
|
+
console.print(response)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@main.command()
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def status(ctx):
|
|
245
|
+
"""Check Ollama connection and model status."""
|
|
246
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
247
|
+
|
|
248
|
+
console.print("\n[bold]AI Code Assistant Status[/bold]\n")
|
|
249
|
+
|
|
250
|
+
# Model info
|
|
251
|
+
info = llm.get_model_info()
|
|
252
|
+
console.print(f"Model: [cyan]{info['model']}[/cyan]")
|
|
253
|
+
console.print(f"Server: [cyan]{info['base_url']}[/cyan]")
|
|
254
|
+
console.print(f"Temperature: {info['temperature']}")
|
|
255
|
+
console.print(f"Max tokens: {info['max_tokens']}")
|
|
256
|
+
|
|
257
|
+
# Connection check
|
|
258
|
+
console.print("\n[bold]Connection Test:[/bold]")
|
|
259
|
+
with console.status("[bold yellow]Testing connection to Ollama..."):
|
|
260
|
+
if llm.check_connection():
|
|
261
|
+
console.print("[green]✓ Ollama is accessible and model is loaded[/green]")
|
|
262
|
+
else:
|
|
263
|
+
console.print("[red]✗ Could not connect to Ollama[/red]")
|
|
264
|
+
console.print("\nMake sure Ollama is running:")
|
|
265
|
+
console.print(" 1. Install Ollama: https://ollama.ai")
|
|
266
|
+
console.print(f" 2. Pull the model: ollama pull {info['model']}")
|
|
267
|
+
console.print(" 3. Start Ollama: ollama serve")
|
|
268
|
+
|
|
269
|
+
# Index status
|
|
270
|
+
console.print("\n[bold]Codebase Index:[/bold]")
|
|
271
|
+
try:
|
|
272
|
+
from ai_code_assistant.retrieval import CodebaseSearch
|
|
273
|
+
search = CodebaseSearch(root_path=str(Path.cwd()))
|
|
274
|
+
count = search._collection.count()
|
|
275
|
+
console.print(f"[green]✓ Index found with {count} chunks[/green]")
|
|
276
|
+
except FileNotFoundError:
|
|
277
|
+
console.print("[yellow]○ No index found. Run 'ai-assist index' to create one.[/yellow]")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
console.print(f"[red]✗ Index error: {e}[/red]")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@main.command()
|
|
283
|
+
@click.argument("directory", default=".", type=click.Path(exists=True, path_type=Path))
|
|
284
|
+
@click.option("--clear", is_flag=True, help="Clear existing index before indexing")
|
|
285
|
+
@click.pass_context
|
|
286
|
+
def index(ctx, directory: Path, clear: bool):
|
|
287
|
+
"""Index codebase for semantic search.
|
|
288
|
+
|
|
289
|
+
This creates a searchable index of your code that enables
|
|
290
|
+
natural language queries to find relevant code.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
ai-assist index .
|
|
294
|
+
ai-assist index ./src --clear
|
|
295
|
+
"""
|
|
296
|
+
from ai_code_assistant.retrieval import CodebaseIndexer
|
|
297
|
+
from ai_code_assistant.retrieval.indexer import IndexConfig
|
|
298
|
+
|
|
299
|
+
config = load_config(ctx.obj.get("config_path"))
|
|
300
|
+
|
|
301
|
+
# Create indexer config from app config
|
|
302
|
+
index_config = IndexConfig(
|
|
303
|
+
embedding_model=config.retrieval.embedding_model,
|
|
304
|
+
persist_directory=config.retrieval.persist_directory,
|
|
305
|
+
collection_name=config.retrieval.collection_name,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
console.print(f"\n[bold]Indexing codebase: {directory.absolute()}[/bold]\n")
|
|
309
|
+
console.print(f"Embedding model: [cyan]{index_config.embedding_model}[/cyan]")
|
|
310
|
+
console.print(f"Index location: [cyan]{index_config.persist_directory}[/cyan]\n")
|
|
311
|
+
|
|
312
|
+
indexer = CodebaseIndexer(config=index_config, root_path=str(directory.absolute()))
|
|
313
|
+
|
|
314
|
+
if clear:
|
|
315
|
+
console.print("[yellow]Clearing existing index...[/yellow]")
|
|
316
|
+
indexer.clear_index()
|
|
317
|
+
|
|
318
|
+
console.print("[bold green]Starting indexing...[/bold green]\n")
|
|
319
|
+
|
|
320
|
+
# First-time model download might take a while
|
|
321
|
+
console.print("[dim]Note: First run downloads the embedding model (~90MB)[/dim]\n")
|
|
322
|
+
|
|
323
|
+
stats = indexer.index_directory(verbose=True)
|
|
324
|
+
|
|
325
|
+
console.print(f"\n[bold green]✓ Indexing complete![/bold green]")
|
|
326
|
+
console.print(f" Files indexed: {len(stats.indexed_files)}")
|
|
327
|
+
console.print(f" Total chunks: {stats.total_chunks}")
|
|
328
|
+
if stats.skipped_files:
|
|
329
|
+
console.print(f" Files skipped: {len(stats.skipped_files)}")
|
|
330
|
+
if stats.errors:
|
|
331
|
+
console.print(f" [yellow]Errors: {len(stats.errors)}[/yellow]")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@main.command()
|
|
335
|
+
@click.argument("query")
|
|
336
|
+
@click.option("--top-k", "-k", default=5, help="Number of results to return")
|
|
337
|
+
@click.option("--file", "-f", "file_filter", help="Filter by file path")
|
|
338
|
+
@click.option("--language", "-l", "lang_filter", help="Filter by language")
|
|
339
|
+
@click.option("--format", "output_format", default="console",
|
|
340
|
+
type=click.Choice(["console", "json", "context"]))
|
|
341
|
+
@click.pass_context
|
|
342
|
+
def search(ctx, query: str, top_k: int, file_filter: Optional[str],
|
|
343
|
+
lang_filter: Optional[str], output_format: str):
|
|
344
|
+
"""Search codebase using natural language.
|
|
345
|
+
|
|
346
|
+
Find relevant code by describing what you're looking for.
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
ai-assist search "user authentication"
|
|
350
|
+
ai-assist search "database connection" -k 10
|
|
351
|
+
ai-assist search "error handling" --language python
|
|
352
|
+
ai-assist search "config loading" --format context
|
|
353
|
+
"""
|
|
354
|
+
from ai_code_assistant.retrieval import CodebaseSearch
|
|
355
|
+
from ai_code_assistant.retrieval.indexer import IndexConfig
|
|
356
|
+
|
|
357
|
+
config = load_config(ctx.obj.get("config_path"))
|
|
358
|
+
|
|
359
|
+
index_config = IndexConfig(
|
|
360
|
+
embedding_model=config.retrieval.embedding_model,
|
|
361
|
+
persist_directory=config.retrieval.persist_directory,
|
|
362
|
+
collection_name=config.retrieval.collection_name,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
searcher = CodebaseSearch(config=index_config, root_path=str(Path.cwd()))
|
|
367
|
+
except FileNotFoundError as e:
|
|
368
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
369
|
+
console.print("\nRun [cyan]ai-assist index .[/cyan] first to create the index.")
|
|
370
|
+
sys.exit(1)
|
|
371
|
+
|
|
372
|
+
with console.status("[bold green]Searching..."):
|
|
373
|
+
response = searcher.search(
|
|
374
|
+
query=query,
|
|
375
|
+
top_k=top_k,
|
|
376
|
+
file_filter=file_filter,
|
|
377
|
+
language_filter=lang_filter,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if output_format == "json":
|
|
381
|
+
import json
|
|
382
|
+
output = {
|
|
383
|
+
"query": response.query,
|
|
384
|
+
"total_results": response.total_results,
|
|
385
|
+
"results": [r.to_dict() for r in response.results],
|
|
386
|
+
}
|
|
387
|
+
console.print(json.dumps(output, indent=2))
|
|
388
|
+
|
|
389
|
+
elif output_format == "context":
|
|
390
|
+
# Format for use as LLM context
|
|
391
|
+
console.print(response.format_for_llm(max_results=top_k))
|
|
392
|
+
|
|
393
|
+
else: # console
|
|
394
|
+
console.print(f"\n[bold]Search:[/bold] {query}")
|
|
395
|
+
console.print(f"[bold]Results:[/bold] {response.total_results}\n")
|
|
396
|
+
|
|
397
|
+
if not response.has_results:
|
|
398
|
+
console.print("[yellow]No results found.[/yellow]")
|
|
399
|
+
console.print("\nTips:")
|
|
400
|
+
console.print(" • Try broader search terms")
|
|
401
|
+
console.print(" • Check if the codebase is indexed: ai-assist status")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
for i, result in enumerate(response.results, 1):
|
|
405
|
+
console.print(f"[bold cyan]─── Result {i} ───[/bold cyan]")
|
|
406
|
+
console.print(f"[bold]{result.file_path}[/bold]:{result.start_line}-{result.end_line}")
|
|
407
|
+
console.print(f"[dim]Type: {result.chunk_type} | Name: {result.name} | Score: {result.score:.3f}[/dim]")
|
|
408
|
+
console.print()
|
|
409
|
+
|
|
410
|
+
# Show code with syntax highlighting
|
|
411
|
+
from rich.syntax import Syntax
|
|
412
|
+
syntax = Syntax(
|
|
413
|
+
result.content,
|
|
414
|
+
result.language or "text",
|
|
415
|
+
line_numbers=True,
|
|
416
|
+
start_line=result.start_line,
|
|
417
|
+
theme="monokai",
|
|
418
|
+
)
|
|
419
|
+
console.print(syntax)
|
|
420
|
+
console.print()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@main.command()
|
|
424
|
+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
|
|
425
|
+
@click.argument("instruction")
|
|
426
|
+
@click.option("--mode", "-m", default="edit",
|
|
427
|
+
type=click.Choice(["edit", "refactor", "fix", "add"]),
|
|
428
|
+
help="Edit mode")
|
|
429
|
+
@click.option("--preview", "-p", is_flag=True, help="Preview changes without applying")
|
|
430
|
+
@click.option("--no-backup", is_flag=True, help="Don't create backup file")
|
|
431
|
+
@click.option("--format", "-f", "output_format", default="console",
|
|
432
|
+
type=click.Choice(["console", "json"]), help="Output format")
|
|
433
|
+
@click.option("--start-line", "-s", type=int, help="Start line for targeted edit")
|
|
434
|
+
@click.option("--end-line", "-e", type=int, help="End line for targeted edit")
|
|
435
|
+
@click.pass_context
|
|
436
|
+
def edit(ctx, file: Path, instruction: str, mode: str, preview: bool,
|
|
437
|
+
no_backup: bool, output_format: str, start_line: Optional[int],
|
|
438
|
+
end_line: Optional[int]):
|
|
439
|
+
"""Edit a file using AI based on natural language instructions.
|
|
440
|
+
|
|
441
|
+
Examples:
|
|
442
|
+
ai-assist edit main.py "Add error handling to the parse function"
|
|
443
|
+
ai-assist edit utils.py "Add type hints" --mode refactor
|
|
444
|
+
ai-assist edit app.py "Fix the null pointer bug" --mode fix
|
|
445
|
+
ai-assist edit api.py "Add logging" --preview
|
|
446
|
+
ai-assist edit config.py "Update the timeout value" -s 10 -e 20
|
|
447
|
+
"""
|
|
448
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
449
|
+
editor = FileEditor(config, llm)
|
|
450
|
+
|
|
451
|
+
# Determine edit mode
|
|
452
|
+
edit_mode = mode
|
|
453
|
+
if start_line and end_line:
|
|
454
|
+
edit_mode = "targeted"
|
|
455
|
+
|
|
456
|
+
console.print(f"\n[bold]Editing {file}...[/bold]")
|
|
457
|
+
console.print(f"Mode: [cyan]{edit_mode}[/cyan]")
|
|
458
|
+
console.print(f"Instruction: [dim]{instruction}[/dim]\n")
|
|
459
|
+
|
|
460
|
+
with console.status("[bold green]Generating edit..."):
|
|
461
|
+
result = editor.edit_file(
|
|
462
|
+
file_path=file,
|
|
463
|
+
instruction=instruction,
|
|
464
|
+
mode=edit_mode,
|
|
465
|
+
preview=preview,
|
|
466
|
+
create_backup=not no_backup,
|
|
467
|
+
start_line=start_line,
|
|
468
|
+
end_line=end_line,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if output_format == "json":
|
|
472
|
+
import json
|
|
473
|
+
console.print(json.dumps(result.to_dict(), indent=2))
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
# Console output
|
|
477
|
+
if not result.success:
|
|
478
|
+
console.print(f"[red]Error:[/red] {result.error}")
|
|
479
|
+
sys.exit(1)
|
|
480
|
+
|
|
481
|
+
if not result.has_changes:
|
|
482
|
+
console.print("[yellow]No changes detected.[/yellow]")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# Show diff
|
|
486
|
+
if result.diff:
|
|
487
|
+
console.print("[bold]Changes:[/bold]")
|
|
488
|
+
console.print(f" [green]+{result.diff.additions}[/green] additions, "
|
|
489
|
+
f"[red]-{result.diff.deletions}[/red] deletions\n")
|
|
490
|
+
|
|
491
|
+
from rich.syntax import Syntax
|
|
492
|
+
diff_text = result.diff.unified_diff
|
|
493
|
+
syntax = Syntax(diff_text, "diff", theme="monokai")
|
|
494
|
+
console.print(syntax)
|
|
495
|
+
|
|
496
|
+
if preview:
|
|
497
|
+
console.print("\n[yellow]Preview mode - changes not applied[/yellow]")
|
|
498
|
+
console.print("Run without --preview to apply changes.")
|
|
499
|
+
else:
|
|
500
|
+
if result.applied:
|
|
501
|
+
console.print(f"\n[green]✓ Changes applied to {file}[/green]")
|
|
502
|
+
if result.backup_path:
|
|
503
|
+
console.print(f"[dim]Backup saved: {result.backup_path}[/dim]")
|
|
504
|
+
else:
|
|
505
|
+
console.print(f"\n[red]✗ Failed to apply changes[/red]")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
@main.command()
|
|
509
|
+
@click.argument("instruction")
|
|
510
|
+
@click.option("--files", "-f", multiple=True, type=click.Path(exists=True, path_type=Path),
|
|
511
|
+
help="Specific files to include")
|
|
512
|
+
@click.option("--pattern", "-p", help="Glob pattern to match files (e.g., '**/*.py')")
|
|
513
|
+
@click.option("--directory", "-d", type=click.Path(exists=True, path_type=Path),
|
|
514
|
+
default=".", help="Directory to search for files")
|
|
515
|
+
@click.option("--dry-run", is_flag=True, help="Show plan without applying changes")
|
|
516
|
+
@click.option("--no-confirm", is_flag=True, help="Skip confirmation prompt")
|
|
517
|
+
@click.option("--no-backup", is_flag=True, help="Don't create backup")
|
|
518
|
+
@click.option("--format", "output_format", default="console",
|
|
519
|
+
type=click.Choice(["console", "json"]), help="Output format")
|
|
520
|
+
@click.pass_context
|
|
521
|
+
def refactor(ctx, instruction: str, files: Tuple[Path, ...], pattern: Optional[str],
|
|
522
|
+
directory: Path, dry_run: bool, no_confirm: bool, no_backup: bool,
|
|
523
|
+
output_format: str):
|
|
524
|
+
"""Perform multi-file refactoring using AI.
|
|
525
|
+
|
|
526
|
+
Analyzes the codebase and applies coordinated changes across multiple files.
|
|
527
|
+
|
|
528
|
+
Examples:
|
|
529
|
+
ai-assist refactor "Add type hints to all functions"
|
|
530
|
+
ai-assist refactor "Rename User class to Account" -p "**/*.py"
|
|
531
|
+
ai-assist refactor "Extract database logic to repository pattern" --dry-run
|
|
532
|
+
ai-assist refactor "Add logging to all API endpoints" -d ./src/api
|
|
533
|
+
"""
|
|
534
|
+
from ai_code_assistant.refactor import MultiFileEditor
|
|
535
|
+
from ai_code_assistant.utils import FileHandler
|
|
536
|
+
|
|
537
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
538
|
+
editor = MultiFileEditor(config, llm)
|
|
539
|
+
file_handler = FileHandler(config)
|
|
540
|
+
|
|
541
|
+
# Collect files to refactor
|
|
542
|
+
all_files: List[Path] = list(files)
|
|
543
|
+
|
|
544
|
+
if pattern:
|
|
545
|
+
# Use glob pattern
|
|
546
|
+
all_files.extend(directory.glob(pattern))
|
|
547
|
+
elif not files:
|
|
548
|
+
# Default: find all code files in directory
|
|
549
|
+
all_files.extend(file_handler.find_code_files(directory, recursive=True))
|
|
550
|
+
|
|
551
|
+
# Remove duplicates and limit
|
|
552
|
+
all_files = list(set(all_files))[:config.refactor.max_files]
|
|
553
|
+
|
|
554
|
+
if not all_files:
|
|
555
|
+
console.print("[red]Error:[/red] No files found to refactor")
|
|
556
|
+
sys.exit(1)
|
|
557
|
+
|
|
558
|
+
console.print(f"\n[bold]Multi-File Refactoring[/bold]")
|
|
559
|
+
console.print(f"Instruction: [cyan]{instruction}[/cyan]")
|
|
560
|
+
console.print(f"Files in scope: [cyan]{len(all_files)}[/cyan]\n")
|
|
561
|
+
|
|
562
|
+
# Show files
|
|
563
|
+
if ctx.obj.get("verbose"):
|
|
564
|
+
for f in all_files[:10]:
|
|
565
|
+
console.print(f" • {f}")
|
|
566
|
+
if len(all_files) > 10:
|
|
567
|
+
console.print(f" ... and {len(all_files) - 10} more")
|
|
568
|
+
console.print()
|
|
569
|
+
|
|
570
|
+
# Analyze and create plan
|
|
571
|
+
with console.status("[bold green]Analyzing codebase..."):
|
|
572
|
+
result = editor.refactor(
|
|
573
|
+
instruction=instruction,
|
|
574
|
+
files=all_files,
|
|
575
|
+
dry_run=True, # Always start with dry run to show plan
|
|
576
|
+
create_backup=not no_backup,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if result.error and not result.plan.changes:
|
|
580
|
+
console.print(f"[red]Error:[/red] {result.error}")
|
|
581
|
+
sys.exit(1)
|
|
582
|
+
|
|
583
|
+
# Output as JSON if requested
|
|
584
|
+
if output_format == "json":
|
|
585
|
+
import json
|
|
586
|
+
console.print(json.dumps(result.to_dict(), indent=2))
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
# Show plan
|
|
590
|
+
plan = result.plan
|
|
591
|
+
console.print(f"[bold]Refactoring Plan[/bold]")
|
|
592
|
+
console.print(f"Summary: {plan.summary}")
|
|
593
|
+
console.print(f"Complexity: [cyan]{plan.complexity}[/cyan]")
|
|
594
|
+
console.print(f"Files affected: [cyan]{plan.total_files}[/cyan]\n")
|
|
595
|
+
|
|
596
|
+
if plan.risks:
|
|
597
|
+
console.print("[yellow]Risks:[/yellow]")
|
|
598
|
+
for risk in plan.risks:
|
|
599
|
+
console.print(f" ⚠ {risk}")
|
|
600
|
+
console.print()
|
|
601
|
+
|
|
602
|
+
# Show changes
|
|
603
|
+
console.print("[bold]Planned Changes:[/bold]")
|
|
604
|
+
for change in plan.changes:
|
|
605
|
+
icon = {"modify": "📝", "create": "✨", "delete": "🗑️", "rename": "📛"}.get(
|
|
606
|
+
change.change_type.value, "•"
|
|
607
|
+
)
|
|
608
|
+
console.print(f" {icon} [{change.change_type.value}] {change.file_path}")
|
|
609
|
+
console.print(f" {change.description}")
|
|
610
|
+
|
|
611
|
+
if dry_run:
|
|
612
|
+
console.print("\n[yellow]Dry run - no changes applied[/yellow]")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
# Confirm before applying
|
|
616
|
+
if not no_confirm and config.refactor.require_confirmation:
|
|
617
|
+
console.print()
|
|
618
|
+
if not click.confirm("Apply these changes?"):
|
|
619
|
+
console.print("[dim]Cancelled[/dim]")
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
# Apply changes
|
|
623
|
+
console.print("\n[bold green]Applying changes...[/bold green]")
|
|
624
|
+
|
|
625
|
+
with console.status("[bold green]Generating and applying changes..."):
|
|
626
|
+
result = editor.refactor(
|
|
627
|
+
instruction=instruction,
|
|
628
|
+
files=all_files,
|
|
629
|
+
dry_run=False,
|
|
630
|
+
create_backup=not no_backup,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if result.error:
|
|
634
|
+
console.print(f"[red]Error:[/red] {result.error}")
|
|
635
|
+
sys.exit(1)
|
|
636
|
+
|
|
637
|
+
# Show results
|
|
638
|
+
console.print(f"\n[bold green]✓ Refactoring complete![/bold green]")
|
|
639
|
+
console.print(f" Files changed: {result.files_changed}")
|
|
640
|
+
console.print(f" Additions: [green]+{result.total_additions}[/green]")
|
|
641
|
+
console.print(f" Deletions: [red]-{result.total_deletions}[/red]")
|
|
642
|
+
|
|
643
|
+
if result.backup_dir:
|
|
644
|
+
console.print(f"\n[dim]Backup saved: {result.backup_dir}[/dim]")
|
|
645
|
+
|
|
646
|
+
if result.files_failed > 0:
|
|
647
|
+
console.print(f"\n[yellow]Warning: {result.files_failed} file(s) failed[/yellow]")
|
|
648
|
+
for change in result.plan.changes:
|
|
649
|
+
if change.error:
|
|
650
|
+
console.print(f" • {change.file_path}: {change.error}")
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@main.command()
|
|
654
|
+
@click.argument("old_name")
|
|
655
|
+
@click.argument("new_name")
|
|
656
|
+
@click.option("--type", "-t", "symbol_type", default="symbol",
|
|
657
|
+
type=click.Choice(["function", "class", "variable", "method", "symbol"]),
|
|
658
|
+
help="Type of symbol to rename")
|
|
659
|
+
@click.option("--files", "-f", multiple=True, type=click.Path(exists=True, path_type=Path),
|
|
660
|
+
help="Specific files to include")
|
|
661
|
+
@click.option("--pattern", "-p", help="Glob pattern to match files")
|
|
662
|
+
@click.option("--directory", "-d", type=click.Path(exists=True, path_type=Path),
|
|
663
|
+
default=".", help="Directory to search")
|
|
664
|
+
@click.option("--dry-run", is_flag=True, help="Show changes without applying")
|
|
665
|
+
@click.pass_context
|
|
666
|
+
def rename(ctx, old_name: str, new_name: str, symbol_type: str, files: Tuple[Path, ...],
|
|
667
|
+
pattern: Optional[str], directory: Path, dry_run: bool):
|
|
668
|
+
"""Rename a symbol across multiple files.
|
|
669
|
+
|
|
670
|
+
Examples:
|
|
671
|
+
ai-assist rename UserService AccountService --type class
|
|
672
|
+
ai-assist rename get_user fetch_user --type function -p "**/*.py"
|
|
673
|
+
ai-assist rename API_KEY API_SECRET --type variable --dry-run
|
|
674
|
+
"""
|
|
675
|
+
from ai_code_assistant.refactor import MultiFileEditor
|
|
676
|
+
from ai_code_assistant.utils import FileHandler
|
|
677
|
+
|
|
678
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
679
|
+
editor = MultiFileEditor(config, llm)
|
|
680
|
+
file_handler = FileHandler(config)
|
|
681
|
+
|
|
682
|
+
# Collect files
|
|
683
|
+
all_files: List[Path] = list(files)
|
|
684
|
+
if pattern:
|
|
685
|
+
all_files.extend(directory.glob(pattern))
|
|
686
|
+
elif not files:
|
|
687
|
+
all_files.extend(file_handler.find_code_files(directory, recursive=True))
|
|
688
|
+
|
|
689
|
+
all_files = list(set(all_files))[:config.refactor.max_files]
|
|
690
|
+
|
|
691
|
+
if not all_files:
|
|
692
|
+
console.print("[red]Error:[/red] No files found")
|
|
693
|
+
sys.exit(1)
|
|
694
|
+
|
|
695
|
+
console.print(f"\n[bold]Rename Symbol[/bold]")
|
|
696
|
+
console.print(f"Renaming {symbol_type}: [cyan]{old_name}[/cyan] → [green]{new_name}[/green]")
|
|
697
|
+
console.print(f"Files to search: [cyan]{len(all_files)}[/cyan]\n")
|
|
698
|
+
|
|
699
|
+
with console.status("[bold green]Searching and renaming..."):
|
|
700
|
+
result = editor.rename_symbol(
|
|
701
|
+
old_name=old_name,
|
|
702
|
+
new_name=new_name,
|
|
703
|
+
symbol_type=symbol_type,
|
|
704
|
+
files=all_files,
|
|
705
|
+
dry_run=dry_run,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if result.error:
|
|
709
|
+
console.print(f"[red]Error:[/red] {result.error}")
|
|
710
|
+
sys.exit(1)
|
|
711
|
+
|
|
712
|
+
# Show results
|
|
713
|
+
console.print(f"[bold]Files affected: {result.plan.total_files}[/bold]\n")
|
|
714
|
+
|
|
715
|
+
for change in result.plan.changes:
|
|
716
|
+
status = "[green]✓[/green]" if change.applied else "[yellow]○[/yellow]"
|
|
717
|
+
console.print(f" {status} {change.file_path}")
|
|
718
|
+
|
|
719
|
+
if dry_run:
|
|
720
|
+
console.print("\n[yellow]Dry run - no changes applied[/yellow]")
|
|
721
|
+
else:
|
|
722
|
+
console.print(f"\n[green]✓ Renamed {old_name} to {new_name} in {result.files_changed} file(s)[/green]")
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@main.command()
|
|
727
|
+
@click.option("--format", "-f", "output_format", default="console",
|
|
728
|
+
type=click.Choice(["console", "json"]), help="Output format")
|
|
729
|
+
@click.pass_context
|
|
730
|
+
def providers(ctx, output_format: str):
|
|
731
|
+
"""List all available LLM providers and their models.
|
|
732
|
+
|
|
733
|
+
Shows provider information including:
|
|
734
|
+
- Whether an API key is required
|
|
735
|
+
- Free tier availability
|
|
736
|
+
- Available models with descriptions
|
|
737
|
+
|
|
738
|
+
Examples:
|
|
739
|
+
ai-assist providers
|
|
740
|
+
ai-assist providers --format json
|
|
741
|
+
"""
|
|
742
|
+
from ai_code_assistant.providers.factory import get_available_providers
|
|
743
|
+
|
|
744
|
+
providers_info = get_available_providers()
|
|
745
|
+
|
|
746
|
+
if output_format == "json":
|
|
747
|
+
import json
|
|
748
|
+
console.print(json.dumps(providers_info, indent=2))
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Console output
|
|
752
|
+
console.print("\n[bold]Available LLM Providers[/bold]\n")
|
|
753
|
+
|
|
754
|
+
for provider_name, info in providers_info.items():
|
|
755
|
+
# Provider header
|
|
756
|
+
api_badge = "[red]API Key Required[/red]" if info["requires_api_key"] else "[green]No API Key[/green]"
|
|
757
|
+
free_badge = "[green]Free Tier[/green]" if info["free_tier"] else "[yellow]Paid Only[/yellow]"
|
|
758
|
+
|
|
759
|
+
console.print(f"[bold cyan]━━━ {info['display_name']} ({provider_name}) ━━━[/bold cyan]")
|
|
760
|
+
console.print(f" {api_badge} | {free_badge}")
|
|
761
|
+
console.print(f" Default model: [dim]{info['default_model']}[/dim]")
|
|
762
|
+
console.print()
|
|
763
|
+
|
|
764
|
+
# Models table
|
|
765
|
+
console.print(" [bold]Models:[/bold]")
|
|
766
|
+
for model in info["models"]:
|
|
767
|
+
free_icon = "🆓" if model["is_free"] else "💰"
|
|
768
|
+
console.print(f" {free_icon} [cyan]{model['name']}[/cyan]")
|
|
769
|
+
console.print(f" {model['description']}")
|
|
770
|
+
console.print(f" Context: {model['context_window']:,} tokens")
|
|
771
|
+
console.print()
|
|
772
|
+
|
|
773
|
+
# Show current configuration
|
|
774
|
+
config = load_config(ctx.obj.get("config_path"))
|
|
775
|
+
console.print("[bold]Current Configuration:[/bold]")
|
|
776
|
+
console.print(f" Provider: [cyan]{config.llm.provider}[/cyan]")
|
|
777
|
+
console.print(f" Model: [cyan]{config.llm.model}[/cyan]")
|
|
778
|
+
|
|
779
|
+
# Check for API keys
|
|
780
|
+
import os
|
|
781
|
+
console.print("\n[bold]API Key Status:[/bold]")
|
|
782
|
+
env_vars = [
|
|
783
|
+
("GOOGLE_API_KEY", "Google"),
|
|
784
|
+
("GROQ_API_KEY", "Groq"),
|
|
785
|
+
("CEREBRAS_API_KEY", "Cerebras"),
|
|
786
|
+
("OPENROUTER_API_KEY", "OpenRouter"),
|
|
787
|
+
("OPENAI_API_KEY", "OpenAI"),
|
|
788
|
+
]
|
|
789
|
+
for env_var, name in env_vars:
|
|
790
|
+
status = "[green]✓ Set[/green]" if os.getenv(env_var) else "[dim]Not set[/dim]"
|
|
791
|
+
console.print(f" {name}: {status}")
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@main.command("use-provider")
|
|
795
|
+
@click.argument("provider", type=click.Choice(["ollama", "google", "groq", "cerebras", "openrouter", "openai"]))
|
|
796
|
+
@click.option("--model", "-m", help="Model to use (uses provider default if not specified)")
|
|
797
|
+
@click.option("--api-key", "-k", help="API key (can also use environment variable)")
|
|
798
|
+
@click.option("--test", "-t", is_flag=True, help="Test the connection after switching")
|
|
799
|
+
@click.pass_context
|
|
800
|
+
def use_provider(ctx, provider: str, model: Optional[str], api_key: Optional[str], test: bool):
|
|
801
|
+
"""Switch to a different LLM provider.
|
|
802
|
+
|
|
803
|
+
This updates the current session's provider. To make permanent changes,
|
|
804
|
+
update your config.yaml file.
|
|
805
|
+
|
|
806
|
+
Examples:
|
|
807
|
+
ai-assist use-provider groq
|
|
808
|
+
ai-assist use-provider google --model gemini-1.5-pro
|
|
809
|
+
ai-assist use-provider openrouter --model deepseek/deepseek-r1:free --test
|
|
810
|
+
"""
|
|
811
|
+
from ai_code_assistant.providers.factory import PROVIDER_REGISTRY, get_provider
|
|
812
|
+
from ai_code_assistant.providers.base import ProviderConfig, ProviderType
|
|
813
|
+
|
|
814
|
+
config = load_config(ctx.obj.get("config_path"))
|
|
815
|
+
|
|
816
|
+
# Get provider class for default model
|
|
817
|
+
provider_type = ProviderType(provider)
|
|
818
|
+
provider_class = PROVIDER_REGISTRY.get(provider_type)
|
|
819
|
+
|
|
820
|
+
if not provider_class:
|
|
821
|
+
console.print(f"[red]Error:[/red] Unknown provider: {provider}")
|
|
822
|
+
sys.exit(1)
|
|
823
|
+
|
|
824
|
+
# Use default model if not specified
|
|
825
|
+
if not model:
|
|
826
|
+
model = provider_class.default_model
|
|
827
|
+
|
|
828
|
+
console.print(f"\n[bold]Switching to {provider_class.display_name}[/bold]")
|
|
829
|
+
console.print(f" Model: [cyan]{model}[/cyan]")
|
|
830
|
+
|
|
831
|
+
# Check API key
|
|
832
|
+
import os
|
|
833
|
+
env_var_map = {
|
|
834
|
+
"google": "GOOGLE_API_KEY",
|
|
835
|
+
"groq": "GROQ_API_KEY",
|
|
836
|
+
"cerebras": "CEREBRAS_API_KEY",
|
|
837
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
838
|
+
"openai": "OPENAI_API_KEY",
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if provider_class.requires_api_key:
|
|
842
|
+
env_var = env_var_map.get(provider)
|
|
843
|
+
env_key = os.getenv(env_var) if env_var else None
|
|
844
|
+
|
|
845
|
+
if not api_key and not env_key:
|
|
846
|
+
console.print(f"\n[yellow]Warning:[/yellow] No API key provided")
|
|
847
|
+
console.print(f"Set {env_var} environment variable or use --api-key")
|
|
848
|
+
console.print(f"\n{provider_class.get_setup_instructions()}")
|
|
849
|
+
sys.exit(1)
|
|
850
|
+
|
|
851
|
+
api_key = api_key or env_key
|
|
852
|
+
|
|
853
|
+
# Create provider config
|
|
854
|
+
provider_config = ProviderConfig(
|
|
855
|
+
provider=provider_type,
|
|
856
|
+
model=model,
|
|
857
|
+
api_key=api_key,
|
|
858
|
+
temperature=config.llm.temperature,
|
|
859
|
+
max_tokens=config.llm.max_tokens,
|
|
860
|
+
timeout=config.llm.timeout,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
# Validate config
|
|
864
|
+
try:
|
|
865
|
+
provider_instance = get_provider(provider_config)
|
|
866
|
+
is_valid, error = provider_instance.validate_config()
|
|
867
|
+
if not is_valid:
|
|
868
|
+
console.print(f"[red]Configuration error:[/red] {error}")
|
|
869
|
+
sys.exit(1)
|
|
870
|
+
except Exception as e:
|
|
871
|
+
console.print(f"[red]Error creating provider:[/red] {e}")
|
|
872
|
+
sys.exit(1)
|
|
873
|
+
|
|
874
|
+
console.print("[green]✓ Provider configured successfully[/green]")
|
|
875
|
+
|
|
876
|
+
# Test connection if requested
|
|
877
|
+
if test:
|
|
878
|
+
console.print("\n[bold]Testing connection...[/bold]")
|
|
879
|
+
with console.status("[bold green]Sending test request..."):
|
|
880
|
+
try:
|
|
881
|
+
response = provider_instance.invoke("Say 'Hello from {provider}!' and nothing else.")
|
|
882
|
+
console.print(f"[green]✓ Connection successful![/green]")
|
|
883
|
+
console.print(f" Response: [dim]{response.strip()}[/dim]")
|
|
884
|
+
except Exception as e:
|
|
885
|
+
console.print(f"[red]✗ Connection failed:[/red] {e}")
|
|
886
|
+
sys.exit(1)
|
|
887
|
+
|
|
888
|
+
# Show how to make permanent
|
|
889
|
+
console.print("\n[dim]To make this permanent, update your config.yaml:[/dim]")
|
|
890
|
+
console.print(f"[dim] llm:[/dim]")
|
|
891
|
+
console.print(f"[dim] provider: {provider}[/dim]")
|
|
892
|
+
console.print(f"[dim] model: {model}[/dim]")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@main.command("test-provider")
|
|
896
|
+
@click.option("--provider", "-p", help="Provider to test (uses current config if not specified)")
|
|
897
|
+
@click.option("--model", "-m", help="Model to test")
|
|
898
|
+
@click.option("--prompt", default="Write a Python function that adds two numbers.", help="Test prompt")
|
|
899
|
+
@click.pass_context
|
|
900
|
+
def test_provider(ctx, provider: Optional[str], model: Optional[str], prompt: str):
|
|
901
|
+
"""Test the current or specified LLM provider.
|
|
902
|
+
|
|
903
|
+
Sends a test prompt and displays the response with timing information.
|
|
904
|
+
|
|
905
|
+
Examples:
|
|
906
|
+
ai-assist test-provider
|
|
907
|
+
ai-assist test-provider --provider groq
|
|
908
|
+
ai-assist test-provider --prompt "Explain recursion in one sentence"
|
|
909
|
+
"""
|
|
910
|
+
import time
|
|
911
|
+
from ai_code_assistant.providers.factory import get_provider, PROVIDER_REGISTRY
|
|
912
|
+
from ai_code_assistant.providers.base import ProviderConfig, ProviderType
|
|
913
|
+
|
|
914
|
+
config = load_config(ctx.obj.get("config_path"))
|
|
915
|
+
|
|
916
|
+
# Use current config or specified provider
|
|
917
|
+
if provider:
|
|
918
|
+
provider_type = ProviderType(provider)
|
|
919
|
+
provider_class = PROVIDER_REGISTRY.get(provider_type)
|
|
920
|
+
if not model:
|
|
921
|
+
model = provider_class.default_model
|
|
922
|
+
else:
|
|
923
|
+
provider_type = ProviderType(config.llm.provider)
|
|
924
|
+
model = model or config.llm.model
|
|
925
|
+
|
|
926
|
+
console.print(f"\n[bold]Testing LLM Provider[/bold]")
|
|
927
|
+
console.print(f" Provider: [cyan]{provider_type.value}[/cyan]")
|
|
928
|
+
console.print(f" Model: [cyan]{model}[/cyan]")
|
|
929
|
+
console.print(f" Prompt: [dim]{prompt[:50]}{'...' if len(prompt) > 50 else ''}[/dim]\n")
|
|
930
|
+
|
|
931
|
+
# Create provider
|
|
932
|
+
import os
|
|
933
|
+
env_var_map = {
|
|
934
|
+
"google": "GOOGLE_API_KEY",
|
|
935
|
+
"groq": "GROQ_API_KEY",
|
|
936
|
+
"cerebras": "CEREBRAS_API_KEY",
|
|
937
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
938
|
+
"openai": "OPENAI_API_KEY",
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
api_key = config.llm.api_key
|
|
942
|
+
if not api_key:
|
|
943
|
+
env_var = env_var_map.get(provider_type.value)
|
|
944
|
+
api_key = os.getenv(env_var) if env_var else None
|
|
945
|
+
|
|
946
|
+
provider_config = ProviderConfig(
|
|
947
|
+
provider=provider_type,
|
|
948
|
+
model=model,
|
|
949
|
+
api_key=api_key,
|
|
950
|
+
base_url=config.llm.base_url if provider_type == ProviderType.OLLAMA else None,
|
|
951
|
+
temperature=config.llm.temperature,
|
|
952
|
+
max_tokens=config.llm.max_tokens,
|
|
953
|
+
timeout=config.llm.timeout,
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
provider_instance = get_provider(provider_config)
|
|
958
|
+
except Exception as e:
|
|
959
|
+
console.print(f"[red]Error creating provider:[/red] {e}")
|
|
960
|
+
sys.exit(1)
|
|
961
|
+
|
|
962
|
+
# Send test request
|
|
963
|
+
console.print("[bold]Sending request...[/bold]\n")
|
|
964
|
+
start_time = time.time()
|
|
965
|
+
|
|
966
|
+
try:
|
|
967
|
+
response = provider_instance.invoke(prompt)
|
|
968
|
+
elapsed = time.time() - start_time
|
|
969
|
+
|
|
970
|
+
console.print("[bold green]Response:[/bold green]")
|
|
971
|
+
console.print(Panel(response.strip(), border_style="green"))
|
|
972
|
+
|
|
973
|
+
console.print(f"\n[dim]Time: {elapsed:.2f}s[/dim]")
|
|
974
|
+
console.print(f"[dim]Tokens (approx): {len(response.split())} words[/dim]")
|
|
975
|
+
|
|
976
|
+
except Exception as e:
|
|
977
|
+
elapsed = time.time() - start_time
|
|
978
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
979
|
+
console.print(f"[dim]Failed after {elapsed:.2f}s[/dim]")
|
|
980
|
+
sys.exit(1)
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
# ============================================================================
|
|
985
|
+
# Git Integration Commands
|
|
986
|
+
# ============================================================================
|
|
987
|
+
|
|
988
|
+
@main.group()
|
|
989
|
+
def git():
|
|
990
|
+
"""Git integration commands with AI-powered commit messages."""
|
|
991
|
+
pass
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
@git.command("status")
|
|
995
|
+
@click.pass_context
|
|
996
|
+
def git_status(ctx):
|
|
997
|
+
"""Show git status with summary.
|
|
998
|
+
|
|
999
|
+
Examples:
|
|
1000
|
+
ai-assist git status
|
|
1001
|
+
"""
|
|
1002
|
+
from ai_code_assistant.git import GitManager
|
|
1003
|
+
|
|
1004
|
+
try:
|
|
1005
|
+
git_mgr = GitManager()
|
|
1006
|
+
except ValueError as e:
|
|
1007
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1008
|
+
sys.exit(1)
|
|
1009
|
+
|
|
1010
|
+
status = git_mgr.get_status()
|
|
1011
|
+
|
|
1012
|
+
console.print(f"\n[bold]Git Status[/bold]")
|
|
1013
|
+
console.print(f" Branch: [cyan]{status.branch}[/cyan]")
|
|
1014
|
+
|
|
1015
|
+
if status.ahead:
|
|
1016
|
+
console.print(f" [green]↑ {status.ahead} commit(s) ahead[/green]")
|
|
1017
|
+
if status.behind:
|
|
1018
|
+
console.print(f" [yellow]↓ {status.behind} commit(s) behind[/yellow]")
|
|
1019
|
+
|
|
1020
|
+
if not status.has_changes:
|
|
1021
|
+
console.print("\n[green]✓ Working tree clean[/green]")
|
|
1022
|
+
return
|
|
1023
|
+
|
|
1024
|
+
console.print(f"\n[bold]Changes ({status.total_changes} files):[/bold]")
|
|
1025
|
+
|
|
1026
|
+
if status.staged:
|
|
1027
|
+
console.print(f"\n [green]Staged ({len(status.staged)}):[/green]")
|
|
1028
|
+
for f in status.staged[:10]:
|
|
1029
|
+
console.print(f" [green]✓[/green] {f}")
|
|
1030
|
+
if len(status.staged) > 10:
|
|
1031
|
+
console.print(f" ... and {len(status.staged) - 10} more")
|
|
1032
|
+
|
|
1033
|
+
if status.modified:
|
|
1034
|
+
console.print(f"\n [yellow]Modified ({len(status.modified)}):[/yellow]")
|
|
1035
|
+
for f in status.modified[:10]:
|
|
1036
|
+
console.print(f" [yellow]●[/yellow] {f}")
|
|
1037
|
+
if len(status.modified) > 10:
|
|
1038
|
+
console.print(f" ... and {len(status.modified) - 10} more")
|
|
1039
|
+
|
|
1040
|
+
if status.untracked:
|
|
1041
|
+
console.print(f"\n [dim]Untracked ({len(status.untracked)}):[/dim]")
|
|
1042
|
+
for f in status.untracked[:10]:
|
|
1043
|
+
console.print(f" [dim]?[/dim] {f}")
|
|
1044
|
+
if len(status.untracked) > 10:
|
|
1045
|
+
console.print(f" ... and {len(status.untracked) - 10} more")
|
|
1046
|
+
|
|
1047
|
+
if status.deleted:
|
|
1048
|
+
console.print(f"\n [red]Deleted ({len(status.deleted)}):[/red]")
|
|
1049
|
+
for f in status.deleted[:10]:
|
|
1050
|
+
console.print(f" [red]✗[/red] {f}")
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
@git.command("commit")
|
|
1054
|
+
@click.option("--message", "-m", help="Commit message (AI generates if not provided)")
|
|
1055
|
+
@click.option("--all", "-a", "stage_all", is_flag=True, help="Stage all changes before commit")
|
|
1056
|
+
@click.option("--push", "-p", "push_after", is_flag=True, help="Push after commit")
|
|
1057
|
+
@click.option("--no-confirm", is_flag=True, help="Skip confirmation prompt")
|
|
1058
|
+
@click.pass_context
|
|
1059
|
+
def git_commit(ctx, message: Optional[str], stage_all: bool, push_after: bool, no_confirm: bool):
|
|
1060
|
+
"""Commit changes with AI-generated message.
|
|
1061
|
+
|
|
1062
|
+
If no message is provided, AI analyzes the diff and generates one.
|
|
1063
|
+
|
|
1064
|
+
Examples:
|
|
1065
|
+
ai-assist git commit # AI generates message
|
|
1066
|
+
ai-assist git commit -m "Fix bug" # Use provided message
|
|
1067
|
+
ai-assist git commit -a --push # Stage all, commit, push
|
|
1068
|
+
ai-assist git commit --no-confirm # Skip confirmation
|
|
1069
|
+
"""
|
|
1070
|
+
from ai_code_assistant.git import GitManager, CommitMessageGenerator
|
|
1071
|
+
|
|
1072
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1073
|
+
|
|
1074
|
+
try:
|
|
1075
|
+
git_mgr = GitManager()
|
|
1076
|
+
except ValueError as e:
|
|
1077
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1078
|
+
sys.exit(1)
|
|
1079
|
+
|
|
1080
|
+
status = git_mgr.get_status()
|
|
1081
|
+
|
|
1082
|
+
# Stage all if requested
|
|
1083
|
+
if stage_all:
|
|
1084
|
+
console.print("[dim]Staging all changes...[/dim]")
|
|
1085
|
+
git_mgr.stage_all()
|
|
1086
|
+
status = git_mgr.get_status()
|
|
1087
|
+
|
|
1088
|
+
# Check if there are staged changes
|
|
1089
|
+
if not status.has_staged:
|
|
1090
|
+
if status.has_changes:
|
|
1091
|
+
console.print("[yellow]No staged changes.[/yellow]")
|
|
1092
|
+
console.print("Use [cyan]--all[/cyan] to stage all changes, or stage manually with [cyan]git add[/cyan]")
|
|
1093
|
+
else:
|
|
1094
|
+
console.print("[yellow]Nothing to commit, working tree clean.[/yellow]")
|
|
1095
|
+
sys.exit(1)
|
|
1096
|
+
|
|
1097
|
+
# Generate or use provided message
|
|
1098
|
+
if not message:
|
|
1099
|
+
console.print("\n[bold]Generating commit message...[/bold]")
|
|
1100
|
+
with console.status("[bold green]Analyzing changes..."):
|
|
1101
|
+
generator = CommitMessageGenerator(llm)
|
|
1102
|
+
message = generator.generate(git_mgr)
|
|
1103
|
+
|
|
1104
|
+
if not message:
|
|
1105
|
+
console.print("[red]Error:[/red] Could not generate commit message")
|
|
1106
|
+
sys.exit(1)
|
|
1107
|
+
|
|
1108
|
+
# Show commit preview
|
|
1109
|
+
diff = git_mgr.get_diff(staged=True)
|
|
1110
|
+
console.print(f"\n[bold]Commit Preview[/bold]")
|
|
1111
|
+
console.print(f" Files: [cyan]{diff.files_changed}[/cyan]")
|
|
1112
|
+
console.print(f" Changes: [green]+{diff.insertions}[/green] / [red]-{diff.deletions}[/red]")
|
|
1113
|
+
console.print(f"\n[bold]Message:[/bold]")
|
|
1114
|
+
console.print(Panel(message, border_style="cyan"))
|
|
1115
|
+
|
|
1116
|
+
# Confirm
|
|
1117
|
+
if not no_confirm:
|
|
1118
|
+
if not click.confirm("\nProceed with commit?"):
|
|
1119
|
+
console.print("[dim]Cancelled[/dim]")
|
|
1120
|
+
return
|
|
1121
|
+
|
|
1122
|
+
# Commit
|
|
1123
|
+
try:
|
|
1124
|
+
commit_hash = git_mgr.commit(message)
|
|
1125
|
+
console.print(f"\n[green]✓ Committed:[/green] {commit_hash}")
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
console.print(f"[red]Error committing:[/red] {e}")
|
|
1128
|
+
sys.exit(1)
|
|
1129
|
+
|
|
1130
|
+
# Push if requested
|
|
1131
|
+
if push_after:
|
|
1132
|
+
console.print("\n[bold]Pushing to remote...[/bold]")
|
|
1133
|
+
success, output = git_mgr.push()
|
|
1134
|
+
if success:
|
|
1135
|
+
console.print(f"[green]✓ Pushed to {status.remote}/{status.branch}[/green]")
|
|
1136
|
+
else:
|
|
1137
|
+
console.print(f"[red]Error pushing:[/red] {output}")
|
|
1138
|
+
sys.exit(1)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
@git.command("push")
|
|
1142
|
+
@click.option("--remote", "-r", default="origin", help="Remote name")
|
|
1143
|
+
@click.option("--branch", "-b", help="Branch name (default: current branch)")
|
|
1144
|
+
@click.option("--set-upstream", "-u", is_flag=True, help="Set upstream for the branch")
|
|
1145
|
+
@click.pass_context
|
|
1146
|
+
def git_push(ctx, remote: str, branch: Optional[str], set_upstream: bool):
|
|
1147
|
+
"""Push commits to remote repository.
|
|
1148
|
+
|
|
1149
|
+
Examples:
|
|
1150
|
+
ai-assist git push
|
|
1151
|
+
ai-assist git push --remote origin --branch main
|
|
1152
|
+
ai-assist git push -u # Set upstream
|
|
1153
|
+
"""
|
|
1154
|
+
from ai_code_assistant.git import GitManager
|
|
1155
|
+
|
|
1156
|
+
try:
|
|
1157
|
+
git_mgr = GitManager()
|
|
1158
|
+
except ValueError as e:
|
|
1159
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1160
|
+
sys.exit(1)
|
|
1161
|
+
|
|
1162
|
+
status = git_mgr.get_status()
|
|
1163
|
+
branch = branch or status.branch
|
|
1164
|
+
|
|
1165
|
+
console.print(f"\n[bold]Pushing to {remote}/{branch}...[/bold]")
|
|
1166
|
+
|
|
1167
|
+
with console.status("[bold green]Pushing..."):
|
|
1168
|
+
success, output = git_mgr.push(remote=remote, branch=branch, set_upstream=set_upstream)
|
|
1169
|
+
|
|
1170
|
+
if success:
|
|
1171
|
+
console.print(f"[green]✓ Successfully pushed to {remote}/{branch}[/green]")
|
|
1172
|
+
|
|
1173
|
+
# Show remote URL
|
|
1174
|
+
remote_url = git_mgr.get_remote_url()
|
|
1175
|
+
if "github.com" in remote_url:
|
|
1176
|
+
# Convert SSH to HTTPS URL for display
|
|
1177
|
+
if remote_url.startswith("git@"):
|
|
1178
|
+
remote_url = remote_url.replace("git@github.com:", "https://github.com/").replace(".git", "")
|
|
1179
|
+
console.print(f"\n[dim]View at: {remote_url}[/dim]")
|
|
1180
|
+
else:
|
|
1181
|
+
console.print(f"[red]Error:[/red] Push failed")
|
|
1182
|
+
console.print(f"[dim]{output}[/dim]")
|
|
1183
|
+
sys.exit(1)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
@git.command("sync")
|
|
1187
|
+
@click.option("--message", "-m", help="Commit message (AI generates if not provided)")
|
|
1188
|
+
@click.option("--no-confirm", is_flag=True, help="Skip confirmation prompt")
|
|
1189
|
+
@click.pass_context
|
|
1190
|
+
def git_sync(ctx, message: Optional[str], no_confirm: bool):
|
|
1191
|
+
"""Stage all changes, commit with AI message, and push.
|
|
1192
|
+
|
|
1193
|
+
This is a convenience command that combines:
|
|
1194
|
+
1. git add -A
|
|
1195
|
+
2. git commit (with AI-generated message)
|
|
1196
|
+
3. git push
|
|
1197
|
+
|
|
1198
|
+
Examples:
|
|
1199
|
+
ai-assist git sync # Full sync with AI message
|
|
1200
|
+
ai-assist git sync -m "Update" # Sync with custom message
|
|
1201
|
+
ai-assist git sync --no-confirm # Skip confirmation
|
|
1202
|
+
"""
|
|
1203
|
+
from ai_code_assistant.git import GitManager, CommitMessageGenerator
|
|
1204
|
+
|
|
1205
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1206
|
+
|
|
1207
|
+
try:
|
|
1208
|
+
git_mgr = GitManager()
|
|
1209
|
+
except ValueError as e:
|
|
1210
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1211
|
+
sys.exit(1)
|
|
1212
|
+
|
|
1213
|
+
status = git_mgr.get_status()
|
|
1214
|
+
|
|
1215
|
+
if not status.has_changes:
|
|
1216
|
+
console.print("[yellow]Nothing to sync, working tree clean.[/yellow]")
|
|
1217
|
+
return
|
|
1218
|
+
|
|
1219
|
+
console.print(f"\n[bold]Git Sync[/bold]")
|
|
1220
|
+
console.print(f" Branch: [cyan]{status.branch}[/cyan]")
|
|
1221
|
+
console.print(f" Changes: [cyan]{status.total_changes} files[/cyan]")
|
|
1222
|
+
|
|
1223
|
+
# Stage all
|
|
1224
|
+
console.print("\n[dim]Staging all changes...[/dim]")
|
|
1225
|
+
git_mgr.stage_all()
|
|
1226
|
+
status = git_mgr.get_status()
|
|
1227
|
+
|
|
1228
|
+
# Generate message if not provided
|
|
1229
|
+
if not message:
|
|
1230
|
+
console.print("[dim]Generating commit message...[/dim]")
|
|
1231
|
+
with console.status("[bold green]Analyzing changes..."):
|
|
1232
|
+
generator = CommitMessageGenerator(llm)
|
|
1233
|
+
message = generator.generate(git_mgr)
|
|
1234
|
+
|
|
1235
|
+
if not message:
|
|
1236
|
+
console.print("[red]Error:[/red] Could not generate commit message")
|
|
1237
|
+
sys.exit(1)
|
|
1238
|
+
|
|
1239
|
+
# Show preview
|
|
1240
|
+
diff = git_mgr.get_diff(staged=True)
|
|
1241
|
+
console.print(f"\n[bold]Sync Preview[/bold]")
|
|
1242
|
+
console.print(f" Files: [cyan]{diff.files_changed}[/cyan]")
|
|
1243
|
+
console.print(f" Changes: [green]+{diff.insertions}[/green] / [red]-{diff.deletions}[/red]")
|
|
1244
|
+
console.print(f" Push to: [cyan]{status.remote}/{status.branch}[/cyan]")
|
|
1245
|
+
console.print(f"\n[bold]Message:[/bold]")
|
|
1246
|
+
console.print(Panel(message, border_style="cyan"))
|
|
1247
|
+
|
|
1248
|
+
# Confirm
|
|
1249
|
+
if not no_confirm:
|
|
1250
|
+
if not click.confirm("\nProceed with sync?"):
|
|
1251
|
+
console.print("[dim]Cancelled[/dim]")
|
|
1252
|
+
return
|
|
1253
|
+
|
|
1254
|
+
# Commit
|
|
1255
|
+
console.print("\n[dim]Committing...[/dim]")
|
|
1256
|
+
try:
|
|
1257
|
+
commit_hash = git_mgr.commit(message)
|
|
1258
|
+
console.print(f"[green]✓ Committed:[/green] {commit_hash}")
|
|
1259
|
+
except Exception as e:
|
|
1260
|
+
console.print(f"[red]Error committing:[/red] {e}")
|
|
1261
|
+
sys.exit(1)
|
|
1262
|
+
|
|
1263
|
+
# Push
|
|
1264
|
+
console.print("[dim]Pushing...[/dim]")
|
|
1265
|
+
success, output = git_mgr.push()
|
|
1266
|
+
if success:
|
|
1267
|
+
console.print(f"[green]✓ Pushed to {status.remote}/{status.branch}[/green]")
|
|
1268
|
+
|
|
1269
|
+
# Show GitHub URL
|
|
1270
|
+
remote_url = git_mgr.get_remote_url()
|
|
1271
|
+
if "github.com" in remote_url:
|
|
1272
|
+
if remote_url.startswith("git@"):
|
|
1273
|
+
remote_url = remote_url.replace("git@github.com:", "https://github.com/").replace(".git", "")
|
|
1274
|
+
elif remote_url.endswith(".git"):
|
|
1275
|
+
remote_url = remote_url[:-4]
|
|
1276
|
+
console.print(f"\n[bold green]✓ Sync complete![/bold green]")
|
|
1277
|
+
console.print(f"[dim]View at: {remote_url}[/dim]")
|
|
1278
|
+
else:
|
|
1279
|
+
console.print(f"[red]Error pushing:[/red] {output}")
|
|
1280
|
+
sys.exit(1)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
@git.command("log")
|
|
1284
|
+
@click.option("--count", "-n", default=10, help="Number of commits to show")
|
|
1285
|
+
@click.pass_context
|
|
1286
|
+
def git_log(ctx, count: int):
|
|
1287
|
+
"""Show recent commit history.
|
|
1288
|
+
|
|
1289
|
+
Examples:
|
|
1290
|
+
ai-assist git log
|
|
1291
|
+
ai-assist git log -n 20
|
|
1292
|
+
"""
|
|
1293
|
+
from ai_code_assistant.git import GitManager
|
|
1294
|
+
|
|
1295
|
+
try:
|
|
1296
|
+
git_mgr = GitManager()
|
|
1297
|
+
except ValueError as e:
|
|
1298
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1299
|
+
sys.exit(1)
|
|
1300
|
+
|
|
1301
|
+
commits = git_mgr.get_recent_commits(count=count)
|
|
1302
|
+
|
|
1303
|
+
console.print(f"\n[bold]Recent Commits[/bold]\n")
|
|
1304
|
+
|
|
1305
|
+
for commit in commits:
|
|
1306
|
+
console.print(f"[yellow]{commit['short_hash']}[/yellow] {commit['message']}")
|
|
1307
|
+
console.print(f" [dim]{commit['author']} • {commit['time']}[/dim]")
|
|
1308
|
+
|
|
1309
|
+
# =============================================================================
|
|
1310
|
+
# Agent Commands
|
|
1311
|
+
# =============================================================================
|
|
1312
|
+
|
|
1313
|
+
@main.command()
|
|
1314
|
+
@click.option("--path", "-p", type=click.Path(exists=True, path_type=Path),
|
|
1315
|
+
default=".", help="Project root path")
|
|
1316
|
+
@click.pass_context
|
|
1317
|
+
def agent(ctx, path: Path):
|
|
1318
|
+
"""Start interactive AI code agent.
|
|
1319
|
+
|
|
1320
|
+
The agent can:
|
|
1321
|
+
- Generate code based on descriptions
|
|
1322
|
+
- Review files for issues
|
|
1323
|
+
- Edit existing code
|
|
1324
|
+
- Explain how code works
|
|
1325
|
+
- Refactor code
|
|
1326
|
+
- Generate tests
|
|
1327
|
+
|
|
1328
|
+
Examples:
|
|
1329
|
+
ai-assist agent
|
|
1330
|
+
ai-assist agent --path ./my-project
|
|
1331
|
+
|
|
1332
|
+
In agent mode, try commands like:
|
|
1333
|
+
"create a function to validate email addresses"
|
|
1334
|
+
"review src/main.py"
|
|
1335
|
+
"explain how the config module works"
|
|
1336
|
+
"refactor utils.py to use async"
|
|
1337
|
+
"generate tests for src/api.py"
|
|
1338
|
+
"""
|
|
1339
|
+
from ai_code_assistant.chat import AgentChatSession
|
|
1340
|
+
from rich.markdown import Markdown
|
|
1341
|
+
from rich.prompt import Prompt
|
|
1342
|
+
|
|
1343
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1344
|
+
|
|
1345
|
+
# Initialize agent session
|
|
1346
|
+
session = AgentChatSession(config, llm, path.resolve())
|
|
1347
|
+
|
|
1348
|
+
# Show welcome message
|
|
1349
|
+
console.print(Panel.fit(
|
|
1350
|
+
"[bold cyan]🤖 AI Code Agent[/bold cyan]\n\n"
|
|
1351
|
+
"I can help you with:\n"
|
|
1352
|
+
" • [green]Generate[/green] - Create new code\n"
|
|
1353
|
+
" • [yellow]Review[/yellow] - Analyze code for issues\n"
|
|
1354
|
+
" • [blue]Edit[/blue] - Modify existing files\n"
|
|
1355
|
+
" • [magenta]Explain[/magenta] - Understand code\n"
|
|
1356
|
+
" • [cyan]Refactor[/cyan] - Improve code quality\n"
|
|
1357
|
+
" • [white]Test[/white] - Generate unit tests\n\n"
|
|
1358
|
+
"Type [bold]'help'[/bold] for examples, [bold]'quit'[/bold] to exit.",
|
|
1359
|
+
title="Welcome",
|
|
1360
|
+
border_style="cyan",
|
|
1361
|
+
))
|
|
1362
|
+
|
|
1363
|
+
# Show project info
|
|
1364
|
+
project_info = session.get_project_info()
|
|
1365
|
+
console.print(f"\n{project_info}\n")
|
|
1366
|
+
|
|
1367
|
+
# Main loop
|
|
1368
|
+
while True:
|
|
1369
|
+
try:
|
|
1370
|
+
user_input = Prompt.ask("\n[bold cyan]You[/bold cyan]")
|
|
1371
|
+
|
|
1372
|
+
if not user_input.strip():
|
|
1373
|
+
continue
|
|
1374
|
+
|
|
1375
|
+
# Handle special commands
|
|
1376
|
+
lower_input = user_input.lower().strip()
|
|
1377
|
+
|
|
1378
|
+
if lower_input in ("quit", "exit", "q"):
|
|
1379
|
+
console.print("[dim]Goodbye![/dim]")
|
|
1380
|
+
break
|
|
1381
|
+
|
|
1382
|
+
if lower_input == "help":
|
|
1383
|
+
_show_agent_help()
|
|
1384
|
+
continue
|
|
1385
|
+
|
|
1386
|
+
if lower_input == "clear":
|
|
1387
|
+
session.clear_history()
|
|
1388
|
+
console.print("[dim]History cleared.[/dim]")
|
|
1389
|
+
continue
|
|
1390
|
+
|
|
1391
|
+
if lower_input == "status":
|
|
1392
|
+
if session.has_pending_changes:
|
|
1393
|
+
console.print("[yellow]You have pending changes awaiting confirmation.[/yellow]")
|
|
1394
|
+
else:
|
|
1395
|
+
console.print("[green]No pending changes.[/green]")
|
|
1396
|
+
continue
|
|
1397
|
+
|
|
1398
|
+
# Process through agent
|
|
1399
|
+
with console.status("[bold green]Thinking..."):
|
|
1400
|
+
response = session.send_message(user_input)
|
|
1401
|
+
|
|
1402
|
+
# Display response
|
|
1403
|
+
console.print(f"\n[bold green]Agent[/bold green]")
|
|
1404
|
+
|
|
1405
|
+
# Use markdown rendering for better formatting
|
|
1406
|
+
try:
|
|
1407
|
+
console.print(Markdown(response.content))
|
|
1408
|
+
except Exception:
|
|
1409
|
+
console.print(response.content)
|
|
1410
|
+
|
|
1411
|
+
# Show confirmation prompt if needed
|
|
1412
|
+
if response.pending_action:
|
|
1413
|
+
console.print("\n[yellow]Apply these changes? (yes/no)[/yellow]")
|
|
1414
|
+
|
|
1415
|
+
except KeyboardInterrupt:
|
|
1416
|
+
console.print("\n[dim]Use 'quit' to exit[/dim]")
|
|
1417
|
+
except Exception as e:
|
|
1418
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
def _show_agent_help():
|
|
1422
|
+
"""Show agent help message."""
|
|
1423
|
+
help_text = """
|
|
1424
|
+
[bold]Code Generation[/bold]
|
|
1425
|
+
• "create a function to parse JSON files"
|
|
1426
|
+
• "write a class for managing database connections"
|
|
1427
|
+
• "generate a REST API endpoint for users"
|
|
1428
|
+
|
|
1429
|
+
[bold]Code Review[/bold]
|
|
1430
|
+
• "review src/main.py"
|
|
1431
|
+
• "check utils.py for security issues"
|
|
1432
|
+
• "analyze the config module"
|
|
1433
|
+
|
|
1434
|
+
[bold]Code Editing[/bold]
|
|
1435
|
+
• "edit src/api.py to add error handling"
|
|
1436
|
+
• "fix the bug in utils.py line 42"
|
|
1437
|
+
• "add logging to the main function"
|
|
1438
|
+
|
|
1439
|
+
[bold]Code Explanation[/bold]
|
|
1440
|
+
• "explain src/config.py"
|
|
1441
|
+
• "how does the authentication work?"
|
|
1442
|
+
• "what does this function do?"
|
|
1443
|
+
|
|
1444
|
+
[bold]Refactoring[/bold]
|
|
1445
|
+
• "refactor utils.py to use async/await"
|
|
1446
|
+
• "improve the code quality of main.py"
|
|
1447
|
+
• "simplify the database module"
|
|
1448
|
+
|
|
1449
|
+
[bold]Test Generation[/bold]
|
|
1450
|
+
• "generate tests for src/api.py"
|
|
1451
|
+
• "write unit tests for the User class"
|
|
1452
|
+
• "create pytest tests for utils.py"
|
|
1453
|
+
|
|
1454
|
+
[bold]Project Info[/bold]
|
|
1455
|
+
• "show project structure"
|
|
1456
|
+
• "what languages are used?"
|
|
1457
|
+
• "list all Python files"
|
|
1458
|
+
|
|
1459
|
+
[bold]Special Commands[/bold]
|
|
1460
|
+
• help - Show this help
|
|
1461
|
+
• status - Check for pending changes
|
|
1462
|
+
• clear - Clear conversation history
|
|
1463
|
+
• quit - Exit agent mode
|
|
1464
|
+
"""
|
|
1465
|
+
console.print(Panel(help_text, title="Agent Help", border_style="cyan"))
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
@main.command("agent-review")
|
|
1469
|
+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
|
|
1470
|
+
@click.option("--path", "-p", type=click.Path(exists=True, path_type=Path),
|
|
1471
|
+
default=".", help="Project root path")
|
|
1472
|
+
@click.pass_context
|
|
1473
|
+
def agent_review(ctx, file: Path, path: Path):
|
|
1474
|
+
"""Quick code review using the agent.
|
|
1475
|
+
|
|
1476
|
+
Examples:
|
|
1477
|
+
ai-assist agent-review src/main.py
|
|
1478
|
+
ai-assist agent-review utils.py --path ./my-project
|
|
1479
|
+
"""
|
|
1480
|
+
from ai_code_assistant.agent import CodeAgent
|
|
1481
|
+
|
|
1482
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1483
|
+
agent = CodeAgent(llm, path.resolve())
|
|
1484
|
+
|
|
1485
|
+
console.print(f"\n[bold]Reviewing {file}...[/bold]\n")
|
|
1486
|
+
|
|
1487
|
+
with console.status("[bold green]Analyzing..."):
|
|
1488
|
+
response = agent.process(f"review {file}")
|
|
1489
|
+
|
|
1490
|
+
console.print(response.message)
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
@main.command("agent-generate")
|
|
1494
|
+
@click.argument("description")
|
|
1495
|
+
@click.option("--file", "-f", type=click.Path(path_type=Path), help="Output file path")
|
|
1496
|
+
@click.option("--language", "-l", help="Programming language")
|
|
1497
|
+
@click.option("--path", "-p", type=click.Path(exists=True, path_type=Path),
|
|
1498
|
+
default=".", help="Project root path")
|
|
1499
|
+
@click.option("--apply", "-a", is_flag=True, help="Apply changes without confirmation")
|
|
1500
|
+
@click.pass_context
|
|
1501
|
+
def agent_generate(ctx, description: str, file: Optional[Path], language: Optional[str],
|
|
1502
|
+
path: Path, apply: bool):
|
|
1503
|
+
"""Generate code using the agent.
|
|
1504
|
+
|
|
1505
|
+
Examples:
|
|
1506
|
+
ai-assist agent-generate "a function to validate email"
|
|
1507
|
+
ai-assist agent-generate "REST API for users" -f src/api.py
|
|
1508
|
+
ai-assist agent-generate "sorting algorithm" -l python
|
|
1509
|
+
"""
|
|
1510
|
+
from ai_code_assistant.agent import CodeAgent
|
|
1511
|
+
|
|
1512
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1513
|
+
agent = CodeAgent(llm, path.resolve())
|
|
1514
|
+
|
|
1515
|
+
# Build the request
|
|
1516
|
+
request = description
|
|
1517
|
+
if file:
|
|
1518
|
+
request = f"create {file}: {description}"
|
|
1519
|
+
if language:
|
|
1520
|
+
request = f"{request} in {language}"
|
|
1521
|
+
|
|
1522
|
+
console.print(f"\n[bold]Generating code...[/bold]\n")
|
|
1523
|
+
|
|
1524
|
+
with console.status("[bold green]Generating..."):
|
|
1525
|
+
response = agent.process(request)
|
|
1526
|
+
|
|
1527
|
+
console.print(response.message)
|
|
1528
|
+
|
|
1529
|
+
if response.requires_confirmation:
|
|
1530
|
+
if apply:
|
|
1531
|
+
success, msg = agent.confirm_changes()
|
|
1532
|
+
console.print(f"\n{msg}")
|
|
1533
|
+
else:
|
|
1534
|
+
if click.confirm("\nApply these changes?"):
|
|
1535
|
+
success, msg = agent.confirm_changes()
|
|
1536
|
+
console.print(f"\n{msg}")
|
|
1537
|
+
else:
|
|
1538
|
+
console.print("[dim]Changes discarded.[/dim]")
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
@main.command("agent-explain")
|
|
1542
|
+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
|
|
1543
|
+
@click.option("--path", "-p", type=click.Path(exists=True, path_type=Path),
|
|
1544
|
+
default=".", help="Project root path")
|
|
1545
|
+
@click.pass_context
|
|
1546
|
+
def agent_explain(ctx, file: Path, path: Path):
|
|
1547
|
+
"""Explain code using the agent.
|
|
1548
|
+
|
|
1549
|
+
Examples:
|
|
1550
|
+
ai-assist agent-explain src/main.py
|
|
1551
|
+
ai-assist agent-explain config.py --path ./my-project
|
|
1552
|
+
"""
|
|
1553
|
+
from ai_code_assistant.agent import CodeAgent
|
|
1554
|
+
from rich.markdown import Markdown
|
|
1555
|
+
|
|
1556
|
+
config, llm = get_components(ctx.obj.get("config_path"))
|
|
1557
|
+
agent = CodeAgent(llm, path.resolve())
|
|
1558
|
+
|
|
1559
|
+
console.print(f"\n[bold]Explaining {file}...[/bold]\n")
|
|
1560
|
+
|
|
1561
|
+
with console.status("[bold green]Analyzing..."):
|
|
1562
|
+
response = agent.process(f"explain {file}")
|
|
1563
|
+
|
|
1564
|
+
try:
|
|
1565
|
+
console.print(Markdown(response.message))
|
|
1566
|
+
except Exception:
|
|
1567
|
+
console.print(response.message)
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
if __name__ == "__main__":
|
|
1571
|
+
main()
|