emdash-cli 0.1.46__py3-none-any.whl → 0.1.67__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.
Files changed (39) hide show
  1. emdash_cli/client.py +12 -28
  2. emdash_cli/commands/__init__.py +2 -2
  3. emdash_cli/commands/agent/constants.py +10 -0
  4. emdash_cli/commands/agent/handlers/__init__.py +10 -0
  5. emdash_cli/commands/agent/handlers/agents.py +67 -39
  6. emdash_cli/commands/agent/handlers/index.py +183 -0
  7. emdash_cli/commands/agent/handlers/misc.py +119 -0
  8. emdash_cli/commands/agent/handlers/registry.py +72 -0
  9. emdash_cli/commands/agent/handlers/rules.py +48 -31
  10. emdash_cli/commands/agent/handlers/sessions.py +1 -1
  11. emdash_cli/commands/agent/handlers/setup.py +187 -54
  12. emdash_cli/commands/agent/handlers/skills.py +42 -4
  13. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  14. emdash_cli/commands/agent/handlers/todos.py +55 -34
  15. emdash_cli/commands/agent/handlers/verify.py +10 -5
  16. emdash_cli/commands/agent/help.py +236 -0
  17. emdash_cli/commands/agent/interactive.py +222 -37
  18. emdash_cli/commands/agent/menus.py +116 -84
  19. emdash_cli/commands/agent/onboarding.py +619 -0
  20. emdash_cli/commands/agent/session_restore.py +210 -0
  21. emdash_cli/commands/index.py +111 -13
  22. emdash_cli/commands/registry.py +635 -0
  23. emdash_cli/commands/skills.py +72 -6
  24. emdash_cli/design.py +328 -0
  25. emdash_cli/diff_renderer.py +438 -0
  26. emdash_cli/integrations/__init__.py +1 -0
  27. emdash_cli/integrations/telegram/__init__.py +15 -0
  28. emdash_cli/integrations/telegram/bot.py +402 -0
  29. emdash_cli/integrations/telegram/bridge.py +865 -0
  30. emdash_cli/integrations/telegram/config.py +155 -0
  31. emdash_cli/integrations/telegram/formatter.py +385 -0
  32. emdash_cli/main.py +52 -2
  33. emdash_cli/sse_renderer.py +632 -171
  34. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -2
  35. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  36. emdash_cli/commands/swarm.py +0 -86
  37. emdash_cli-0.1.46.dist-info/RECORD +0 -49
  38. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  39. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
emdash_cli/client.py CHANGED
@@ -259,6 +259,18 @@ class EmdashClient:
259
259
  """
260
260
  return self._client.get(f"{self.base_url}{path}")
261
261
 
262
+ def post(self, path: str, json: dict | None = None) -> "httpx.Response":
263
+ """Make a POST request to the API.
264
+
265
+ Args:
266
+ path: API path (e.g., "/api/agent/chat/123/compact")
267
+ json: Optional JSON body
268
+
269
+ Returns:
270
+ HTTP response
271
+ """
272
+ return self._client.post(f"{self.base_url}{path}", json=json)
273
+
262
274
  def list_sessions(self) -> list[dict]:
263
275
  """List active agent sessions.
264
276
 
@@ -660,34 +672,6 @@ class EmdashClient:
660
672
  response.raise_for_status()
661
673
  return response.json()
662
674
 
663
- # ==================== Swarm ====================
664
-
665
- def swarm_run_stream(
666
- self,
667
- tasks: list[str],
668
- model: Optional[str] = None,
669
- auto_merge: bool = False,
670
- ) -> Iterator[str]:
671
- """Run multi-agent swarm with SSE streaming."""
672
- payload = {"tasks": tasks, "auto_merge": auto_merge}
673
- if model:
674
- payload["model"] = model
675
-
676
- with self._client.stream(
677
- "POST",
678
- f"{self.base_url}/api/swarm/run",
679
- json=payload,
680
- ) as response:
681
- response.raise_for_status()
682
- for line in response.iter_lines():
683
- yield line
684
-
685
- def swarm_status(self) -> dict:
686
- """Get swarm execution status."""
687
- response = self._client.get(f"{self.base_url}/api/swarm/status")
688
- response.raise_for_status()
689
- return response.json()
690
-
691
675
  # ==================== Todos ====================
