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,51 @@
1
+ """PROJECT.md generation CLI command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from ..client import EmdashClient
7
+ from ..server_manager import get_server_manager
8
+ from ..sse_renderer import SSERenderer
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command("projectmd")
14
+ @click.option("--output", "-o", default="PROJECT.md", help="Output file path")
15
+ @click.option("--save/--no-save", default=True, help="Save to file")
16
+ @click.option("--model", "-m", default=None, help="Model to use")
17
+ @click.option("--quiet", "-q", is_flag=True, help="Hide exploration progress")
18
+ def projectmd(output: str, save: bool, model: str, quiet: bool):
19
+ """Generate PROJECT.md by exploring the codebase.
20
+
21
+ Uses AI to analyze the code graph and generate a comprehensive
22
+ project document that describes architecture, patterns, and
23
+ key components.
24
+
25
+ Examples:
26
+ emdash projectmd
27
+ emdash projectmd --output docs/PROJECT.md
28
+ emdash projectmd --model gpt-4
29
+ """
30
+ server = get_server_manager()
31
+ client = EmdashClient(server.get_server_url())
32
+ renderer = SSERenderer(console=console, verbose=not quiet)
33
+
34
+ try:
35
+ console.print("[cyan]Generating PROJECT.md...[/cyan]")
36
+ console.print()
37
+
38
+ stream = client.projectmd_generate_stream(
39
+ output=output,
40
+ save=save,
41
+ model=model,
42
+ )
43
+ result = renderer.render_stream(stream)
44
+
45
+ if save:
46
+ console.print()
47
+ console.print(f"[green]Saved to {output}[/green]")
48
+
49
+ except Exception as e:
50
+ console.print(f"[red]Error: {e}[/red]")
51
+ raise click.Abort()
@@ -0,0 +1,47 @@
1
+ """Research CLI command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from ..client import EmdashClient
7
+ from ..server_manager import get_server_manager
8
+ from ..sse_renderer import SSERenderer
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command("research")
14
+ @click.argument("goal")
15
+ @click.option("--max-iterations", default=10, help="Maximum research iterations")
16
+ @click.option("--budget", default=50, help="Token budget (in thousands)")
17
+ @click.option("--model", "-m", default=None, help="Model to use")
18
+ @click.option("--quiet", "-q", is_flag=True, help="Hide progress output")
19
+ def research(goal: str, max_iterations: int, budget: int, model: str, quiet: bool):
20
+ """Deep research with multi-LLM loops and critic evaluation.
21
+
22
+ Performs iterative research on a goal, using a planner, researcher,
23
+ and critic to synthesize comprehensive answers.
24
+
25
+ Examples:
26
+ emdash research "How does authentication work?"
27
+ emdash research "What are the main API endpoints?" --max-iterations 15
28
+ """
29
+ server = get_server_manager()
30
+ client = EmdashClient(server.get_server_url())
31
+ renderer = SSERenderer(console=console, verbose=not quiet)
32
+
33
+ try:
34
+ console.print(f"[cyan]Researching: {goal}[/cyan]")
35
+ console.print()
36
+
37
+ stream = client.research_stream(
38
+ goal=goal,
39
+ max_iterations=max_iterations,
40
+ budget=budget,
41
+ model=model,
42
+ )
43
+ renderer.render_stream(stream)
44
+
45
+ except Exception as e:
46
+ console.print(f"[red]Error: {e}[/red]")
47
+ raise click.Abort()
@@ -0,0 +1,93 @@
1
+ """Rules/templates management CLI commands."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+
8
+ from ..client import EmdashClient
9
+ from ..server_manager import get_server_manager
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.group()
15
+ def rules():
16
+ """Manage .emdash-rules templates."""
17
+ pass
18
+
19
+
20
+ @rules.command("init")
21
+ @click.option("--global", "global_", is_flag=True, help="Save to ~/.emdash-rules instead of ./.emdash-rules")
22
+ @click.option("--force", is_flag=True, help="Overwrite existing templates")
23
+ def rules_init(global_: bool, force: bool):
24
+ """Initialize custom templates in .emdash-rules directory."""
25
+ server = get_server_manager()
26
+ client = EmdashClient(server.get_server_url())
27
+
28
+ try:
29
+ result = client.rules_init(global_templates=global_, force=force)
30
+
31
+ if result.get("success"):
32
+ path = result.get("path")
33
+ templates = result.get("templates", [])
34
+ console.print(f"[green]Templates initialized at {path}[/green]")
35
+ console.print(f"[dim]Copied {len(templates)} templates[/dim]")
36
+ else:
37
+ console.print(f"[red]Error: {result.get('error')}[/red]")
38
+
39
+ except Exception as e:
40
+ console.print(f"[red]Error: {e}[/red]")
41
+ raise click.Abort()
42
+
43
+
44
+ @rules.command("show")
45
+ @click.argument("template", type=click.Choice(["spec", "tasks", "project", "focus", "pr-review"]))
46
+ def rules_show(template: str):
47
+ """Show the active template and its source."""
48
+ server = get_server_manager()
49
+ client = EmdashClient(server.get_server_url())
50
+
51
+ try:
52
+ result = client.rules_get(template)
53
+
54
+ source = result.get("source", "unknown")
55
+ content = result.get("content", "")
56
+
57
+ console.print(Panel(
58
+ content,
59
+ title=f"[cyan]{template}[/cyan] [dim]({source})[/dim]",
60
+ border_style="dim",
61
+ ))
62
+
63
+ except Exception as e:
64
+ console.print(f"[red]Error: {e}[/red]")
65
+ raise click.Abort()
66
+
67
+
68
+ @rules.command("list")
69
+ def rules_list():
70
+ """List all templates and their active sources."""
71
+ server = get_server_manager()
72
+ client = EmdashClient(server.get_server_url())
73
+
74
+ try:
75
+ templates = client.rules_list()
76
+
77
+ table = Table(title="Templates")
78
+ table.add_column("Name", style="cyan")
79
+ table.add_column("Source")
80
+ table.add_column("Description")
81
+
82
+ for t in templates:
83
+ table.add_row(
84
+ t.get("name", ""),
85
+ t.get("source", ""),
86
+ t.get("description", ""),
87
+ )
88
+
89
+ console.print(table)
90
+
91
+ except Exception as e:
92
+ console.print(f"[red]Error: {e}[/red]")
93
+ raise click.Abort()
@@ -0,0 +1,56 @@
1
+ """Search CLI command."""
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.command("search")
14
+ @click.argument("query")
15
+ @click.option("--semantic/--text", default=True, help="Use semantic search (default) or text search")
16
+ @click.option("--limit", default=20, help="Maximum results")
17
+ def search(query: str, semantic: bool, limit: int):
18
+ """Search codebase by semantic similarity or text.
19
+
20
+ Examples:
21
+ emdash search "authentication flow"
22
+ emdash search "login" --text
23
+ emdash search "error handling" --limit 10
24
+ """
25
+ server = get_server_manager()
26
+ client = EmdashClient(server.get_server_url())
27
+
28
+ try:
29
+ search_type = "semantic" if semantic else "text"
30
+ result = client.search(query=query, search_type=search_type, limit=limit)
31
+
32
+ results = result.get("results", [])
33
+
34
+ if not results:
35
+ console.print("[yellow]No results found[/yellow]")
36
+ return
37
+
38
+ table = Table(title=f"Search Results ({len(results)})")
39
+ table.add_column("Score", justify="right", style="dim")
40
+ table.add_column("Type")
41
+ table.add_column("Name", style="cyan")
42
+ table.add_column("File")
43
+
44
+ for r in results:
45
+ table.add_row(
46
+ f"{r.get('score', 0):.3f}",
47
+ r.get("type", ""),
48
+ r.get("name", ""),
49
+ r.get("file", ""),
50
+ )
51
+
52
+ console.print(table)
53
+
54
+ except Exception as e:
55
+ console.print(f"[red]Error: {e}[/red]")
56
+ raise click.Abort()
@@ -0,0 +1,117 @@
1
+ """Server management commands."""
2
+
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from rich.console import Console
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.group()
15
+ def server():
16
+ """Manage the emdash-core server."""
17
+ pass
18
+
19
+
20
+ @server.command("killall")
21
+ def server_killall():
22
+ """Kill all running emdash servers.
23
+
24
+ Example:
25
+ emdash server killall
26
+ """
27
+ killed = 0
28
+
29
+ # Kill by PID file first
30
+ pid_file = Path.home() / ".emdash" / "server.pid"
31
+ if pid_file.exists():
32
+ try:
33
+ pid = int(pid_file.read_text().strip())
34
+ os.kill(pid, signal.SIGTERM)
35
+ console.print(f"[green]Killed server process {pid}[/green]")
36
+ killed += 1
37
+ except (ValueError, ProcessLookupError, PermissionError):
38
+ pass
39
+ finally:
40
+ pid_file.unlink(missing_ok=True)
41
+
42
+ # Clean up port file
43
+ port_file = Path.home() / ".emdash" / "server.port"
44
+ if port_file.exists():
45
+ port_file.unlink(missing_ok=True)
46
+
47
+ # Kill any remaining emdash_core.server processes
48
+ try:
49
+ result = subprocess.run(
50
+ ["pgrep", "-f", "emdash_core.server"],
51
+ capture_output=True,
52
+ text=True,
53
+ )
54
+ if result.returncode == 0:
55
+ pids = result.stdout.strip().split("\n")
56
+ for pid_str in pids:
57
+ if pid_str:
58
+ try:
59
+ pid = int(pid_str)
60
+ os.kill(pid, signal.SIGTERM)
61
+ console.print(f"[green]Killed server process {pid}[/green]")
62
+ killed += 1
63
+ except (ValueError, ProcessLookupError, PermissionError):
64
+ pass
65
+ except FileNotFoundError:
66
+ # pgrep not available, try pkill
67
+ subprocess.run(
68
+ ["pkill", "-f", "emdash_core.server"],
69
+ capture_output=True,
70
+ )
71
+
72
+ if killed > 0:
73
+ console.print(f"\n[bold green]Killed {killed} server(s)[/bold green]")
74
+ else:
75
+ console.print("[yellow]No running servers found[/yellow]")
76
+
77
+
78
+ @server.command("status")
79
+ def server_status():
80
+ """Show server status.
81
+
82
+ Example:
83
+ emdash server status
84
+ """
85
+ port_file = Path.home() / ".emdash" / "server.port"
86
+ pid_file = Path.home() / ".emdash" / "server.pid"
87
+
88
+ if not port_file.exists():
89
+ console.print("[yellow]No server running[/yellow]")
90
+ return
91
+
92
+ try:
93
+ port = int(port_file.read_text().strip())
94
+ except (ValueError, IOError):
95
+ console.print("[yellow]No server running (invalid port file)[/yellow]")
96
+ return
97
+
98
+ # Check if server is responsive
99
+ import httpx
100
+ try:
101
+ response = httpx.get(f"http://localhost:{port}/api/health", timeout=2.0)
102
+ if response.status_code == 200:
103
+ pid = "unknown"
104
+ if pid_file.exists():
105
+ try:
106
+ pid = pid_file.read_text().strip()
107
+ except IOError:
108
+ pass
109
+
110
+ console.print(f"[bold green]Server running[/bold green]")
111
+ console.print(f" Port: {port}")
112
+ console.print(f" PID: {pid}")
113
+ console.print(f" URL: http://localhost:{port}")
114
+ else:
115
+ console.print(f"[yellow]Server on port {port} not healthy[/yellow]")
116
+ except (httpx.RequestError, httpx.TimeoutException):
117
+ console.print(f"[yellow]Server on port {port} not responding[/yellow]")
@@ -0,0 +1,49 @@
1
+ """Spec generation CLI command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from ..client import EmdashClient
7
+ from ..server_manager import get_server_manager
8
+ from ..sse_renderer import SSERenderer
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command("spec")
14
+ @click.argument("feature")
15
+ @click.option("--save", is_flag=True, help="Save spec to specs/<feature>/")
16
+ @click.option("--model", "-m", default=None, help="Model to use")
17
+ @click.option("--quiet", "-q", is_flag=True, help="Hide exploration progress")
18
+ def spec(feature: str, save: bool, model: str, quiet: bool):
19
+ """Generate a detailed specification for a feature.
20
+
21
+ Explores the codebase to understand existing patterns and generates
22
+ a comprehensive specification document.
23
+
24
+ Examples:
25
+ emdash spec "user authentication"
26
+ emdash spec "dark mode toggle" --save
27
+ """
28
+ server = get_server_manager()
29
+ client = EmdashClient(server.get_server_url())
30
+ renderer = SSERenderer(console=console, verbose=not quiet)
31
+
32
+ try:
33
+ console.print(f"[cyan]Generating spec for: {feature}[/cyan]")
34
+ console.print()
35
+
36
+ stream = client.spec_generate_stream(
37
+ feature=feature,
38
+ model=model,
39
+ save=save,
40
+ )
41
+ renderer.render_stream(stream)
42
+
43
+ if save:
44
+ console.print()
45
+ console.print(f"[green]Spec saved to specs/{feature}/[/green]")
46
+
47
+ except Exception as e:
48
+ console.print(f"[red]Error: {e}[/red]")
49
+ raise click.Abort()
@@ -0,0 +1,86 @@
1
+ """Swarm multi-agent CLI commands."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from ..client import EmdashClient
7
+ from ..server_manager import get_server_manager
8
+ from ..sse_renderer import SSERenderer
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.group()
14
+ def swarm():
15
+ """Multi-agent parallel execution with git worktrees."""
16
+ pass
17
+
18
+
19
+ @swarm.command("run")
20
+ @click.argument("tasks", nargs=-1, required=True)
21
+ @click.option("--model", "-m", default=None, help="Model to use")
22
+ @click.option("--auto-merge", is_flag=True, help="Automatically merge completed tasks")
23
+ @click.option("--quiet", "-q", is_flag=True, help="Hide progress output")
24
+ def swarm_run(tasks: tuple, model: str, auto_merge: bool, quiet: bool):
25
+ """Run multiple tasks in parallel using git worktrees.
26
+
27
+ Each task runs in its own worktree with a dedicated agent.
28
+ Tasks can be merged after completion.
29
+
30
+ Examples:
31
+ emdash swarm run "Fix login bug" "Add logout button"
32
+ emdash swarm run "Task 1" "Task 2" "Task 3" --auto-merge
33
+ """
34
+ server = get_server_manager()
35
+ client = EmdashClient(server.get_server_url())
36
+ renderer = SSERenderer(console=console, verbose=not quiet)
37
+
38
+ try:
39
+ console.print(f"[cyan]Starting swarm with {len(tasks)} tasks...[/cyan]")
40
+ console.print()
41
+
42
+ stream = client.swarm_run_stream(
43
+ tasks=list(tasks),
44
+ model=model,
45
+ auto_merge=auto_merge,
46
+ )
47
+ renderer.render_stream(stream)
48
+
49
+ except Exception as e:
50
+ console.print(f"[red]Error: {e}[/red]")
51
+ raise click.Abort()
52
+
53
+
54
+ @swarm.command("status")
55
+ def swarm_status():
56
+ """Show status of current swarm execution."""
57
+ server = get_server_manager()
58
+ client = EmdashClient(server.get_server_url())
59
+
60
+ try:
61
+ status = client.swarm_status()
62
+
63
+ if not status.get("active"):
64
+ console.print("[dim]No active swarm execution[/dim]")
65
+ return
66
+
67
+ console.print("[bold]Swarm Status[/bold]")
68
+ console.print()
69
+
70
+ tasks = status.get("tasks", [])
71
+ for task in tasks:
72
+ task_status = task.get("status", "unknown")
73
+ name = task.get("name", "Unknown task")
74
+
75
+ if task_status == "completed":
76
+ console.print(f" [green]✓[/green] {name}")
77
+ elif task_status == "running":
78
+ console.print(f" [cyan]⟳[/cyan] {name}")
79
+ elif task_status == "failed":
80
+ console.print(f" [red]✗[/red] {name}")
81
+ else:
82
+ console.print(f" [dim]○[/dim] {name}")
83
+
84
+ except Exception as e:
85
+ console.print(f"[red]Error: {e}[/red]")
86
+ raise click.Abort()
@@ -0,0 +1,52 @@
1
+ """Tasks generation CLI command."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from ..client import EmdashClient
7
+ from ..server_manager import get_server_manager
8
+ from ..sse_renderer import SSERenderer
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.command("tasks")
14
+ @click.argument("spec_name", required=False)
15
+ @click.option("--save", is_flag=True, help="Save tasks to spec directory")
16
+ @click.option("--model", "-m", default=None, help="Model to use")
17
+ @click.option("--quiet", "-q", is_flag=True, help="Hide progress output")
18
+ def tasks(spec_name: str, save: bool, model: str, quiet: bool):
19
+ """Generate implementation tasks from a specification.
20
+
21
+ If spec_name is provided, loads the spec from specs/<spec_name>/spec.json.
22
+ Otherwise, uses the most recent spec from the current session.
23
+
24
+ Examples:
25
+ emdash tasks
26
+ emdash tasks "user-authentication" --save
27
+ """
28
+ server = get_server_manager()
29
+ client = EmdashClient(server.get_server_url())
30
+ renderer = SSERenderer(console=console, verbose=not quiet)
31
+
32
+ try:
33
+ if spec_name:
34
+ console.print(f"[cyan]Generating tasks for: {spec_name}[/cyan]")
35
+ else:
36
+ console.print("[cyan]Generating tasks from current spec...[/cyan]")
37
+ console.print()
38
+
39
+ stream = client.tasks_generate_stream(
40
+ spec_name=spec_name,
41
+ model=model,
42
+ save=save,
43
+ )
44
+ renderer.render_stream(stream)
45
+
46
+ if save:
47
+ console.print()
48
+ console.print("[green]Tasks saved[/green]")
49
+
50
+ except Exception as e:
51
+ console.print(f"[red]Error: {e}[/red]")
52
+ raise click.Abort()
@@ -0,0 +1,51 @@
1
+ """Team analysis CLI commands."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.markdown import Markdown
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 team():
15
+ """Team activity and collaboration analysis commands."""
16
+ pass
17
+
18
+
19
+ @team.command("focus")
20
+ @click.option("--days", default=14, help="Days to look back")
21
+ @click.option("--model", "-m", default=None, help="Model to use")
22
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
23
+ def team_focus(days: int, model: str, output_json: bool):
24
+ """Get team's recent focus and work-in-progress.
25
+
26
+ Analyzes recent commits, PRs, and code changes to identify
27
+ what the team is working on.
28
+
29
+ Examples:
30
+ emdash team focus
31
+ emdash team focus --days 7
32
+ """
33
+ server = get_server_manager()
34
+ client = EmdashClient(server.get_server_url())
35
+
36
+ try:
37
+ result = client.team_focus(days=days, model=model)
38
+
39
+ if output_json:
40
+ import json
41
+ console.print(json.dumps(result, indent=2))
42
+ else:
43
+ content = result.get("content", "")
44
+ if content:
45
+ console.print(Markdown(content))
46
+ else:
47
+ console.print("[yellow]No focus data available[/yellow]")
48
+
49
+ except Exception as e:
50
+ console.print(f"[red]Error: {e}[/red]")
51
+ raise click.Abort()