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.
@@ -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()