692
676
 
693
677
  def get_todos(self, session_id: str) -> dict:
@@ -7,12 +7,12 @@ from .analyze import analyze
7
7
  from .embed import embed
8
8
  from .index import index
9
9
  from .plan import plan
10
+ from .registry import registry
10
11
  from .rules import rules
11
12
  from .search import search
12
13
  from .server import server
13
14
  from .skills import skills
14
15
  from .team import team
15
- from .swarm import swarm
16
16
  from .projectmd import projectmd
17
17
  from .research import research
18
18
  from .spec import spec
@@ -26,12 +26,12 @@ __all__ = [
26
26
  "embed",
27
27
  "index",
28
28
  "plan",
29
+ "registry",
29
30
  "rules",
30
31
  "search",
31
32
  "server",
32
33
  "skills",
33
34
  "team",
34
- "swarm",
35
35
  "projectmd",
36
36
  "research",
37
37
  "spec",
@@ -21,6 +21,7 @@ SLASH_COMMANDS = {
21
21
  "/research [goal]": "Deep research on a topic",
22
22
  # Status commands
23
23
  "/status": "Show index and PROJECT.md status",
24
+ "/diff": "Show uncommitted changes in GitHub-style diff view",
24
25
  "/agents": "Manage agents (interactive menu, or /agents [create|show|edit|delete] <name>)",
25
26
  # Todo management
26
27
  "/todos": "Show current agent todo list",
@@ -35,12 +36,19 @@ SLASH_COMMANDS = {
35
36
  "/rules": "Manage rules (list, add, delete)",
36
37
  # Skills
37
38
  "/skills": "Manage skills (list, show, add, delete)",
39
+ # Index
40
+ "/index": "Manage codebase index (status, start, hook install/uninstall)",
38
41
  # MCP
39
42
  "/mcp": "Manage global MCP servers (list, edit)",
43
+ # Registry
44
+ "/registry": "Browse and install community skills, rules, agents, verifiers",
40
45
  # Auth
41
46
  "/auth": "GitHub authentication (login, logout, status)",
42
47
  # Context
43
48
  "/context": "Show current context frame (tokens, reranked items)",
49
+ "/compact": "Compact message history using LLM summarization",
50
+ # Image
51
+ "/paste": "Attach image from clipboard (or use Ctrl+V)",
44
52
  # Diagnostics
45
53
  "/doctor": "Check Python environment and diagnose issues",
46
54
  # Verification
@@ -48,6 +56,8 @@ SLASH_COMMANDS = {
48
56
  "/verify-loop [task]": "Run task in loop until verifications pass",
49
57
  # Setup wizard
50
58
  "/setup": "Setup wizard for rules, agents, skills, and verifiers",
59
+ # Telegram integration
60
+ "/telegram": "Telegram integration (setup, connect, status, test)",
51
61
  "/help": "Show available commands",
52
62
  "/quit": "Exit the agent",
53
63
  }
@@ -6,7 +6,9 @@ from .todos import handle_todos, handle_todo_add
6
6
  from .hooks import handle_hooks
7
7
  from .rules import handle_rules
8
8
  from .skills import handle_skills
9
+ from .index import handle_index
9
10
  from .mcp import handle_mcp
11
+ from .registry import handle_registry
10
12
  from .auth import handle_auth
11
13
  from .doctor import handle_doctor
12
14
  from .verify import handle_verify, handle_verify_loop
@@ -17,7 +19,10 @@ from .misc import (
17
19
  handle_projectmd,
18
20
  handle_research,
19
21
  handle_context,
22
+ handle_compact,
23
+ handle_diff,
20
24
  )
25
+ from .telegram import handle_telegram
21
26
 
22
27
  __all__ = [
23
28
  "handle_agents",
@@ -27,7 +32,9 @@ __all__ = [
27
32
  "handle_hooks",
28
33
  "handle_rules",
29
34
  "handle_skills",
35
+ "handle_index",
30
36
  "handle_mcp",
37
+ "handle_registry",
31
38
  "handle_auth",
32
39
  "handle_doctor",
33
40
  "handle_verify",
@@ -38,4 +45,7 @@ __all__ = [
38
45
  "handle_projectmd",
39
46
  "handle_research",
40
47
  "handle_context",
48
+ "handle_compact",
49
+ "handle_diff",
50
+ "handle_telegram",
41
51
  ]
@@ -8,6 +8,14 @@ from rich.console import Console
8
8
  from rich.panel import Panel
9
9
 
10
10
  from ..menus import show_agents_interactive_menu, prompt_agent_name, confirm_delete
11
+ from ....design import (
12
+ Colors,
13
+ header,
14
+ footer,
15
+ SEPARATOR_WIDTH,
16
+ STATUS_ACTIVE,
17
+ ARROW_PROMPT,
18
+ )
11
19
 
12
20
  console = Console()
13
21
 
@@ -26,6 +34,9 @@ def create_agent(name: str) -> bool:
26
34
  template = f'''---
27
35
  description: Custom agent for specific tasks
28
36
  tools: [grep, glob, read_file, semantic_search]
37
+ # rules: [typescript, security] # Optional: reference rules from .emdash/rules/
38
+ # skills: [code-review] # Optional: reference skills from .emdash/skills/
39
+ # verifiers: [eslint] # Optional: reference verifiers from .emdash/verifiers.json
29
40
  ---
30
41
 
31
42
  # System Prompt
@@ -52,8 +63,10 @@ Describe what this agent should accomplish:
52
63
  Describe how the agent should format its responses.
53
64
  '''
54
65
  agent_file.write_text(template)
55
- console.print(f"[green]Created agent: {name}[/green]")
56
- console.print(f"[dim]File: {agent_file}[/dim]")
66
+ console.print()
67
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.TEXT}]created:[/{Colors.TEXT}] {name}")
68
+ console.print(f" [{Colors.DIM}]{agent_file}[/{Colors.DIM}]")
69
+ console.print()
57
70
  return True
58
71
 
59
72
 
@@ -64,57 +77,67 @@ def show_agent_details(name: str) -> None:
64
77
  builtin_agents = ["Explore", "Plan"]
65
78
 
66
79
  console.print()
67
- console.print("[dim]─" * 50 + "[/dim]")
80
+ console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
68
81
  console.print()
82
+
69
83
  if name in builtin_agents:
70
- console.print(f"[bold cyan]{name}[/bold cyan] [dim](built-in)[/dim]\n")
84
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.MUTED}]built-in[/{Colors.MUTED}]")
71
85
  if name == "Explore":
72
- console.print("[bold]Description:[/bold] Fast codebase exploration (read-only)")
73
- console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
86
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Fast codebase exploration (read-only)")
87
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
74
88
  elif name == "Plan":
75
- console.print("[bold]Description:[/bold] Design implementation plans")
76
- console.print("[bold]Tools:[/bold] glob, grep, read_file, list_files, semantic_search")
77
- console.print("\n[dim]Built-in agents cannot be edited or deleted.[/dim]")
89
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] Design implementation plans")
90
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] glob, grep, read_file, list_files, semantic_search")
91
+ console.print()
92
+ console.print(f" [{Colors.DIM}]Built-in agents cannot be edited or deleted.[/{Colors.DIM}]")
78
93
  else:
