cortexcode 0.1.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.
- cortexcode/__init__.py +3 -0
- cortexcode/analysis.py +331 -0
- cortexcode/cli.py +845 -0
- cortexcode/context.py +298 -0
- cortexcode/dashboard.py +152 -0
- cortexcode/docs.py +1266 -0
- cortexcode/git_diff.py +157 -0
- cortexcode/indexer.py +1860 -0
- cortexcode/lsp_server.py +315 -0
- cortexcode/mcp_server.py +455 -0
- cortexcode/plugins.py +188 -0
- cortexcode/semantic_search.py +237 -0
- cortexcode/vuln_scan.py +241 -0
- cortexcode/watcher.py +122 -0
- cortexcode/workspace.py +180 -0
- cortexcode-0.1.0.dist-info/METADATA +448 -0
- cortexcode-0.1.0.dist-info/RECORD +21 -0
- cortexcode-0.1.0.dist-info/WHEEL +5 -0
- cortexcode-0.1.0.dist-info/entry_points.txt +2 -0
- cortexcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- cortexcode-0.1.0.dist-info/top_level.txt +1 -0
cortexcode/cli.py
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"""CLI - Command-line interface for CortexCode."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
|
|
11
|
+
from rich.prompt import Confirm, Prompt
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich import box
|
|
14
|
+
|
|
15
|
+
from cortexcode import indexer
|
|
16
|
+
from cortexcode.context import get_context, calculate_token_savings
|
|
17
|
+
from cortexcode.git_diff import get_diff_context
|
|
18
|
+
from cortexcode.docs import generate_all_docs
|
|
19
|
+
from cortexcode.watcher import start_watcher
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
@click.version_option(version="0.1.0", prog_name="cortexcode")
|
|
27
|
+
def main():
|
|
28
|
+
"""CortexCode - Lightweight code indexing for AI assistants."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@main.command()
|
|
33
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
34
|
+
@click.option("-o", "--output", default=".cortexcode/index.json", help="Output file path")
|
|
35
|
+
@click.option("-v", "--verbose", is_flag=True, help="Show verbose output")
|
|
36
|
+
@click.option("-w", "--watch", is_flag=True, help="Start watching after indexing")
|
|
37
|
+
@click.option("-i", "--incremental", is_flag=True, help="Only re-index changed files")
|
|
38
|
+
def index(path, output, verbose, watch, incremental):
|
|
39
|
+
"""Index a directory and save the code graph."""
|
|
40
|
+
path = Path(path).resolve()
|
|
41
|
+
output = Path(output)
|
|
42
|
+
|
|
43
|
+
console.print(Panel.fit(
|
|
44
|
+
f"[bold cyan]⚡ CortexCode Indexer[/bold cyan]\n"
|
|
45
|
+
f"[dim]Scanning: {path}[/dim]" + (" (incremental)" if incremental else ""),
|
|
46
|
+
border_style="cyan"
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
with Progress(
|
|
50
|
+
SpinnerColumn(),
|
|
51
|
+
TextColumn("[progress.description]{task.description}"),
|
|
52
|
+
BarColumn(bar_width=40),
|
|
53
|
+
TaskProgressColumn(),
|
|
54
|
+
TimeRemainingColumn(),
|
|
55
|
+
console=console,
|
|
56
|
+
) as progress:
|
|
57
|
+
task = progress.add_task("[cyan]Indexing files...", total=None)
|
|
58
|
+
|
|
59
|
+
start_time = time.time()
|
|
60
|
+
index_data = indexer.index_directory(path, incremental=incremental)
|
|
61
|
+
elapsed = time.time() - start_time
|
|
62
|
+
|
|
63
|
+
progress.update(task, completed=True)
|
|
64
|
+
|
|
65
|
+
indexer.save_index(index_data, output)
|
|
66
|
+
|
|
67
|
+
file_count = len(index_data.get("files", {}))
|
|
68
|
+
symbol_count = sum(len(s.get("symbols", [])) if isinstance(s, dict) else len(s) for s in index_data.get("files", {}).values())
|
|
69
|
+
languages = index_data.get("languages", [])
|
|
70
|
+
|
|
71
|
+
table = Table(box=box.ROUNDED, show_header=False)
|
|
72
|
+
table.add_column("Key", style="cyan")
|
|
73
|
+
table.add_column("Value", style="white")
|
|
74
|
+
|
|
75
|
+
table.add_row("Files", f"[bold]{file_count}[/bold]")
|
|
76
|
+
table.add_row("Symbols", f"[bold]{symbol_count}[/bold]")
|
|
77
|
+
table.add_row("Languages", ", ".join(languages) if languages else "N/A")
|
|
78
|
+
table.add_row("Time", f"{elapsed:.2f}s")
|
|
79
|
+
table.add_row("Output", str(output))
|
|
80
|
+
if incremental:
|
|
81
|
+
table.add_row("Mode", "[yellow]Incremental[/yellow]")
|
|
82
|
+
|
|
83
|
+
console.print()
|
|
84
|
+
console.print(Panel(
|
|
85
|
+
table,
|
|
86
|
+
title="[bold green]✓ Indexing Complete[/bold green]",
|
|
87
|
+
border_style="green"
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
if verbose:
|
|
91
|
+
_show_index_summary(index_data)
|
|
92
|
+
|
|
93
|
+
if watch:
|
|
94
|
+
console.print(f"\n[yellow]Starting watcher...[/yellow]")
|
|
95
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
96
|
+
start_watcher(path, verbose=verbose)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@main.command()
|
|
100
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
101
|
+
@click.option("-v", "--verbose", is_flag=True, help="Show file change events")
|
|
102
|
+
def watch(path, verbose):
|
|
103
|
+
"""Watch for file changes and auto-reindex."""
|
|
104
|
+
path = Path(path).resolve()
|
|
105
|
+
|
|
106
|
+
console.print(f"[bold blue]Watching:[/bold blue] {path}")
|
|
107
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
108
|
+
|
|
109
|
+
start_watcher(path, verbose=verbose)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@main.command()
|
|
113
|
+
@click.argument("query", required=False)
|
|
114
|
+
@click.option("-n", "--num-results", default=5, help="Number of results to return")
|
|
115
|
+
@click.option("-f", "--format", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
116
|
+
@click.option("--tokens", is_flag=True, help="Show token savings estimate")
|
|
117
|
+
def context(query, num_results, output_format, tokens):
|
|
118
|
+
"""Get relevant context for AI assistants."""
|
|
119
|
+
index_path = Path(".cortexcode/index.json")
|
|
120
|
+
|
|
121
|
+
if not index_path.exists():
|
|
122
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
result = get_context(index_path, query, num_results)
|
|
126
|
+
|
|
127
|
+
if output_format == "json":
|
|
128
|
+
import json
|
|
129
|
+
console.print(json.dumps(result, indent=2))
|
|
130
|
+
else:
|
|
131
|
+
_print_context(result)
|
|
132
|
+
|
|
133
|
+
if tokens:
|
|
134
|
+
savings = calculate_token_savings(index_path, query, num_results)
|
|
135
|
+
_print_token_savings(savings)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@main.command()
|
|
139
|
+
@click.argument("query")
|
|
140
|
+
@click.option("-t", "--type", "sym_type", default=None, help="Filter by type (function, class, method, interface)")
|
|
141
|
+
@click.option("-f", "--file", "file_filter", default=None, help="Filter by file path")
|
|
142
|
+
@click.option("-n", "--limit", default=20, help="Max results")
|
|
143
|
+
def search(query, sym_type, file_filter, limit):
|
|
144
|
+
"""Search indexed symbols (grep-like)."""
|
|
145
|
+
index_path = Path(".cortexcode/index.json")
|
|
146
|
+
|
|
147
|
+
if not index_path.exists():
|
|
148
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
|
|
151
|
+
import json as _json
|
|
152
|
+
index = _json.loads(index_path.read_text(encoding="utf-8"))
|
|
153
|
+
files = index.get("files", {})
|
|
154
|
+
call_graph = index.get("call_graph", {})
|
|
155
|
+
query_lower = query.lower()
|
|
156
|
+
|
|
157
|
+
results = []
|
|
158
|
+
for rel_path, file_data in files.items():
|
|
159
|
+
if file_filter and file_filter.lower() not in rel_path.lower():
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
symbols = file_data.get("symbols", []) if isinstance(file_data, dict) else file_data
|
|
163
|
+
for sym in symbols:
|
|
164
|
+
name = sym.get("name", "").lower()
|
|
165
|
+
if query_lower in name:
|
|
166
|
+
if sym_type and sym.get("type") != sym_type:
|
|
167
|
+
continue
|
|
168
|
+
results.append({**sym, "file": rel_path})
|
|
169
|
+
|
|
170
|
+
if not results:
|
|
171
|
+
console.print(f"[yellow]No symbols matching '{query}'[/yellow]")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
console.print(f"\n[bold]Found {len(results)} symbols matching '{query}':[/bold]\n")
|
|
175
|
+
|
|
176
|
+
table = Table(box=box.SIMPLE, show_header=True)
|
|
177
|
+
table.add_column("Type", style="dim", width=10)
|
|
178
|
+
table.add_column("Name", style="cyan")
|
|
179
|
+
table.add_column("File", style="dim")
|
|
180
|
+
table.add_column("Line", justify="right")
|
|
181
|
+
table.add_column("Calls", style="dim")
|
|
182
|
+
|
|
183
|
+
for sym in results[:limit]:
|
|
184
|
+
calls = sym.get("calls", [])
|
|
185
|
+
calls_str = ", ".join(calls[:3]) + ("..." if len(calls) > 3 else "") if calls else ""
|
|
186
|
+
table.add_row(
|
|
187
|
+
sym.get("type", "?"),
|
|
188
|
+
sym.get("name", "?"),
|
|
189
|
+
sym.get("file", "?"),
|
|
190
|
+
str(sym.get("line", "?")),
|
|
191
|
+
calls_str,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
console.print(table)
|
|
195
|
+
|
|
196
|
+
if len(results) > limit:
|
|
197
|
+
console.print(f"\n[dim]Showing {limit}/{len(results)} results. Use -n to see more.[/dim]")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@main.command()
|
|
201
|
+
@click.option("--ref", default="HEAD", help="Git ref to compare against (default: HEAD)")
|
|
202
|
+
@click.option("-f", "--format", "output_format", default="text", type=click.Choice(["text", "json"]))
|
|
203
|
+
def diff(ref, output_format):
|
|
204
|
+
"""Show changed symbols since last commit (git diff-aware context)."""
|
|
205
|
+
index_path = Path(".cortexcode/index.json")
|
|
206
|
+
|
|
207
|
+
if not index_path.exists():
|
|
208
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
result = get_diff_context(index_path, ref)
|
|
212
|
+
|
|
213
|
+
if output_format == "json":
|
|
214
|
+
import json as _json
|
|
215
|
+
console.print(_json.dumps(result, indent=2))
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if result["changed_files"] == 0:
|
|
219
|
+
console.print("[green]No changes detected.[/green]")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
console.print(Panel.fit(
|
|
223
|
+
f"[bold cyan]Git Diff Context[/bold cyan]\n"
|
|
224
|
+
f"[dim]Comparing against: {ref}[/dim]",
|
|
225
|
+
border_style="cyan"
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
console.print(f"\n[bold]{result['changed_files']}[/bold] files changed\n")
|
|
229
|
+
|
|
230
|
+
# Changed symbols
|
|
231
|
+
changed = result.get("changed_symbols", [])
|
|
232
|
+
if changed:
|
|
233
|
+
table = Table(title="Changed Symbols", box=box.SIMPLE, show_header=True)
|
|
234
|
+
table.add_column("", width=3)
|
|
235
|
+
table.add_column("Type", style="dim", width=10)
|
|
236
|
+
table.add_column("Name", style="cyan")
|
|
237
|
+
table.add_column("File", style="dim")
|
|
238
|
+
table.add_column("Line", justify="right")
|
|
239
|
+
|
|
240
|
+
for sym in changed:
|
|
241
|
+
marker = "[red]*[/red]" if sym.get("changed") else "[dim]~[/dim]"
|
|
242
|
+
table.add_row(
|
|
243
|
+
marker,
|
|
244
|
+
sym.get("type", "?"),
|
|
245
|
+
sym.get("name", "?"),
|
|
246
|
+
sym.get("file", "?"),
|
|
247
|
+
str(sym.get("line", "?")),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
console.print(table)
|
|
251
|
+
console.print("[dim] [red]*[/red] = in changed lines [dim]~[/dim] = in changed file[/dim]\n")
|
|
252
|
+
|
|
253
|
+
# Affected symbols (callers of changed code)
|
|
254
|
+
affected = result.get("affected_symbols", [])
|
|
255
|
+
if affected:
|
|
256
|
+
console.print("[bold yellow]Potentially affected symbols:[/bold yellow]\n")
|
|
257
|
+
for sym in affected:
|
|
258
|
+
calls_changed = ", ".join(sym.get("calls_changed", []))
|
|
259
|
+
console.print(f" [yellow]{sym['name']}[/yellow] ({sym.get('type', '?')}) in {sym.get('file', '?')}")
|
|
260
|
+
console.print(f" [dim]calls changed: {calls_changed}[/dim]")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@main.command()
|
|
264
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
265
|
+
@click.option("-o", "--output", default=".cortexcode/docs", help="Output directory")
|
|
266
|
+
@click.option("--open", "open_browser", is_flag=True, help="Open HTML docs in browser")
|
|
267
|
+
def docs(path, output, open_browser):
|
|
268
|
+
"""Generate project documentation."""
|
|
269
|
+
path = Path(path).resolve()
|
|
270
|
+
output = Path(output)
|
|
271
|
+
|
|
272
|
+
console.print(Panel.fit(
|
|
273
|
+
f"[bold cyan]📄 Documentation Generator[/bold cyan]\n"
|
|
274
|
+
f"[dim]Source: {path}[/dim]",
|
|
275
|
+
border_style="cyan"
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
index_path = path / ".cortexcode" / "index.json"
|
|
279
|
+
if not index_path.exists():
|
|
280
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
with Progress(
|
|
284
|
+
SpinnerColumn(),
|
|
285
|
+
TextColumn("[progress.description]{task.description}"),
|
|
286
|
+
console=console,
|
|
287
|
+
) as progress:
|
|
288
|
+
task = progress.add_task("[cyan]Generating documentation...", total=None)
|
|
289
|
+
generate_all_docs(index_path, output)
|
|
290
|
+
progress.update(task, completed=True)
|
|
291
|
+
|
|
292
|
+
console.print()
|
|
293
|
+
console.print(Panel(
|
|
294
|
+
f"[bold green]✓[/bold green] Documentation generated successfully!\n\n"
|
|
295
|
+
f"[cyan]Output:[/cyan] {output}\n"
|
|
296
|
+
f"[cyan]HTML:[/cyan] {output}/index.html\n\n"
|
|
297
|
+
f"[dim]Open index.html in a browser to view interactive documentation.[/dim]",
|
|
298
|
+
title="[bold]Documentation Complete[/bold]",
|
|
299
|
+
border_style="green"
|
|
300
|
+
))
|
|
301
|
+
|
|
302
|
+
if open_browser:
|
|
303
|
+
import webbrowser
|
|
304
|
+
html_path = (output / "index.html").resolve()
|
|
305
|
+
webbrowser.open(f"file:///{html_path}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _show_index_summary(index_data: dict) -> None:
|
|
309
|
+
"""Show a summary table of the index."""
|
|
310
|
+
table = Table(title="Index Summary")
|
|
311
|
+
table.add_column("Metric", style="cyan")
|
|
312
|
+
table.add_column("Value", style="green")
|
|
313
|
+
|
|
314
|
+
table.add_row("Files", str(len(index_data.get("files", {}))))
|
|
315
|
+
table.add_row("Symbols", str(len(index_data.get("call_graph", {}))))
|
|
316
|
+
table.add_row("Last Indexed", index_data.get("last_indexed", "N/A"))
|
|
317
|
+
|
|
318
|
+
console.print(table)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _print_context(result: dict) -> None:
|
|
322
|
+
"""Print context in human-readable format."""
|
|
323
|
+
if not result.get("symbols"):
|
|
324
|
+
console.print("[yellow]No matching symbols found.[/yellow]")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
console.print("\n[bold]Relevant Symbols:[/bold]\n")
|
|
328
|
+
|
|
329
|
+
for sym in result["symbols"]:
|
|
330
|
+
console.print(f" [cyan]{sym['name']}[/cyan] ({sym.get('type', 'unknown')})")
|
|
331
|
+
console.print(f" File: {sym.get('file', 'unknown')}:{sym.get('line', '?')}")
|
|
332
|
+
if sym.get("params"):
|
|
333
|
+
console.print(f" Params: {', '.join(sym['params'])}")
|
|
334
|
+
if sym.get("calls"):
|
|
335
|
+
console.print(f" Calls: {', '.join(sym['calls'])}")
|
|
336
|
+
if sym.get("called_by"):
|
|
337
|
+
console.print(f" Called by: {', '.join(sym['called_by'])}")
|
|
338
|
+
console.print()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _print_token_savings(savings: dict) -> None:
|
|
342
|
+
"""Print token savings analysis."""
|
|
343
|
+
console.print()
|
|
344
|
+
table = Table(
|
|
345
|
+
title="[bold]Token Savings Analysis[/bold]",
|
|
346
|
+
box=box.ROUNDED,
|
|
347
|
+
show_header=False,
|
|
348
|
+
)
|
|
349
|
+
table.add_column("Metric", style="cyan")
|
|
350
|
+
table.add_column("Value", style="white", justify="right")
|
|
351
|
+
|
|
352
|
+
raw = savings["raw_project_tokens"]
|
|
353
|
+
ctx = savings["context_tokens"]
|
|
354
|
+
idx = savings["index_tokens"]
|
|
355
|
+
|
|
356
|
+
table.add_row("Source files", f"{savings['file_count']} files")
|
|
357
|
+
table.add_row("Raw project tokens", f"[red]{raw:,}[/red]")
|
|
358
|
+
table.add_row("Full index tokens", f"[yellow]{idx:,}[/yellow]")
|
|
359
|
+
table.add_row("Context query tokens", f"[green]{ctx:,}[/green]")
|
|
360
|
+
table.add_row("", "")
|
|
361
|
+
table.add_row("Tokens saved", f"[bold green]{savings['savings_tokens']:,}[/bold green]")
|
|
362
|
+
table.add_row("Savings", f"[bold green]{savings['savings_percent']}%[/bold green]")
|
|
363
|
+
table.add_row("Compression ratio", f"[bold]{savings['compression_ratio']}x[/bold]")
|
|
364
|
+
|
|
365
|
+
console.print(Panel(table, border_style="green"))
|
|
366
|
+
|
|
367
|
+
# Cost estimate (GPT-4 pricing ~$30/1M input tokens)
|
|
368
|
+
cost_raw = raw / 1_000_000 * 30
|
|
369
|
+
cost_ctx = ctx / 1_000_000 * 30
|
|
370
|
+
cost_saved = cost_raw - cost_ctx
|
|
371
|
+
|
|
372
|
+
if cost_saved > 0.001:
|
|
373
|
+
console.print(
|
|
374
|
+
f" [dim]Estimated cost per query (GPT-4 rates):[/dim]\n"
|
|
375
|
+
f" [red]Without CortexCode:[/red] ${cost_raw:.4f}\n"
|
|
376
|
+
f" [green]With CortexCode:[/green] ${cost_ctx:.4f}\n"
|
|
377
|
+
f" [bold green]Saved per query:[/bold green] ${cost_saved:.4f}\n"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@main.command()
|
|
382
|
+
def stats():
|
|
383
|
+
"""Show project index statistics and token savings."""
|
|
384
|
+
index_path = Path(".cortexcode/index.json")
|
|
385
|
+
|
|
386
|
+
if not index_path.exists():
|
|
387
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
388
|
+
sys.exit(1)
|
|
389
|
+
|
|
390
|
+
console.print(Panel.fit(
|
|
391
|
+
"[bold cyan]CortexCode Stats[/bold cyan]",
|
|
392
|
+
border_style="cyan"
|
|
393
|
+
))
|
|
394
|
+
|
|
395
|
+
savings = calculate_token_savings(index_path)
|
|
396
|
+
_print_token_savings(savings)
|
|
397
|
+
|
|
398
|
+
# Also show per-query savings for common queries
|
|
399
|
+
import json
|
|
400
|
+
index = json.loads(index_path.read_text(encoding="utf-8"))
|
|
401
|
+
call_graph = index.get("call_graph", {})
|
|
402
|
+
|
|
403
|
+
# Pick top 3 symbols by call count
|
|
404
|
+
top_symbols = sorted(
|
|
405
|
+
call_graph.items(),
|
|
406
|
+
key=lambda x: len(x[1]) if isinstance(x[1], list) else 0,
|
|
407
|
+
reverse=True
|
|
408
|
+
)[:3]
|
|
409
|
+
|
|
410
|
+
if top_symbols:
|
|
411
|
+
console.print("\n[bold]Per-Query Savings (top symbols):[/bold]\n")
|
|
412
|
+
query_table = Table(box=box.SIMPLE, show_header=True)
|
|
413
|
+
query_table.add_column("Query", style="cyan")
|
|
414
|
+
query_table.add_column("Context Tokens", justify="right")
|
|
415
|
+
query_table.add_column("vs Raw", justify="right", style="green")
|
|
416
|
+
|
|
417
|
+
for name, _ in top_symbols:
|
|
418
|
+
q_savings = calculate_token_savings(index_path, name, 5)
|
|
419
|
+
query_table.add_row(
|
|
420
|
+
name,
|
|
421
|
+
f"{q_savings['context_tokens']:,}",
|
|
422
|
+
f"{q_savings['savings_percent']}% saved"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
console.print(query_table)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@main.command()
|
|
429
|
+
def mcp():
|
|
430
|
+
"""Start MCP server for AI agent integration (stdin/stdout)."""
|
|
431
|
+
from cortexcode.mcp_server import run_stdio_server
|
|
432
|
+
|
|
433
|
+
console.print("[dim]CortexCode MCP server started (stdin/stdout)[/dim]")
|
|
434
|
+
run_stdio_server()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@main.command()
|
|
438
|
+
def lsp():
|
|
439
|
+
"""Start Language Server Protocol server (stdin/stdout)."""
|
|
440
|
+
from cortexcode.lsp_server import run_lsp_server
|
|
441
|
+
run_lsp_server()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@main.command(name="find")
|
|
445
|
+
@click.argument("query")
|
|
446
|
+
@click.option("-n", "--limit", default=10, help="Max results")
|
|
447
|
+
def semantic_find(query, limit):
|
|
448
|
+
"""Semantic search — find symbols by meaning, not just name.
|
|
449
|
+
|
|
450
|
+
Examples: cortexcode find "authentication handler"
|
|
451
|
+
cortexcode find "database models"
|
|
452
|
+
cortexcode find "user login flow"
|
|
453
|
+
"""
|
|
454
|
+
from cortexcode.semantic_search import semantic_search
|
|
455
|
+
|
|
456
|
+
index_path = Path(".cortexcode/index.json")
|
|
457
|
+
if not index_path.exists():
|
|
458
|
+
console.print("[bold red]Error:[/bold red] No index found. Run 'cortexcode index' first.")
|
|
459
|
+
sys.exit(1)
|
|
460
|
+
|
|
461
|
+
result = semantic_search(index_path, query, limit)
|
|
462
|
+
results = result.get("results", [])
|
|
463
|
+
|
|
464
|
+
if not results:
|
|
465
|
+
console.print(f"[yellow]No results for '{query}'[/yellow]")
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
console.print(f"\n[bold]Semantic search: \"{query}\"[/bold] ({len(results)} results from {result['total_symbols']} symbols)\n")
|
|
469
|
+
|
|
470
|
+
table = Table(box=box.SIMPLE, show_header=True)
|
|
471
|
+
table.add_column("Score", justify="right", width=6)
|
|
472
|
+
table.add_column("Type", style="dim", width=10)
|
|
473
|
+
table.add_column("Name", style="cyan")
|
|
474
|
+
table.add_column("File", style="dim")
|
|
475
|
+
table.add_column("Line", justify="right", width=5)
|
|
476
|
+
|
|
477
|
+
for r in results:
|
|
478
|
+
score_color = "green" if r["score"] > 0.3 else "yellow" if r["score"] > 0.1 else "dim"
|
|
479
|
+
table.add_row(
|
|
480
|
+
f"[{score_color}]{r['score']:.2f}[/{score_color}]",
|
|
481
|
+
r.get("type", "?"),
|
|
482
|
+
r.get("name", "?"),
|
|
483
|
+
r.get("file", "?"),
|
|
484
|
+
str(r.get("line", "?")),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
console.print(table)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@main.command()
|
|
491
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
492
|
+
def scan(path):
|
|
493
|
+
"""Scan dependencies for known issues and security warnings."""
|
|
494
|
+
from cortexcode.vuln_scan import scan_dependencies
|
|
495
|
+
|
|
496
|
+
path = Path(path).resolve()
|
|
497
|
+
|
|
498
|
+
console.print(Panel.fit(
|
|
499
|
+
f"[bold cyan]Dependency Scanner[/bold cyan]\n"
|
|
500
|
+
f"[dim]Scanning: {path}[/dim]",
|
|
501
|
+
border_style="cyan"
|
|
502
|
+
))
|
|
503
|
+
|
|
504
|
+
result = scan_dependencies(path)
|
|
505
|
+
|
|
506
|
+
if not result["scanned_files"]:
|
|
507
|
+
console.print("[yellow]No dependency files found.[/yellow]")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
console.print(f"\n[bold]Scanned:[/bold] {', '.join(result['scanned_files'])}")
|
|
511
|
+
console.print(f"[bold]Dependencies found:[/bold] {result['total_dependencies']}")
|
|
512
|
+
|
|
513
|
+
warnings = result.get("warnings", [])
|
|
514
|
+
if warnings:
|
|
515
|
+
console.print(f"\n[bold yellow]Warnings ({len(warnings)}):[/bold yellow]\n")
|
|
516
|
+
for w in warnings:
|
|
517
|
+
severity_color = {"high": "red", "medium": "yellow", "low": "dim"}.get(w["severity"], "white")
|
|
518
|
+
console.print(f" [{severity_color}][{w['severity'].upper()}][/{severity_color}] {w['package']}: {w['message']}")
|
|
519
|
+
else:
|
|
520
|
+
console.print("\n[green]No warnings found.[/green]")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@main.command("dead-code")
|
|
524
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
525
|
+
def dead_code(path):
|
|
526
|
+
"""Detect potentially unused symbols (dead code)."""
|
|
527
|
+
from cortexcode.analysis import detect_dead_code
|
|
528
|
+
|
|
529
|
+
path = Path(path).resolve()
|
|
530
|
+
index_path = path / ".cortexcode" / "index.json"
|
|
531
|
+
if not index_path.exists():
|
|
532
|
+
console.print("[red]No index found. Run `cortexcode index` first.[/red]")
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
import json as _json
|
|
536
|
+
index = _json.loads(index_path.read_text(encoding="utf-8"))
|
|
537
|
+
dead = detect_dead_code(index)
|
|
538
|
+
|
|
539
|
+
if not dead:
|
|
540
|
+
console.print("[green]No dead code detected — all symbols are referenced.[/green]")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
console.print(Panel.fit(
|
|
544
|
+
f"[bold yellow]Potentially Unused Symbols[/bold yellow]\n"
|
|
545
|
+
f"[dim]Found {len(dead)} symbols that are never called or imported[/dim]",
|
|
546
|
+
border_style="yellow"
|
|
547
|
+
))
|
|
548
|
+
|
|
549
|
+
table = Table(box=box.ROUNDED)
|
|
550
|
+
table.add_column("Type", width=10)
|
|
551
|
+
table.add_column("Name", style="cyan")
|
|
552
|
+
table.add_column("File")
|
|
553
|
+
table.add_column("Line", justify="right", width=5)
|
|
554
|
+
|
|
555
|
+
for d in dead[:50]:
|
|
556
|
+
table.add_row(d["type"], d["name"], d["file"], str(d["line"]))
|
|
557
|
+
|
|
558
|
+
console.print(table)
|
|
559
|
+
if len(dead) > 50:
|
|
560
|
+
console.print(f"[dim]... and {len(dead) - 50} more[/dim]")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@main.command()
|
|
564
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
565
|
+
@click.option("--top", "-n", default=20, help="Show top N most complex functions")
|
|
566
|
+
@click.option("--min-score", default=0, help="Minimum complexity score to show")
|
|
567
|
+
def complexity(path, top, min_score):
|
|
568
|
+
"""Analyze code complexity metrics for all functions."""
|
|
569
|
+
from cortexcode.analysis import compute_complexity
|
|
570
|
+
|
|
571
|
+
path = Path(path).resolve()
|
|
572
|
+
index_path = path / ".cortexcode" / "index.json"
|
|
573
|
+
if not index_path.exists():
|
|
574
|
+
console.print("[red]No index found. Run `cortexcode index` first.[/red]")
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
import json as _json
|
|
578
|
+
index = _json.loads(index_path.read_text(encoding="utf-8"))
|
|
579
|
+
results = compute_complexity(index, project_root=str(path))
|
|
580
|
+
|
|
581
|
+
if min_score > 0:
|
|
582
|
+
results = [r for r in results if r.get("score", 0) >= min_score]
|
|
583
|
+
|
|
584
|
+
if not results:
|
|
585
|
+
console.print("[green]No complex functions found.[/green]")
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Summary stats
|
|
589
|
+
ratings = {"low": 0, "medium": 0, "high": 0, "critical": 0}
|
|
590
|
+
for r in results:
|
|
591
|
+
ratings[r.get("rating", "low")] += 1
|
|
592
|
+
|
|
593
|
+
console.print(Panel.fit(
|
|
594
|
+
f"[bold cyan]Complexity Analysis[/bold cyan]\n"
|
|
595
|
+
f"[green]Low: {ratings['low']}[/green] [yellow]Medium: {ratings['medium']}[/yellow] "
|
|
596
|
+
f"[red]High: {ratings['high']}[/red] [bold red]Critical: {ratings['critical']}[/bold red]",
|
|
597
|
+
border_style="cyan"
|
|
598
|
+
))
|
|
599
|
+
|
|
600
|
+
table = Table(title=f"Top {top} Most Complex Functions", box=box.ROUNDED)
|
|
601
|
+
table.add_column("Score", justify="right", width=6)
|
|
602
|
+
table.add_column("Rating", width=8)
|
|
603
|
+
table.add_column("Name", style="cyan")
|
|
604
|
+
table.add_column("Lines", justify="right", width=6)
|
|
605
|
+
table.add_column("CC", justify="right", width=4)
|
|
606
|
+
table.add_column("Nest", justify="right", width=4)
|
|
607
|
+
table.add_column("Params", justify="right", width=6)
|
|
608
|
+
table.add_column("File")
|
|
609
|
+
|
|
610
|
+
for r in results[:top]:
|
|
611
|
+
rating = r.get("rating", "low")
|
|
612
|
+
color = {"low": "green", "medium": "yellow", "high": "red", "critical": "bold red"}[rating]
|
|
613
|
+
table.add_row(
|
|
614
|
+
str(r.get("score", 0)),
|
|
615
|
+
f"[{color}]{rating}[/{color}]",
|
|
616
|
+
r["name"],
|
|
617
|
+
str(r.get("lines", "?")),
|
|
618
|
+
str(r.get("cyclomatic", "?")),
|
|
619
|
+
str(r.get("max_nesting", "?")),
|
|
620
|
+
str(r.get("params_count", 0)),
|
|
621
|
+
r["file"],
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
console.print(table)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@main.command()
|
|
628
|
+
@click.argument("symbol")
|
|
629
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
630
|
+
def impact(symbol, path):
|
|
631
|
+
"""Analyze change impact — what breaks if a symbol is modified."""
|
|
632
|
+
from cortexcode.analysis import analyze_change_impact
|
|
633
|
+
|
|
634
|
+
path = Path(path).resolve()
|
|
635
|
+
index_path = path / ".cortexcode" / "index.json"
|
|
636
|
+
if not index_path.exists():
|
|
637
|
+
console.print("[red]No index found. Run `cortexcode index` first.[/red]")
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
import json as _json
|
|
641
|
+
index = _json.loads(index_path.read_text(encoding="utf-8"))
|
|
642
|
+
result = analyze_change_impact(index, symbol)
|
|
643
|
+
|
|
644
|
+
risk_color = {"low": "green", "medium": "yellow", "high": "red"}[result["risk"]]
|
|
645
|
+
|
|
646
|
+
console.print(Panel.fit(
|
|
647
|
+
f"[bold cyan]Change Impact Analysis[/bold cyan]\n"
|
|
648
|
+
f"Symbol: [bold]{symbol}[/bold]\n"
|
|
649
|
+
f"Risk: [{risk_color}]{result['risk'].upper()}[/{risk_color}] | "
|
|
650
|
+
f"Total impact: {result['total_impact']} symbols",
|
|
651
|
+
border_style="cyan"
|
|
652
|
+
))
|
|
653
|
+
|
|
654
|
+
if result["direct_callers"]:
|
|
655
|
+
console.print(f"\n[bold]Direct callers ({len(result['direct_callers'])}):[/bold]")
|
|
656
|
+
for c in result["direct_callers"]:
|
|
657
|
+
console.print(f" [cyan]{c}[/cyan]")
|
|
658
|
+
|
|
659
|
+
if result["indirect_callers"]:
|
|
660
|
+
console.print(f"\n[bold]Indirect callers ({len(result['indirect_callers'])}):[/bold]")
|
|
661
|
+
for c in result["indirect_callers"]:
|
|
662
|
+
console.print(f" [dim]{c}[/dim]")
|
|
663
|
+
|
|
664
|
+
if result["affected_files"]:
|
|
665
|
+
console.print(f"\n[bold]Affected files ({len(result['affected_files'])}):[/bold]")
|
|
666
|
+
for f in result["affected_files"]:
|
|
667
|
+
console.print(f" {f}")
|
|
668
|
+
|
|
669
|
+
if result["affected_tests"]:
|
|
670
|
+
console.print(f"\n[bold yellow]Tests to update ({len(result['affected_tests'])}):[/bold yellow]")
|
|
671
|
+
for f in result["affected_tests"]:
|
|
672
|
+
console.print(f" {f}")
|
|
673
|
+
|
|
674
|
+
if result["importing_files"]:
|
|
675
|
+
console.print(f"\n[bold]Files importing affected modules ({len(result['importing_files'])}):[/bold]")
|
|
676
|
+
for f in result["importing_files"]:
|
|
677
|
+
console.print(f" [dim]{f}[/dim]")
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@main.group()
|
|
681
|
+
def workspace():
|
|
682
|
+
"""Multi-repo workspace management."""
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@workspace.command("init")
|
|
687
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
688
|
+
def workspace_init(path):
|
|
689
|
+
"""Initialize a workspace at the given path."""
|
|
690
|
+
from cortexcode.workspace import Workspace
|
|
691
|
+
|
|
692
|
+
ws = Workspace(Path(path).resolve())
|
|
693
|
+
if ws.load_config():
|
|
694
|
+
console.print("[yellow]Workspace already exists here.[/yellow]")
|
|
695
|
+
return
|
|
696
|
+
ws.save_config()
|
|
697
|
+
console.print(f"[green]Workspace initialized at {ws.workspace_root}[/green]")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@workspace.command("add")
|
|
701
|
+
@click.argument("repo_path", type=click.Path(exists=True))
|
|
702
|
+
@click.option("--alias", "-a", default=None, help="Short alias for the repo")
|
|
703
|
+
def workspace_add(repo_path, alias):
|
|
704
|
+
"""Add a repo to the workspace."""
|
|
705
|
+
from cortexcode.workspace import Workspace
|
|
706
|
+
|
|
707
|
+
ws = Workspace(Path(".").resolve())
|
|
708
|
+
if not ws.load_config():
|
|
709
|
+
console.print("[red]No workspace found. Run `cortexcode workspace init` first.[/red]")
|
|
710
|
+
return
|
|
711
|
+
try:
|
|
712
|
+
repo = ws.add_repo(repo_path, alias)
|
|
713
|
+
console.print(f"[green]Added {repo['alias']} → {repo['path']}[/green]")
|
|
714
|
+
except ValueError as e:
|
|
715
|
+
console.print(f"[red]{e}[/red]")
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
@workspace.command("remove")
|
|
719
|
+
@click.argument("alias_or_path")
|
|
720
|
+
def workspace_remove(alias_or_path):
|
|
721
|
+
"""Remove a repo from the workspace."""
|
|
722
|
+
from cortexcode.workspace import Workspace
|
|
723
|
+
|
|
724
|
+
ws = Workspace(Path(".").resolve())
|
|
725
|
+
if not ws.load_config():
|
|
726
|
+
console.print("[red]No workspace found.[/red]")
|
|
727
|
+
return
|
|
728
|
+
if ws.remove_repo(alias_or_path):
|
|
729
|
+
console.print(f"[green]Removed {alias_or_path}[/green]")
|
|
730
|
+
else:
|
|
731
|
+
console.print(f"[yellow]Not found: {alias_or_path}[/yellow]")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@workspace.command("list")
|
|
735
|
+
def workspace_list():
|
|
736
|
+
"""List all repos in the workspace."""
|
|
737
|
+
from cortexcode.workspace import Workspace
|
|
738
|
+
|
|
739
|
+
ws = Workspace(Path(".").resolve())
|
|
740
|
+
if not ws.load_config():
|
|
741
|
+
console.print("[red]No workspace found. Run `cortexcode workspace init` first.[/red]")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
repos = ws.list_repos()
|
|
745
|
+
if not repos:
|
|
746
|
+
console.print("[dim]No repos in workspace. Use `cortexcode workspace add <path>`[/dim]")
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
table = Table(title="Workspace Repos", box=box.ROUNDED)
|
|
750
|
+
table.add_column("Alias", style="cyan")
|
|
751
|
+
table.add_column("Path")
|
|
752
|
+
table.add_column("Indexed", justify="center")
|
|
753
|
+
|
|
754
|
+
for r in repos:
|
|
755
|
+
table.add_row(r["alias"], r["path"], "✓" if r["indexed"] else "✗")
|
|
756
|
+
console.print(table)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
@workspace.command("index")
|
|
760
|
+
@click.option("--full", is_flag=True, help="Full re-index (not incremental)")
|
|
761
|
+
def workspace_index(full):
|
|
762
|
+
"""Index all repos in the workspace."""
|
|
763
|
+
from cortexcode.workspace import Workspace
|
|
764
|
+
|
|
765
|
+
ws = Workspace(Path(".").resolve())
|
|
766
|
+
if not ws.load_config():
|
|
767
|
+
console.print("[red]No workspace found.[/red]")
|
|
768
|
+
return
|
|
769
|
+
|
|
770
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
|
|
771
|
+
progress.add_task("Indexing workspace repos...", total=None)
|
|
772
|
+
results = ws.index_all(incremental=not full)
|
|
773
|
+
|
|
774
|
+
table = Table(title="Workspace Index Results", box=box.ROUNDED)
|
|
775
|
+
table.add_column("Repo", style="cyan")
|
|
776
|
+
table.add_column("Symbols", justify="right")
|
|
777
|
+
|
|
778
|
+
for alias, count in results.items():
|
|
779
|
+
color = "green" if count >= 0 else "red"
|
|
780
|
+
table.add_row(alias, f"[{color}]{count}[/{color}]")
|
|
781
|
+
console.print(table)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
@workspace.command("search")
|
|
785
|
+
@click.argument("query")
|
|
786
|
+
def workspace_search(query):
|
|
787
|
+
"""Search symbols across all workspace repos."""
|
|
788
|
+
from cortexcode.workspace import Workspace
|
|
789
|
+
|
|
790
|
+
ws = Workspace(Path(".").resolve())
|
|
791
|
+
if not ws.load_config():
|
|
792
|
+
console.print("[red]No workspace found.[/red]")
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
results = ws.search_across_repos(query)
|
|
796
|
+
if not results:
|
|
797
|
+
console.print(f"[dim]No results for '{query}'[/dim]")
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
table = Table(title=f"Results for '{query}'", box=box.ROUNDED)
|
|
801
|
+
table.add_column("Repo", style="dim", width=12)
|
|
802
|
+
table.add_column("Type", width=10)
|
|
803
|
+
table.add_column("Name", style="cyan")
|
|
804
|
+
table.add_column("File")
|
|
805
|
+
|
|
806
|
+
for r in results:
|
|
807
|
+
table.add_row(r.get("repo", "?"), r.get("type", "?"), r.get("name", "?"), r.get("file", "?"))
|
|
808
|
+
console.print(table)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
@main.command()
|
|
812
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
813
|
+
@click.option("--port", "-p", default=8787, help="Port to serve the dashboard")
|
|
814
|
+
def dashboard(path, port):
|
|
815
|
+
"""Launch a live web dashboard with auto-refresh on index changes."""
|
|
816
|
+
from cortexcode.dashboard import DashboardServer
|
|
817
|
+
|
|
818
|
+
path = Path(path).resolve()
|
|
819
|
+
index_path = path / ".cortexcode" / "index.json"
|
|
820
|
+
|
|
821
|
+
if not index_path.exists():
|
|
822
|
+
console.print("[yellow]No index found. Indexing first...[/yellow]")
|
|
823
|
+
idx = indexer.CodeIndexer()
|
|
824
|
+
index = idx.index_directory(path)
|
|
825
|
+
output_dir = path / ".cortexcode"
|
|
826
|
+
output_dir.mkdir(exist_ok=True)
|
|
827
|
+
import json as _json
|
|
828
|
+
index_path.write_text(_json.dumps(index, indent=2, default=str), encoding="utf-8")
|
|
829
|
+
|
|
830
|
+
docs_dir = path / ".cortexcode" / "docs"
|
|
831
|
+
console.print(Panel.fit(
|
|
832
|
+
f"[bold cyan]CortexCode Live Dashboard[/bold cyan]\n"
|
|
833
|
+
f"[dim]Serving: {docs_dir}[/dim]\n"
|
|
834
|
+
f"[bold green]http://localhost:{port}[/bold green]\n"
|
|
835
|
+
f"[dim]Auto-refreshes when index changes[/dim]",
|
|
836
|
+
border_style="cyan"
|
|
837
|
+
))
|
|
838
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
839
|
+
|
|
840
|
+
server = DashboardServer(path, port)
|
|
841
|
+
server.start(open_browser=True)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
if __name__ == "__main__":
|
|
845
|
+
main()
|