-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
290 lines (229 loc) · 9.34 KB
/
cli.py
File metadata and controls
290 lines (229 loc) · 9.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/usr/bin/env python3
"""
CodeLens – CLI interface.
Fixes vs original:
- Correct langgraph import in chat command
- Env loading via python-dotenv (one place, not repeated)
- Config imported from config.py
- Removed duplicate load_env_file() implementations
"""
import os
import sys
import click
from pathlib import Path
from dotenv import load_dotenv
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from rich.markdown import Markdown
from rich.panel import Panel
from config import LLM_MODEL, EMBED_MODEL, DB_PATH, REPO_PATH
from create_db import main as create_database
from tools import (
codebase_search, read_file, write_file, list_files,
run_terminal_command, get_directory_tree, grep_search,
get_file_outline, ALL_TOOLS,
)
load_dotenv()
console = Console()
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""
🔍 CodeLens — AI-powered code intelligence agent.
Understand, search, and analyze any codebase using natural language.
"""
pass
@cli.command()
@click.option("--path", default=None, help="Path to the project directory")
@click.option("--db-path", default=None, help="Path to store the vector database")
@click.option("--batch-size", default=50, type=int, help="Embedding batch size")
@click.option("--force", is_flag=True, help="Wipe and rebuild existing database")
def init(path, db_path, batch_size, force):
"""
Index a codebase into the vector database.
Example: codelens init --path ./my-project
"""
path = path or REPO_PATH
db_path = db_path or DB_PATH
console.print(Panel.fit(
f"🔧 Indexing [bold cyan]{path}[/bold cyan] → [bold]{db_path}[/bold]",
title="CodeLens Init",
))
os.environ["CODELENS_REPO_PATH"] = path
os.environ["CODELENS_DB_PATH"] = db_path
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
task = progress.add_task("Indexing files...", total=None)
try:
create_database(repo_path=path, db_path=db_path, force=force)
progress.update(task, completed=True)
console.print("✅ [bold green]Database ready![/bold green]")
except Exception as exc:
console.print(f"❌ [bold red]Error:[/bold red] {exc}")
sys.exit(1)
@cli.command()
@click.argument("question")
@click.option("--context", default=5, help="Number of context chunks to retrieve")
@click.option("--show-sources", is_flag=True, help="Show source file paths")
def ask(question, context, show_sources):
"""
Ask a semantic question about the indexed codebase.
Example: codelens ask "How is authentication handled?"
"""
console.print(f"\n🔍 [bold]Searching for:[/bold] {question}\n")
result = codebase_search.invoke({"query": question})
if "Error" in result:
console.print(f"❌ [bold red]{result}[/bold red]")
console.print("\n💡 Run [cyan]codelens init --path <project>[/cyan] first.")
sys.exit(1)
sections = result.split("--- Source:")
shown = 0
for i, section in enumerate(sections[1:], 1):
if shown >= context:
break
if not section.strip():
continue
lines = section.strip().split("\n", 1)
source = lines[0].strip().replace("---", "").strip()
content = lines[1] if len(lines) > 1 else ""
if show_sources:
console.print(f"\n[bold cyan]📄 Source {i}:[/bold cyan] {source}")
console.print(Panel(
content[:500] + ("…" if len(content) > 500 else ""),
title=f"Match {i}",
border_style="green",
))
shown += 1
@cli.command()
@click.argument("file_path")
@click.option("--type", "review_type",
type=click.Choice(["security", "performance", "style", "all"]),
default="all")
def review(file_path, review_type):
"""
Show outline and preview of a file for quick code review.
Example: codelens review ./src/main.py --type security
"""
console.print(f"\n📝 [bold]Reviewing:[/bold] {file_path}\n")
if not os.path.exists(file_path):
console.print(f"❌ [bold red]File not found:[/bold red] {file_path}")
sys.exit(1)
content = read_file.invoke({"file_path": file_path})
outline = get_file_outline.invoke({"file_path": file_path})
console.print("[bold cyan]File Outline:[/bold cyan]")
console.print(outline)
console.print(f"\n[bold cyan]Review type:[/bold cyan] {review_type}")
lines = content.split("\n")[:20]
console.print(Panel(
"\n".join(lines),
title="File preview (first 20 lines)",
border_style="blue",
))
@cli.command()
@click.option("--path", default=".", help="Directory to display")
@click.option("--max-depth", default=2, help="Maximum depth to traverse")
def tree(path, max_depth):
"""
Display project directory tree.
Example: codelens tree --path ./src --max-depth 3
"""
console.print(f"\n📁 [bold]Project structure:[/bold] {path}\n")
result = get_directory_tree.invoke({"directory": path, "max_depth": max_depth})
console.print(result)
@cli.command()
@click.argument("query")
@click.option("--path", default=".", help="Directory to search in")
@click.option("--regex", is_flag=True, help="Treat query as a regex pattern")
def search(query, path, regex):
"""
Exact string or regex search across the codebase.
Example: codelens search "def authenticate" --path ./src
"""
console.print(f"\n🔎 [bold]Searching:[/bold] '{query}' in {path}\n")
result = grep_search.invoke({"query": query, "path": path, "is_regex": regex})
if "No matches found" in result:
console.print("❌ [yellow]No matches found[/yellow]")
return
matches = [l for l in result.split("\n") if l.strip() and not l.startswith("Matches:")]
console.print(f"✅ Found [bold green]{len(matches)}[/bold green] matches:\n")
for m in matches[:20]:
console.print(m)
if len(matches) > 20:
console.print(f"\n… and {len(matches) - 20} more matches.")
@cli.command()
@click.argument("file_path")
def outline(file_path):
"""
Show classes and functions defined in a file.
Example: codelens outline ./src/agent.py
"""
console.print(f"\n📋 [bold]Outline:[/bold] {file_path}\n")
if not os.path.exists(file_path):
console.print(f"❌ [bold red]File not found:[/bold red] {file_path}")
sys.exit(1)
result = get_file_outline.invoke({"file_path": file_path})
table = Table(title=os.path.basename(file_path))
table.add_column("Line", style="cyan", no_wrap=True)
table.add_column("Definition", style="green")
for line in result.split("\n"):
if line.strip():
parts = line.split(": ", 1)
if len(parts) == 2:
table.add_row(parts[0], parts[1])
console.print(table)
@cli.command()
def chat():
"""
Start an interactive AI chat session about the codebase.
The agent can read files, search code, and suggest edits.
"""
from langchain_groq import ChatGroq
from langgraph.prebuilt import create_react_agent
from agent import SYSTEM_PROMPT
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
console.print("❌ [bold red]GROQ_API_KEY not set.[/bold red] Add it to your .env file.")
sys.exit(1)
llm = ChatGroq(model_name=LLM_MODEL, groq_api_key=api_key, temperature=0)
agent = create_react_agent(model=llm, tools=ALL_TOOLS, prompt=SYSTEM_PROMPT)
console.print(Panel.fit(
"🔍 [bold]CodeLens – Interactive Mode[/bold]\n"
"Ask anything about your codebase.\n"
"Type [cyan]exit[/cyan] to quit.",
border_style="cyan",
))
while True:
try:
query = console.input("\n[bold cyan]You:[/bold cyan] ").strip()
except (EOFError, KeyboardInterrupt):
console.print("\n\n👋 [bold]Goodbye![/bold]")
break
if query.lower() in ("exit", "quit", "q"):
console.print("👋 [bold]Goodbye![/bold]")
break
if not query:
continue
console.print("\n[bold yellow]CodeLens:[/bold yellow] Thinking…\n")
try:
result = agent.invoke({"messages": [{"role": "user", "content": query}]})
answer = result["messages"][-1].content
console.print(Panel(Markdown(answer), title="Response", border_style="green"))
except Exception as exc:
console.print(f"\n❌ [bold red]Error:[/bold red] {exc}")
@cli.command()
def info():
"""Show CodeLens configuration and status."""
table = Table(title="CodeLens – System Info")
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
db_exists = os.path.exists(DB_PATH)
table.add_row("Version", "1.0.0")
table.add_row("Repo path", REPO_PATH)
table.add_row("Database path", DB_PATH)
table.add_row("Database ready", "✅ Yes" if db_exists else "❌ No — run codelens init")
table.add_row("Groq API key", "✅ Set" if os.getenv("GROQ_API_KEY") else "❌ Not set")
table.add_row("Embedding model", EMBED_MODEL)
table.add_row("LLM model", LLM_MODEL)
console.print(table)
if __name__ == "__main__":
cli()