79
94
  agent = get_custom_agent(name, Path.cwd())
80
95
  if agent:
81
- console.print(f"[bold cyan]{agent.name}[/bold cyan] [dim](custom)[/dim]\n")
96
+ console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.PRIMARY}]custom[/{Colors.PRIMARY}]")
82
97
 
83
- # Show description
84
98
  if agent.description:
85
- console.print(f"[bold]Description:[/bold] {agent.description}")
99
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] {agent.description}")
86
100
 
87
- # Show model
88
101
  if agent.model:
89
- console.print(f"[bold]Model:[/bold] {agent.model}")
102
+ console.print(f" [{Colors.DIM}]model[/{Colors.DIM}] {agent.model}")
90
103
 
91
- # Show tools
92
104
  if agent.tools:
93
- console.print(f"[bold]Tools:[/bold] {', '.join(agent.tools)}")
105
+ console.print(f" [{Colors.DIM}]tools[/{Colors.DIM}] {', '.join(agent.tools)}")
94
106
 
95
- # Show MCP servers
96
107
  if agent.mcp_servers:
97
- console.print(f"\n[bold]MCP Servers:[/bold]")
108
+ console.print()
109
+ console.print(f" [{Colors.DIM}]mcp servers:[/{Colors.DIM}]")
98
110
  for server in agent.mcp_servers:
