emdash-cli 0.1.4__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.
- emdash_cli/__init__.py +3 -0
- emdash_cli/client.py +556 -0
- emdash_cli/commands/__init__.py +37 -0
- emdash_cli/commands/agent.py +883 -0
- emdash_cli/commands/analyze.py +137 -0
- emdash_cli/commands/auth.py +121 -0
- emdash_cli/commands/db.py +95 -0
- emdash_cli/commands/embed.py +103 -0
- emdash_cli/commands/index.py +134 -0
- emdash_cli/commands/plan.py +77 -0
- emdash_cli/commands/projectmd.py +51 -0
- emdash_cli/commands/research.py +47 -0
- emdash_cli/commands/rules.py +93 -0
- emdash_cli/commands/search.py +56 -0
- emdash_cli/commands/server.py +117 -0
- emdash_cli/commands/spec.py +49 -0
- emdash_cli/commands/swarm.py +86 -0
- emdash_cli/commands/tasks.py +52 -0
- emdash_cli/commands/team.py +51 -0
- emdash_cli/main.py +104 -0
- emdash_cli/server_manager.py +231 -0
- emdash_cli/sse_renderer.py +442 -0
- emdash_cli-0.1.4.dist-info/METADATA +17 -0
- emdash_cli-0.1.4.dist-info/RECORD +26 -0
- emdash_cli-0.1.4.dist-info/WHEEL +4 -0
- emdash_cli-0.1.4.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Analytics CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from ..client import EmdashClient
|
|
8
|
+
from ..server_manager import get_server_manager
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def analyze():
|
|
15
|
+
"""Run graph analytics."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@analyze.command("pagerank")
|
|
20
|
+
@click.option("--top", default=20, help="Number of results to show")
|
|
21
|
+
@click.option("--damping", default=0.85, help="Damping factor")
|
|
22
|
+
def analyze_pagerank(top: int, damping: float):
|
|
23
|
+
"""Compute PageRank scores to identify important code entities."""
|
|
24
|
+
server = get_server_manager()
|
|
25
|
+
client = EmdashClient(server.get_server_url())
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = client.analyze_pagerank(top=top, damping=damping)
|
|
29
|
+
|
|
30
|
+
entities = result.get("entities", [])
|
|
31
|
+
|
|
32
|
+
table = Table(title=f"Top {top} by PageRank")
|
|
33
|
+
table.add_column("Rank", justify="right", style="dim")
|
|
34
|
+
table.add_column("Entity", style="cyan")
|
|
35
|
+
table.add_column("Type")
|
|
36
|
+
table.add_column("Score", justify="right")
|
|
37
|
+
|
|
38
|
+
for i, entity in enumerate(entities, 1):
|
|
39
|
+
table.add_row(
|
|
40
|
+
str(i),
|
|
41
|
+
entity.get("name", ""),
|
|
42
|
+
entity.get("type", ""),
|
|
43
|
+
f"{entity.get('score', 0):.4f}",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console.print(table)
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
50
|
+
raise click.Abort()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@analyze.command("communities")
|
|
54
|
+
@click.option("--resolution", default=1.0, help="Resolution parameter")
|
|
55
|
+
@click.option("--min-size", default=3, help="Minimum community size")
|
|
56
|
+
@click.option("--top", default=20, help="Number of communities to show")
|
|
57
|
+
def analyze_communities(resolution: float, min_size: int, top: int):
|
|
58
|
+
"""Detect code communities using Louvain algorithm."""
|
|
59
|
+
server = get_server_manager()
|
|
60
|
+
client = EmdashClient(server.get_server_url())
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
result = client.analyze_communities(
|
|
64
|
+
resolution=resolution,
|
|
65
|
+
min_size=min_size,
|
|
66
|
+
top=top,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
communities = result.get("communities", [])
|
|
70
|
+
|
|
71
|
+
table = Table(title=f"Top {top} Communities")
|
|
72
|
+
table.add_column("ID", justify="right", style="dim")
|
|
73
|
+
table.add_column("Size", justify="right")
|
|
74
|
+
table.add_column("Members", style="cyan")
|
|
75
|
+
|
|
76
|
+
for comm in communities:
|
|
77
|
+
members = comm.get("members", [])
|
|
78
|
+
member_str = ", ".join(members[:5])
|
|
79
|
+
if len(members) > 5:
|
|
80
|
+
member_str += f" (+{len(members) - 5} more)"
|
|
81
|
+
|
|
82
|
+
table.add_row(
|
|
83
|
+
str(comm.get("id", "")),
|
|
84
|
+
str(comm.get("size", 0)),
|
|
85
|
+
member_str,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
console.print(table)
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
92
|
+
raise click.Abort()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@analyze.command("areas")
|
|
96
|
+
@click.option("--depth", default=2, help="Directory depth")
|
|
97
|
+
@click.option("--days", default=30, help="Days to look back for focus")
|
|
98
|
+
@click.option("--top", default=20, help="Number of results")
|
|
99
|
+
@click.option("--sort", type=click.Choice(["focus", "importance", "commits", "authors"]),
|
|
100
|
+
default="focus", help="Sort metric")
|
|
101
|
+
@click.option("--files", is_flag=True, help="Show individual files instead of directories")
|
|
102
|
+
def analyze_areas(depth: int, days: int, top: int, sort: str, files: bool):
|
|
103
|
+
"""Get importance metrics by directory/area or individual files."""
|
|
104
|
+
server = get_server_manager()
|
|
105
|
+
client = EmdashClient(server.get_server_url())
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
result = client.analyze_areas(
|
|
109
|
+
depth=depth,
|
|
110
|
+
days=days,
|
|
111
|
+
top=top,
|
|
112
|
+
sort=sort,
|
|
113
|
+
files=files,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
areas = result.get("areas", [])
|
|
117
|
+
|
|
118
|
+
title = f"Top {top} {'Files' if files else 'Areas'} by {sort.title()}"
|
|
119
|
+
table = Table(title=title)
|
|
120
|
+
table.add_column("Path", style="cyan")
|
|
121
|
+
table.add_column("Commits", justify="right")
|
|
122
|
+
table.add_column("Authors", justify="right")
|
|
123
|
+
table.add_column("Focus %", justify="right")
|
|
124
|
+
|
|
125
|
+
for area in areas:
|
|
126
|
+
table.add_row(
|
|
127
|
+
area.get("path", ""),
|
|
128
|
+
str(area.get("commits", 0)),
|
|
129
|
+
str(area.get("authors", 0)),
|
|
130
|
+
f"{area.get('focus_pct', 0):.1f}%",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
console.print(table)
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
137
|
+
raise click.Abort()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Authentication CLI commands."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import webbrowser
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from ..client import EmdashClient
|
|
10
|
+
from ..server_manager import get_server_manager
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def auth():
|
|
17
|
+
"""Manage GitHub authentication."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@auth.command("login")
|
|
22
|
+
@click.option("--no-browser", is_flag=True, help="Don't open browser automatically")
|
|
23
|
+
def auth_login(no_browser: bool):
|
|
24
|
+
"""Authenticate with GitHub using device flow."""
|
|
25
|
+
server = get_server_manager()
|
|
26
|
+
client = EmdashClient(server.get_server_url())
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
# Start device flow
|
|
30
|
+
result = client.auth_login()
|
|
31
|
+
|
|
32
|
+
user_code = result.get("user_code")
|
|
33
|
+
verification_uri = result.get("verification_uri")
|
|
34
|
+
interval = result.get("interval", 5)
|
|
35
|
+
expires_in = result.get("expires_in", 900)
|
|
36
|
+
|
|
37
|
+
console.print()
|
|
38
|
+
console.print("[bold]GitHub Device Authorization[/bold]")
|
|
39
|
+
console.print()
|
|
40
|
+
console.print(f"1. Go to: [cyan]{verification_uri}[/cyan]")
|
|
41
|
+
console.print(f"2. Enter code: [bold yellow]{user_code}[/bold yellow]")
|
|
42
|
+
console.print()
|
|
43
|
+
|
|
44
|
+
if not no_browser:
|
|
45
|
+
webbrowser.open(verification_uri)
|
|
46
|
+
console.print("[dim]Browser opened automatically[/dim]")
|
|
47
|
+
|
|
48
|
+
console.print("[dim]Waiting for authorization...[/dim]")
|
|
49
|
+
|
|
50
|
+
# Poll for completion
|
|
51
|
+
start_time = time.time()
|
|
52
|
+
while time.time() - start_time < expires_in:
|
|
53
|
+
time.sleep(interval)
|
|
54
|
+
|
|
55
|
+
poll_result = client.auth_poll(user_code)
|
|
56
|
+
status = poll_result.get("status")
|
|
57
|
+
|
|
58
|
+
if status == "success":
|
|
59
|
+
username = poll_result.get("username")
|
|
60
|
+
console.print()
|
|
61
|
+
console.print(f"[green]Successfully authenticated as {username}![/green]")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
elif status == "expired":
|
|
65
|
+
console.print("[red]Authorization expired. Please try again.[/red]")
|
|
66
|
+
raise click.Abort()
|
|
67
|
+
|
|
68
|
+
elif status == "error":
|
|
69
|
+
error = poll_result.get("error", "Unknown error")
|
|
70
|
+
console.print(f"[red]Error: {error}[/red]")
|
|
71
|
+
raise click.Abort()
|
|
72
|
+
|
|
73
|
+
# status == "pending" - continue polling
|
|
74
|
+
|
|
75
|
+
console.print("[red]Authorization timed out. Please try again.[/red]")
|
|
76
|
+
raise click.Abort()
|
|
77
|
+
|
|
78
|
+
except click.Abort:
|
|
79
|
+
raise
|
|
80
|
+
except Exception as e:
|
|
81
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
82
|
+
raise click.Abort()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@auth.command("logout")
|
|
86
|
+
def auth_logout():
|
|
87
|
+
"""Remove stored GitHub authentication."""
|
|
88
|
+
server = get_server_manager()
|
|
89
|
+
client = EmdashClient(server.get_server_url())
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
result = client.auth_logout()
|
|
93
|
+
if result.get("success"):
|
|
94
|
+
console.print("[green]Successfully logged out.[/green]")
|
|
95
|
+
else:
|
|
96
|
+
console.print(f"[yellow]{result.get('message', 'Logout completed')}[/yellow]")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
99
|
+
raise click.Abort()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@auth.command("status")
|
|
103
|
+
def auth_status():
|
|
104
|
+
"""Show current GitHub authentication status."""
|
|
105
|
+
server = get_server_manager()
|
|
106
|
+
client = EmdashClient(server.get_server_url())
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
status = client.auth_status()
|
|
110
|
+
|
|
111
|
+
if status.get("authenticated"):
|
|
112
|
+
console.print("[green]Authenticated[/green]")
|
|
113
|
+
console.print(f" User: [cyan]{status.get('username')}[/cyan]")
|
|
114
|
+
if status.get("scope"):
|
|
115
|
+
console.print(f" Scope: [dim]{status.get('scope')}[/dim]")
|
|
116
|
+
else:
|
|
117
|
+
console.print("[yellow]Not authenticated[/yellow]")
|
|
118
|
+
console.print("[dim]Run 'emdash auth login' to authenticate[/dim]")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
121
|
+
raise click.Abort()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Database CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from ..client import EmdashClient
|
|
8
|
+
from ..server_manager import get_server_manager
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def db():
|
|
15
|
+
"""Database management commands."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@db.command("init")
|
|
20
|
+
def db_init():
|
|
21
|
+
"""Initialize the Kuzu database schema."""
|
|
22
|
+
server = get_server_manager()
|
|
23
|
+
client = EmdashClient(server.get_server_url())
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
result = client.db_init()
|
|
27
|
+
if result.get("success"):
|
|
28
|
+
console.print("[green]Database schema initialized successfully![/green]")
|
|
29
|
+
else:
|
|
30
|
+
console.print(f"[red]Error: {result.get('message')}[/red]")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
33
|
+
raise click.Abort()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@db.command("clear")
|
|
37
|
+
@click.confirmation_option(prompt="Are you sure you want to clear all data?")
|
|
38
|
+
def db_clear():
|
|
39
|
+
"""Clear all data from the database."""
|
|
40
|
+
server = get_server_manager()
|
|
41
|
+
client = EmdashClient(server.get_server_url())
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
result = client.db_clear(confirm=True)
|
|
45
|
+
if result.get("success"):
|
|
46
|
+
console.print("[green]Database cleared successfully![/green]")
|
|
47
|
+
else:
|
|
48
|
+
console.print(f"[red]Error: {result.get('message')}[/red]")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
51
|
+
raise click.Abort()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@db.command("stats")
|
|
55
|
+
def db_stats():
|
|
56
|
+
"""Show database statistics."""
|
|
57
|
+
server = get_server_manager()
|
|
58
|
+
client = EmdashClient(server.get_server_url())
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
stats = client.db_stats()
|
|
62
|
+
|
|
63
|
+
table = Table(title="Database Statistics")
|
|
64
|
+
table.add_column("Metric", style="cyan")
|
|
65
|
+
table.add_column("Count", justify="right")
|
|
66
|
+
|
|
67
|
+
table.add_row("Files", str(stats.get("file_count", 0)))
|
|
68
|
+
table.add_row("Functions", str(stats.get("function_count", 0)))
|
|
69
|
+
table.add_row("Classes", str(stats.get("class_count", 0)))
|
|
70
|
+
table.add_row("Communities", str(stats.get("community_count", 0)))
|
|
71
|
+
table.add_row("Total Nodes", str(stats.get("node_count", 0)))
|
|
72
|
+
table.add_row("Relationships", str(stats.get("relationship_count", 0)))
|
|
73
|
+
|
|
74
|
+
console.print(table)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
77
|
+
raise click.Abort()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@db.command("test")
|
|
81
|
+
def db_test():
|
|
82
|
+
"""Test the database connection."""
|
|
83
|
+
server = get_server_manager()
|
|
84
|
+
client = EmdashClient(server.get_server_url())
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
result = client.db_test()
|
|
88
|
+
if result.get("connected"):
|
|
89
|
+
console.print("[green]Database connection successful![/green]")
|
|
90
|
+
console.print(f"[dim]Path: {result.get('database_path')}[/dim]")
|
|
91
|
+
else:
|
|
92
|
+
console.print(f"[red]Connection failed: {result.get('message')}[/red]")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
95
|
+
raise click.Abort()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Embedding management CLI commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from ..client import EmdashClient
|
|
8
|
+
from ..server_manager import get_server_manager
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def embed():
|
|
15
|
+
"""Embedding management commands."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@embed.command("index")
|
|
20
|
+
@click.option("--prs/--no-prs", default=True, help="Index PR embeddings")
|
|
21
|
+
@click.option("--functions/--no-functions", default=True, help="Index function embeddings")
|
|
22
|
+
@click.option("--classes/--no-classes", default=True, help="Index class embeddings")
|
|
23
|
+
@click.option("--reindex", is_flag=True, help="Re-generate all embeddings")
|
|
24
|
+
def embed_index(prs: bool, functions: bool, classes: bool, reindex: bool):
|
|
25
|
+
"""Generate embeddings for graph entities."""
|
|
26
|
+
server = get_server_manager()
|
|
27
|
+
client = EmdashClient(server.get_server_url())
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
console.print("[cyan]Generating embeddings...[/cyan]")
|
|
31
|
+
|
|
32
|
+
result = client.embed_index(
|
|
33
|
+
include_prs=prs,
|
|
34
|
+
include_functions=functions,
|
|
35
|
+
include_classes=classes,
|
|
36
|
+
reindex=reindex,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if result.get("success"):
|
|
40
|
+
indexed = result.get("indexed", 0)
|
|
41
|
+
skipped = result.get("skipped", 0)
|
|
42
|
+
console.print(f"[green]Indexed {indexed} entities ({skipped} skipped)[/green]")
|
|
43
|
+
else:
|
|
44
|
+
console.print(f"[red]Error: {result.get('error')}[/red]")
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
48
|
+
raise click.Abort()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@embed.command("status")
|
|
52
|
+
def embed_status():
|
|
53
|
+
"""Show embedding coverage statistics."""
|
|
54
|
+
server = get_server_manager()
|
|
55
|
+
client = EmdashClient(server.get_server_url())
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
status = client.embed_status()
|
|
59
|
+
|
|
60
|
+
table = Table(title="Embedding Coverage")
|
|
61
|
+
table.add_column("Metric", style="cyan")
|
|
62
|
+
table.add_column("Value", justify="right")
|
|
63
|
+
|
|
64
|
+
table.add_row("Total Entities", str(status.get("total_entities", 0)))
|
|
65
|
+
table.add_row("Embedded", str(status.get("embedded_entities", 0)))
|
|
66
|
+
table.add_row("Coverage", f"{status.get('coverage_percent', 0):.1f}%")
|
|
67
|
+
table.add_row("PRs", str(status.get("pr_count", 0)))
|
|
68
|
+
table.add_row("Functions", str(status.get("function_count", 0)))
|
|
69
|
+
table.add_row("Classes", str(status.get("class_count", 0)))
|
|
70
|
+
|
|
71
|
+
console.print(table)
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
75
|
+
raise click.Abort()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@embed.command("models")
|
|
79
|
+
def embed_models():
|
|
80
|
+
"""List all available embedding models."""
|
|
81
|
+
server = get_server_manager()
|
|
82
|
+
client = EmdashClient(server.get_server_url())
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
models = client.embed_models()
|
|
86
|
+
|
|
87
|
+
table = Table(title="Available Embedding Models")
|
|
88
|
+
table.add_column("Name", style="cyan")
|
|
89
|
+
table.add_column("Dimension", justify="right")
|
|
90
|
+
table.add_column("Description")
|
|
91
|
+
|
|
92
|
+
for model in models:
|
|
93
|
+
table.add_row(
|
|
94
|
+
model.get("name", ""),
|
|
95
|
+
str(model.get("dimension", 0)),
|
|
96
|
+
model.get("description", ""),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
console.print(table)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
103
|
+
raise click.Abort()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Index command - parse and index a codebase."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.status import Status
|
|
9
|
+
|
|
10
|
+
from ..client import EmdashClient
|
|
11
|
+
from ..server_manager import get_server_manager
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def index():
|
|
18
|
+
"""Index a codebase into the knowledge graph."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@index.command("start")
|
|
23
|
+
@click.argument("repo_path", required=False)
|
|
24
|
+
@click.option("--changed-only", is_flag=True, help="Only index changed files")
|
|
25
|
+
@click.option("--skip-git", is_flag=True, help="Skip git history indexing")
|
|
26
|
+
@click.option("--github-prs", default=0, help="Number of GitHub PRs to index")
|
|
27
|
+
@click.option("--detect-communities", is_flag=True, default=True, help="Run community detection")
|
|
28
|
+
@click.option("--describe-communities", is_flag=True, help="Use LLM to describe communities")
|
|
29
|
+
@click.option("--model", "-m", default=None, help="Model for community descriptions")
|
|
30
|
+
def index_start(
|
|
31
|
+
repo_path: str | None,
|
|
32
|
+
changed_only: bool,
|
|
33
|
+
skip_git: bool,
|
|
34
|
+
github_prs: int,
|
|
35
|
+
detect_communities: bool,
|
|
36
|
+
describe_communities: bool,
|
|
37
|
+
model: str | None,
|
|
38
|
+
):
|
|
39
|
+
"""Index a repository into the knowledge graph.
|
|
40
|
+
|
|
41
|
+
If REPO_PATH is not provided, indexes the current directory.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
emdash index start # Index current directory
|
|
45
|
+
emdash index start /path/to/repo # Index specific repo
|
|
46
|
+
emdash index start --changed-only # Only index changed files
|
|
47
|
+
emdash index start --github-prs 50 # Also index 50 PRs
|
|
48
|
+
"""
|
|
49
|
+
# Default to current directory
|
|
50
|
+
if not repo_path:
|
|
51
|
+
repo_path = os.getcwd()
|
|
52
|
+
|
|
53
|
+
# Ensure server is running
|
|
54
|
+
server = get_server_manager()
|
|
55
|
+
client = EmdashClient(server.get_server_url())
|
|
56
|
+
|
|
57
|
+
console.print(f"\n[bold cyan]Indexing[/bold cyan] {repo_path}\n")
|
|
58
|
+
|
|
59
|
+
# Build options
|
|
60
|
+
options = {
|
|
61
|
+
"changed_only": changed_only,
|
|
62
|
+
"index_git": not skip_git,
|
|
63
|
+
"index_github": github_prs,
|
|
64
|
+
"detect_communities": detect_communities,
|
|
65
|
+
"describe_communities": describe_communities,
|
|
66
|
+
}
|
|
67
|
+
if model:
|
|
68
|
+
options["model"] = model
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Stream indexing progress with spinner
|
|
72
|
+
with Status("[bold cyan]Indexing in progress...[/bold cyan]", console=console) as status:
|
|
73
|
+
for line in client.index_start_stream(repo_path, changed_only):
|
|
74
|
+
line = line.strip()
|
|
75
|
+
if line.startswith("data: "):
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(line[6:])
|
|
78
|
+
step = data.get("step") or data.get("message", "")
|
|
79
|
+
percent = data.get("percent")
|
|
80
|
+
if step:
|
|
81
|
+
if percent is not None:
|
|
82
|
+
status.update(f"[bold cyan]{step}[/bold cyan] ({percent:.0f}%)")
|
|
83
|
+
else:
|
|
84
|
+
status.update(f"[bold cyan]{step}[/bold cyan]")
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
console.print("\n[green]Indexing complete![/green]")
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
console.print(f"\n[red]Error:[/red] {e}")
|
|
92
|
+
raise click.Abort()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@index.command("status")
|
|
96
|
+
@click.argument("repo_path", required=False)
|
|
97
|
+
def index_status(repo_path: str | None):
|
|
98
|
+
"""Show current indexing status.
|
|
99
|
+
|
|
100
|
+
If REPO_PATH is not provided, checks the current directory.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
emdash index status
|
|
104
|
+
emdash index status /path/to/repo
|
|
105
|
+
"""
|
|
106
|
+
# Default to current directory
|
|
107
|
+
if not repo_path:
|
|
108
|
+
repo_path = os.getcwd()
|
|
109
|
+
|
|
110
|
+
server = get_server_manager()
|
|
111
|
+
client = EmdashClient(server.get_server_url())
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
status = client.index_status(repo_path)
|
|
115
|
+
|
|
116
|
+
console.print("\n[bold]Index Status[/bold]")
|
|
117
|
+
console.print(f" Indexed: {'[green]Yes[/green]' if status.get('is_indexed') else '[yellow]No[/yellow]'}")
|
|
118
|
+
|
|
119
|
+
if status.get("is_indexed"):
|
|
120
|
+
console.print(f" Files: {status.get('file_count', 0)}")
|
|
121
|
+
console.print(f" Functions: {status.get('function_count', 0)}")
|
|
122
|
+
console.print(f" Classes: {status.get('class_count', 0)}")
|
|
123
|
+
console.print(f" Communities: {status.get('community_count', 0)}")
|
|
124
|
+
|
|
125
|
+
if status.get("last_indexed"):
|
|
126
|
+
console.print(f" Last indexed: {status.get('last_indexed')}")
|
|
127
|
+
if status.get("last_commit"):
|
|
128
|
+
console.print(f" Last commit: {status.get('last_commit')}")
|
|
129
|
+
|
|
130
|
+
console.print()
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
console.print(f"\n[red]Error:[/red] {e}")
|
|
134
|
+
raise click.Abort()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Planning CLI commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..client import EmdashClient
|
|
10
|
+
from ..server_manager import get_server_manager
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def plan():
|
|
17
|
+
"""Feature planning commands."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@plan.command("context")
|
|
22
|
+
@click.argument("description")
|
|
23
|
+
@click.option("--similar-prs", default=5, help="Number of similar PRs to show")
|
|
24
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
25
|
+
def plan_context(description: str, similar_prs: int, output_json: bool):
|
|
26
|
+
"""Get planning context for a feature.
|
|
27
|
+
|
|
28
|
+
Finds similar PRs and relevant code patterns to help plan
|
|
29
|
+
the implementation of a new feature.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
emdash plan context "add dark mode toggle"
|
|
33
|
+
emdash plan context "user authentication" --similar-prs 10
|
|
34
|
+
"""
|
|
35
|
+
server = get_server_manager()
|
|
36
|
+
client = EmdashClient(server.get_server_url())
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = client.plan_context(
|
|
40
|
+
description=description,
|
|
41
|
+
similar_prs=similar_prs,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if output_json:
|
|
45
|
+
console.print(json.dumps(result, indent=2))
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Display similar PRs
|
|
49
|
+
prs = result.get("similar_prs", [])
|
|
50
|
+
if prs:
|
|
51
|
+
table = Table(title="Similar PRs")
|
|
52
|
+
table.add_column("Score", justify="right", style="dim")
|
|
53
|
+
table.add_column("PR", style="cyan")
|
|
54
|
+
table.add_column("Title")
|
|
55
|
+
|
|
56
|
+
for pr in prs:
|
|
57
|
+
table.add_row(
|
|
58
|
+
f"{pr.get('score', 0):.3f}",
|
|
59
|
+
f"#{pr.get('number', '')}",
|
|
60
|
+
pr.get("title", ""),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
console.print(table)
|
|
64
|
+
else:
|
|
65
|
+
console.print("[dim]No similar PRs found[/dim]")
|
|
66
|
+
|
|
67
|
+
# Display relevant patterns
|
|
68
|
+
patterns = result.get("patterns", [])
|
|
69
|
+
if patterns:
|
|
70
|
+
console.print()
|
|
71
|
+
console.print("[bold]Relevant Patterns[/bold]")
|
|
72
|
+
for pattern in patterns:
|
|
73
|
+
console.print(f" • {pattern}")
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
77
|
+
raise click.Abort()
|