99
- status = "[green]enabled[/green]" if server.enabled else "[dim]disabled[/dim]"
100
- console.print(f" [cyan]{server.name}[/cyan] ({status})")
101
- console.print(f" [dim]{server.command} {' '.join(server.args)}[/dim]")
111
+ status = f"[{Colors.SUCCESS}][/{Colors.SUCCESS}]" if server.enabled else f"[{Colors.MUTED}][/{Colors.MUTED}]"
112
+ console.print(f" {status} [{Colors.PRIMARY}]{server.name}[/{Colors.PRIMARY}]")
113
+ console.print(f" [{Colors.DIM}]{server.command} {' '.join(server.args)}[/{Colors.DIM}]")
114
+
115
+ if agent.rules:
116
+ console.print(f" [{Colors.DIM}]rules[/{Colors.DIM}] {', '.join(agent.rules)}")
117
+
118
+ if agent.skills:
119
+ console.print(f" [{Colors.DIM}]skills[/{Colors.DIM}] {', '.join(agent.skills)}")
120
+
121
+ if agent.verifiers:
122
+ console.print(f" [{Colors.DIM}]verify[/{Colors.DIM}] {', '.join(agent.verifiers)}")
102
123
 
103
- # Show file path
104
124
  if agent.file_path:
105
- console.print(f"\n[bold]File:[/bold] {agent.file_path}")
125
+ console.print()
126
+ console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {agent.file_path}")
106
127
 
107
- # Show system prompt preview
108
128
  if agent.system_prompt:
109
- console.print(f"\n[bold]System Prompt Preview:[/bold]")
110
- preview = agent.system_prompt[:300]
111
- if len(agent.system_prompt) > 300:
129
+ console.print()
130
+ console.print(f" [{Colors.DIM}]prompt preview:[/{Colors.DIM}]")
131
+ preview = agent.system_prompt[:250]
132
+ if len(agent.system_prompt) > 250:
112
133
  preview += "..."
113
- console.print(Panel(preview, border_style="dim"))
134
+ for line in preview.split('\n')[:6]:
135
+ console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
114
136
  else:
115
- console.print(f"[yellow]Agent '{name}' not found[/yellow]")
137
+ console.print(f" [{Colors.WARNING}]Agent '{name}' not found[/{Colors.WARNING}]")
138
+
116
139
  console.print()
117
- console.print("[dim]─" * 50 + "[/dim]")
140
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
118
141
 
119
142
 
120
143
  def delete_agent(name: str) -> bool:
@@ -177,19 +200,20 @@ def chat_edit_agent(name: str, client, renderer, model, max_iterations, render_w
177
200
  agent_file = agents_dir / f"{name}.md"
178
201
 
179
202
  if not agent_file.exists():
180
- console.print(f"[yellow]Agent file not found: {agent_file}[/yellow]")
203
+ console.print(f" [{Colors.WARNING}]Agent file not found: {agent_file}[/{Colors.WARNING}]")
181
204
  return
182
205
 
183
206
  # Read current content
184
207
  content = agent_file.read_text()
185
208
 
186
209
  console.print()
187
- console.print(f"[bold cyan]Chat: Editing agent '{name}'[/bold cyan]")
188
- console.print("[dim]What would you like to change? Type 'done' to finish, Ctrl+C to cancel[/dim]")
210
+ console.print(f"[{Colors.MUTED}]{header(f'Edit: {name}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
211
+ console.print()
212
+ console.print(f" [{Colors.DIM}]Describe changes. Type 'done' to finish.[/{Colors.DIM}]")
189
213
  console.print()
190
214
 
191
215
  chat_style = Style.from_dict({
192
- "prompt": "#00cc66 bold",
216
+ "prompt": f"{Colors.PRIMARY} bold",
193
217
  })
194
218
 
195
219
  ps = PromptSession(style=chat_style)
@@ -265,13 +289,14 @@ def chat_create_agent(client, renderer, model, max_iterations, render_with_inter
265
289
  agents_dir = Path.cwd() / ".emdash" / "agents"
266
290
 
267
291
  console.print()
268
- console.print("[bold cyan]Create New Agent[/bold cyan]")
269
- console.print("[dim]Describe what agent you want to create. The AI will help you design it.[/dim]")
270
- console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
292
+ console.print(f"[{Colors.MUTED}]{header('Create Agent', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
293
+ console.print()
294
+ console.print(f" [{Colors.DIM}]Describe your agent. AI will help design it.[/{Colors.DIM}]")
295
+ console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
271
296
  console.print()
272
297
 
273
298
  chat_style = Style.from_dict({
274
- "prompt": "#00cc66 bold",
299
+ "prompt": f"{Colors.PRIMARY} bold",
275
300
  })
276
301
 
277
302
  ps = PromptSession(style=chat_style)
@@ -399,7 +424,7 @@ def handle_agents(args: str, client, renderer, model, max_iterations, render_wit
399
424
  is_custom = agent_name not in ("Explore", "Plan")
400
425
  try:
401
426
  if is_custom:
402
- console.print("[dim]'c' chat • 'e' edit • Enter back[/dim]", end="")
427
+ console.print("[cyan]'c'[/cyan] chat • [cyan]'e'[/cyan] edit • [red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
403
428
  else:
404
429
  console.print("[dim]Press Enter to go back...[/dim]", end="")
405
430
  ps = PromptSession()
@@ -408,6 +433,9 @@ def handle_agents(args: str, client, renderer, model, max_iterations, render_wit
408
433
  chat_edit_agent(agent_name, client, renderer, model, max_iterations, render_with_interrupt)
409
434
  elif is_custom and resp == 'e':
410
435
  edit_agent(agent_name)
436
+ elif is_custom and resp == 'd':
437
+ if delete_agent(agent_name):
438
+ continue # Refresh menu after deletion
411
439
  console.print() # Add spacing before menu reappears
412
440
  except (KeyboardInterrupt, EOFError):
413
441
  break
@@ -0,0 +1,183 @@
1
+ """Handler for /index command."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ def handle_index(args: str, client) -> None:
12
+ """Handle /index command.
13
+
14
+ Args:
15
+ args: Command arguments (status, start, hook install/uninstall)
16
+ client: EmdashClient instance
17
+ """
18
+ # Parse subcommand
19
+ subparts = args.split(maxsplit=1) if args else []
20
+ subcommand = subparts[0].lower() if subparts else "status"
21
+ subargs = subparts[1].strip() if len(subparts) > 1 else ""
22
+
23
+ repo_path = os.getcwd()
24
+
25
+ if subcommand == "status":
26
+ _show_status(client, repo_path)
27
+
28
+ elif subcommand == "start":
29
+ _start_index(client, repo_path, subargs)
30
+
31
+ elif subcommand == "hook":
32
+ _handle_hook(repo_path, subargs)
33
+
34
+ else:
35
+ console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
36
+ console.print("[dim]Usage: /index [status|start|hook][/dim]")
37
+ console.print("[dim] /index - Show index status[/dim]")
38
+ console.print("[dim] /index start - Start incremental indexing[/dim]")
39
+ console.print("[dim] /index start --full - Force full reindex[/dim]")
40
+ console.print("[dim] /index hook install - Install post-commit hook[/dim]")
41
+ console.print("[dim] /index hook uninstall - Remove post-commit hook[/dim]")
42
+
43
+
44
+ def _show_status(client, repo_path: str) -> None:
45
+ """Show index status."""
46
+ try:
47
+ status = client.index_status(repo_path)
48
+
49
+ console.print("\n[bold cyan]Index Status[/bold cyan]\n")
50
+ is_indexed = status.get("is_indexed", False)
51
+ console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
52
+
53
+ if is_indexed:
54
+ console.print(f" Files: {status.get('file_count', 0)}")
55
+ console.print(f" Functions: {status.get('function_count', 0)}")
56
+ console.print(f" Classes: {status.get('class_count', 0)}")
57
+ console.print(f" Communities: {status.get('community_count', 0)}")
58
+
59
+ if status.get("last_indexed"):
60
+ console.print(f" Last indexed: {status.get('last_indexed')}")
61
+ if status.get("last_commit"):
62
+ console.print(f" Last commit: {status.get('last_commit')[:8]}")
63
+
64
+ # Check hook status
65
+ hooks_dir = Path(repo_path) / ".git" / "hooks"
66
+ hook_path = hooks_dir / "post-commit"
67
+ if hook_path.exists() and "emdash" in hook_path.read_text():
68
+ console.print(f" Auto-index: [green]Enabled[/green] (post-commit hook)")
69
+ else:
70
+ console.print(f" Auto-index: [dim]Disabled[/dim] (run /index hook install)")
71
+
72
+ console.print()
73
+
74
+ except Exception as e:
75
+ console.print(f"[red]Error getting status: {e}[/red]")
76
+
77
+
78
+ def _start_index(client, repo_path: str, args: str) -> None:
79
+ """Start indexing."""
80
+ import json
81
+ from rich.progress import Progress, BarColumn, TaskProgressColumn, TextColumn
82
+
83
+ # Parse options
84
+ full = "--full" in args
85
+
86
+ console.print(f"\n[bold cyan]Indexing[/bold cyan] {repo_path}\n")
87
+
88
+ try:
89
+ with Progress(
90
+ TextColumn("[bold cyan]{task.description}[/bold cyan]"),
91
+ BarColumn(bar_width=40, complete_style="cyan", finished_style="green"),
92
+ TaskProgressColumn(),
93
+ console=console,
94
+ transient=True,
95
+ ) as progress:
96
+ task = progress.add_task("Starting...", total=100)
97
+
98
+ for line in client.index_start_stream(repo_path, not full):
99
+ line = line.strip()
100
+ if line.startswith("event: "):
101
+ continue
102
+ if line.startswith("data: "):
103
+ try:
104
+ data = json.loads(line[6:])
105
+ step = data.get("step") or data.get("message", "")
106
+ percent = data.get("percent")
107
+
108
+ if step:
109
+ progress.update(task, description=step)
110
+ if percent is not None:
111
+ progress.update(task, completed=percent)
112
+ except json.JSONDecodeError:
113
+ pass
114
+
115
+ progress.update(task, completed=100, description="Complete")
116
+
117
+ console.print("[bold green]Indexing complete![/bold green]\n")
118
+
119
+ except Exception as e:
120
+ console.print(f"[red]Error: {e}[/red]")
121
+
122
+
123
+ def _handle_hook(repo_path: str, args: str) -> None:
124
+ """Handle hook install/uninstall."""
125
+ action = args.lower() if args else ""
126
+
127
+ if action not in ("install", "uninstall"):
128
+ console.print("[yellow]Usage: /index hook [install|uninstall][/yellow]")
129
+ return
130
+
131
+ hooks_dir = Path(repo_path) / ".git" / "hooks"
132
+ hook_path = hooks_dir / "post-commit"
133
+
134
+ if not hooks_dir.exists():
135
+ console.print(f"[red]Error:[/red] Not a git repository: {repo_path}")
136
+ return
137
+
138
+ hook_content = """#!/bin/sh
139
+ # emdash post-commit hook - auto-reindex on commit
140
+ # Installed by: emdash index hook install
141
+
142
+ # Run indexing in background to not block the commit
143
+ emdash index start > /dev/null 2>&1 &
144
+ """
145
+
146
+ if action == "install":
147
+ if hook_path.exists():
148
+ existing = hook_path.read_text()
149
+ if "emdash" in existing:
150
+ console.print("[yellow]Hook already installed[/yellow]")
151
+ return
152
+ else:
153
+ console.print("[yellow]Appending to existing post-commit hook[/yellow]")
154
+ with open(hook_path, "a") as f:
155
+ f.write("\n# emdash auto-index\nemdash index start > /dev/null 2>&1 &\n")
156
+ else:
157
+ hook_path.write_text(hook_content)
158
+
159
+ hook_path.chmod(0o755)
160
+ console.print(f"[green]Post-commit hook installed[/green]")
161
+ console.print("[dim]Index will update automatically after each commit[/dim]")
162
+
163
+ elif action == "uninstall":
164
+ if not hook_path.exists():
165
+ console.print("[yellow]No post-commit hook found[/yellow]")
166
+ return
167
+
168
+ existing = hook_path.read_text()
169
+ if "emdash" not in existing:
170
+ console.print("[yellow]No emdash hook found in post-commit[/yellow]")
171
+ return
172
+
173
+ if existing.strip() == hook_content.strip():
174
+ hook_path.unlink()
175
+ console.print("[green]Post-commit hook removed[/green]")
176
+ else:
177
+ lines = existing.split("\n")
178
+ new_lines = [
179
+ line for line in lines
180
+ if "emdash" not in line and "auto-reindex" not in line
181
+ ]
182
+ hook_path.write_text("\n".join(new_lines))
183
+ console.print("[green]Emdash hook lines removed from post-commit[/green]")
@@ -1,9 +1,17 @@
1
1
  """Handlers for miscellaneous slash commands."""
2
2
 
3
+ import json
4
+ import subprocess
3
5
  from datetime import datetime
4
6
  from pathlib import Path
5
7
 
6
8
  from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.syntax import Syntax
11
+ from rich.text import Text
12
+
13
+ from emdash_cli.design import Colors, EM_DASH
14
+ from emdash_cli.diff_renderer import render_diff
7
15
 
8
16
  console = Console()
9
17
 
@@ -197,4 +205,115 @@ def handle_context(renderer) -> None:
197
205
  console.print(f"\n[dim]No reranked items: {debug_info}[/dim]")
198
206
  else:
199
207
  console.print(f"\n[dim]No reranked items yet. Items appear after exploration (file reads, searches).[/dim]")
208
+
209
+ # Show full context frame as JSON
210
+ console.print(f"\n[bold]Full Context Frame:[/bold]")
211
+ context_json = json.dumps(context_data, indent=2, default=str)
212
+ syntax = Syntax(context_json, "json", theme="monokai", line_numbers=False)
213
+ console.print(syntax)
200
214
  console.print()
215
+
216
+
217
+ def handle_compact(client, session_id: str | None) -> None:
218
+ """Handle /compact command.
219
+
220
+ Manually triggers message history compaction using LLM summarization.
221
+
222
+ Args:
223
+ client: EmdashClient instance
224
+ session_id: Current session ID (if any)
225
+ """
226
+ if not session_id:
227
+ console.print("\n[yellow]No active session. Start a conversation first.[/yellow]\n")
228
+ return
229
+
230
+ console.print("\n[bold cyan]Compacting message history...[/bold cyan]\n")
231
+
232
+ try:
233
+ response = client.post(f"/api/agent/chat/{session_id}/compact")
234
+
235
+ if response.status_code == 404:
236
+ console.print("[yellow]Session not found.[/yellow]\n")
237
+ return
238
+
239
+ if response.status_code != 200:
240
+ console.print(f"[red]Error: {response.text}[/red]\n")
241
+ return
242
+
243
+ data = response.json()
244
+
245
+ if not data.get("compacted"):
246
+ reason = data.get("reason", "Unknown reason")
247
+ console.print(f"[yellow]Could not compact: {reason}[/yellow]\n")
248
+ return
249
+
250
+ # Show stats
251
+ original_msgs = data.get("original_message_count", 0)
252
+ new_msgs = data.get("new_message_count", 0)
253
+ original_tokens = data.get("original_tokens", 0)
254
+ new_tokens = data.get("new_tokens", 0)
255
+ reduction = data.get("reduction_percent", 0)
256
+
257
+ console.print("[green]✓ Compaction complete![/green]\n")
258
+ console.print(f"[bold]Messages:[/bold] {original_msgs} → {new_msgs}")
259
+ console.print(f"[bold]Tokens:[/bold] {original_tokens:,} → {new_tokens:,} ([green]-{reduction}%[/green])")
260
+
261
+ # Show the summary
262
+ summary = data.get("summary")
263
+ if summary:
264
+ console.print(f"\n[bold]Summary:[/bold]")
265
+ console.print(f"[dim]{'─' * 60}[/dim]")
266
+ console.print(summary)
267
+ console.print(f"[dim]{'─' * 60}[/dim]")
268
+
269
+ console.print()
270
+
271
+ except Exception as e:
272
+ console.print(f"[red]Error during compaction: {e}[/red]\n")
273
+
274
+
275
+ def handle_diff(args: str = "") -> None:
276
+ """Handle /diff command - show uncommitted changes in GitHub-style diff view.
277
+
278
+ Args:
279
+ args: Optional file path to show diff for specific file
280
+ """
281
+ try:
282
+ # Build git diff command
283
+ cmd = ["git", "diff", "--no-color"]
284
+ if args:
285
+ cmd.append(args)
286
+
287
+ # Also include staged changes
288
+ result_unstaged = subprocess.run(
289
+ cmd, capture_output=True, text=True, cwd=Path.cwd()
290
+ )
291
+
292
+ cmd_staged = ["git", "diff", "--staged", "--no-color"]
293
+ if args:
294
+ cmd_staged.append(args)
295
+
296
+ result_staged = subprocess.run(
297
+ cmd_staged, capture_output=True, text=True, cwd=Path.cwd()
298
+ )
299
+
300
+ # Combine diffs
301
+ diff_output = ""
302
+ if result_staged.stdout:
303
+ diff_output += result_staged.stdout
304
+ if result_unstaged.stdout:
305
+ if diff_output:
306
+ diff_output += "\n"
307
+ diff_output += result_unstaged.stdout
308
+
309
+ if not diff_output:
310
+ console.print(f"\n[{Colors.MUTED}]No uncommitted changes.[/{Colors.MUTED}]\n")
311
+ return
312
+
313
+ # Render diff with line numbers and syntax highlighting
314
+ render_diff(diff_output, console)
315
+
316
+ except FileNotFoundError:
317
+ console.print(f"\n[{Colors.ERROR}]Git not found. Make sure git is installed.[/{Colors.ERROR}]\n")
318
+ except Exception as e:
319
+ console.print(f"\n[{Colors.ERROR}]Error running git diff: {e}[/{Colors.ERROR}]\n")