emdash-cli 0.1.35__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.
- emdash_cli/client.py +41 -22
- emdash_cli/clipboard.py +30 -61
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/__init__.py +14 -0
- emdash_cli/commands/agent/cli.py +100 -0
- emdash_cli/commands/agent/constants.py +63 -0
- emdash_cli/commands/agent/file_utils.py +178 -0
- emdash_cli/commands/agent/handlers/__init__.py +51 -0
- emdash_cli/commands/agent/handlers/agents.py +449 -0
- emdash_cli/commands/agent/handlers/auth.py +69 -0
- emdash_cli/commands/agent/handlers/doctor.py +319 -0
- emdash_cli/commands/agent/handlers/hooks.py +121 -0
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/mcp.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +319 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +411 -0
- emdash_cli/commands/agent/handlers/sessions.py +168 -0
- emdash_cli/commands/agent/handlers/setup.py +715 -0
- emdash_cli/commands/agent/handlers/skills.py +478 -0
- emdash_cli/commands/agent/handlers/telegram.py +475 -0
- emdash_cli/commands/agent/handlers/todos.py +119 -0
- emdash_cli/commands/agent/handlers/verify.py +653 -0
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +842 -0
- emdash_cli/commands/agent/menus.py +760 -0
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/agent.py +7 -1321
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- emdash_cli/commands/server.py +99 -40
- emdash_cli/commands/skills.py +72 -6
- emdash_cli/design.py +328 -0
- emdash_cli/diff_renderer.py +438 -0
- emdash_cli/integrations/__init__.py +1 -0
- emdash_cli/integrations/telegram/__init__.py +15 -0
- emdash_cli/integrations/telegram/bot.py +402 -0
- emdash_cli/integrations/telegram/bridge.py +865 -0
- emdash_cli/integrations/telegram/config.py +155 -0
- emdash_cli/integrations/telegram/formatter.py +385 -0
- emdash_cli/main.py +52 -2
- emdash_cli/server_manager.py +70 -10
- emdash_cli/sse_renderer.py +659 -167
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
- emdash_cli-0.1.67.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.35.dist-info/RECORD +0 -30
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
- {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Handler for /mcp command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_mcp(args: str) -> None:
|
|
14
|
+
"""Handle /mcp command.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
args: Command arguments
|
|
18
|
+
"""
|
|
19
|
+
from emdash_core.agent.mcp.manager import get_mcp_manager
|
|
20
|
+
from emdash_core.agent.mcp.config import get_default_mcp_config_path
|
|
21
|
+
|
|
22
|
+
manager = get_mcp_manager(config_path=get_default_mcp_config_path(Path.cwd()))
|
|
23
|
+
|
|
24
|
+
# Parse subcommand
|
|
25
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
26
|
+
subcommand = subparts[0].lower() if subparts else ""
|
|
27
|
+
|
|
28
|
+
def _show_mcp_interactive():
|
|
29
|
+
"""Show interactive MCP server list with toggle."""
|
|
30
|
+
from prompt_toolkit import Application
|
|
31
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
32
|
+
from prompt_toolkit.layout import Layout, Window, FormattedTextControl
|
|
33
|
+
from prompt_toolkit.styles import Style
|
|
34
|
+
|
|
35
|
+
servers = manager.list_servers()
|
|
36
|
+
if not servers:
|
|
37
|
+
console.print("\n[dim]No global MCP servers configured.[/dim]")
|
|
38
|
+
console.print(f"[dim]Edit {manager.config_path} to add servers[/dim]\n")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
selected_index = [0]
|
|
42
|
+
server_names = [s["name"] for s in servers]
|
|
43
|
+
|
|
44
|
+
kb = KeyBindings()
|
|
45
|
+
|
|
46
|
+
@kb.add("up")
|
|
47
|
+
@kb.add("k")
|
|
48
|
+
def move_up(event):
|
|
49
|
+
selected_index[0] = (selected_index[0] - 1) % len(servers)
|
|
50
|
+
|
|
51
|
+
@kb.add("down")
|
|
52
|
+
@kb.add("j")
|
|
53
|
+
def move_down(event):
|
|
54
|
+
selected_index[0] = (selected_index[0] + 1) % len(servers)
|
|
55
|
+
|
|
56
|
+
@kb.add("enter")
|
|
57
|
+
@kb.add("space")
|
|
58
|
+
def toggle_server(event):
|
|
59
|
+
# Toggle the selected server's enabled status
|
|
60
|
+
server_name = server_names[selected_index[0]]
|
|
61
|
+
config_path = manager.config_path
|
|
62
|
+
|
|
63
|
+
# Read current config
|
|
64
|
+
if config_path.exists():
|
|
65
|
+
with open(config_path) as f:
|
|
66
|
+
config = json.load(f)
|
|
67
|
+
else:
|
|
68
|
+
config = {"mcpServers": {}}
|
|
69
|
+
|
|
70
|
+
# Toggle enabled status
|
|
71
|
+
if server_name in config.get("mcpServers", {}):
|
|
72
|
+
current = config["mcpServers"][server_name].get("enabled", True)
|
|
73
|
+
config["mcpServers"][server_name]["enabled"] = not current
|
|
74
|
+
|
|
75
|
+
# Save config
|
|
76
|
+
with open(config_path, "w") as f:
|
|
77
|
+
json.dump(config, f, indent=2)
|
|
78
|
+
|
|
79
|
+
# Reload manager
|
|
80
|
+
manager.reload_config()
|
|
81
|
+
|
|
82
|
+
# Update local servers list
|
|
83
|
+
servers[:] = manager.list_servers()
|
|
84
|
+
|
|
85
|
+
@kb.add("e")
|
|
86
|
+
def edit_config(event):
|
|
87
|
+
event.app.exit(result="edit")
|
|
88
|
+
|
|
89
|
+
@kb.add("q")
|
|
90
|
+
@kb.add("escape")
|
|
91
|
+
def quit_menu(event):
|
|
92
|
+
event.app.exit()
|
|
93
|
+
|
|
94
|
+
def get_formatted_content():
|
|
95
|
+
lines = []
|
|
96
|
+
lines.append(("class:title", "Global MCP Servers\n"))
|
|
97
|
+
lines.append(("class:dim", f"Config: {manager.config_path}\n\n"))
|
|
98
|
+
|
|
99
|
+
for i, server in enumerate(servers):
|
|
100
|
+
if server["enabled"]:
|
|
101
|
+
if server["running"]:
|
|
102
|
+
status = "running"
|
|
103
|
+
status_style = "class:running"
|
|
104
|
+
else:
|
|
105
|
+
status = "enabled"
|
|
106
|
+
status_style = "class:enabled"
|
|
107
|
+
else:
|
|
108
|
+
status = "disabled"
|
|
109
|
+
status_style = "class:disabled"
|
|
110
|
+
|
|
111
|
+
if i == selected_index[0]:
|
|
112
|
+
lines.append(("class:selected", f" > {server['name']}"))
|
|
113
|
+
else:
|
|
114
|
+
lines.append(("class:normal", f" {server['name']}"))
|
|
115
|
+
|
|
116
|
+
lines.append((status_style, f" ({status})\n"))
|
|
117
|
+
|
|
118
|
+
lines.append(("class:hint", "\n↑/↓ navigate • Enter toggle • e edit • q quit"))
|
|
119
|
+
return lines
|
|
120
|
+
|
|
121
|
+
style = Style.from_dict({
|
|
122
|
+
"title": "#00cc66 bold",
|
|
123
|
+
"dim": "#888888",
|
|
124
|
+
"selected": "#00cc66 bold",
|
|
125
|
+
"normal": "#cccccc",
|
|
126
|
+
"running": "#00cc66",
|
|
127
|
+
"enabled": "#cccc00",
|
|
128
|
+
"disabled": "#888888",
|
|
129
|
+
"hint": "#888888 italic",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
layout = Layout(Window(FormattedTextControl(get_formatted_content)))
|
|
133
|
+
app = Application(layout=layout, key_bindings=kb, style=style, full_screen=False)
|
|
134
|
+
|
|
135
|
+
result = app.run()
|
|
136
|
+
if result == "edit":
|
|
137
|
+
return "edit"
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
if subcommand == "" or subcommand == "list":
|
|
141
|
+
# Show interactive menu (default)
|
|
142
|
+
result = _show_mcp_interactive()
|
|
143
|
+
if result == "edit":
|
|
144
|
+
subcommand = "edit"
|
|
145
|
+
else:
|
|
146
|
+
# Don't continue to edit
|
|
147
|
+
subcommand = "done"
|
|
148
|
+
|
|
149
|
+
if subcommand == "edit":
|
|
150
|
+
# Open MCP config in editor
|
|
151
|
+
config_path = manager.config_path
|
|
152
|
+
|
|
153
|
+
# Create default config if it doesn't exist
|
|
154
|
+
if not config_path.exists():
|
|
155
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
config_path.write_text('{\n "mcpServers": {}\n}\n')
|
|
157
|
+
console.print(f"[dim]Created {config_path}[/dim]")
|
|
158
|
+
|
|
159
|
+
editor = os.environ.get("EDITOR", "")
|
|
160
|
+
if not editor:
|
|
161
|
+
for ed in ["code", "vim", "nano", "vi"]:
|
|
162
|
+
try:
|
|
163
|
+
subprocess.run(["which", ed], capture_output=True, check=True)
|
|
164
|
+
editor = ed
|
|
165
|
+
break
|
|
166
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if editor:
|
|
170
|
+
console.print(f"[dim]Opening {config_path} in {editor}...[/dim]")
|
|
171
|
+
try:
|
|
172
|
+
subprocess.run([editor, str(config_path)])
|
|
173
|
+
manager.reload_config()
|
|
174
|
+
console.print("[dim]Config reloaded[/dim]")
|
|
175
|
+
except Exception as e:
|
|
176
|
+
console.print(f"[red]Failed to open editor: {e}[/red]")
|
|
177
|
+
else:
|
|
178
|
+
console.print(f"[yellow]No editor found. Edit manually:[/yellow]")
|
|
179
|
+
console.print(f" {config_path}")
|
|
180
|
+
|
|
181
|
+
elif subcommand != "done":
|
|
182
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
183
|
+
console.print("[dim]Usage: /mcp [list|edit][/dim]")
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Handlers for miscellaneous slash commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
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
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_status(client) -> None:
|
|
20
|
+
"""Handle /status command.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
client: EmdashClient instance
|
|
24
|
+
"""
|
|
25
|
+
console.print("\n[bold cyan]Status[/bold cyan]\n")
|
|
26
|
+
|
|
27
|
+
# Index status
|
|
28
|
+
console.print("[bold]Index Status[/bold]")
|
|
29
|
+
try:
|
|
30
|
+
status = client.index_status(str(Path.cwd()))
|
|
31
|
+
is_indexed = status.get("is_indexed", False)
|
|
32
|
+
console.print(f" Indexed: {'[green]Yes[/green]' if is_indexed else '[yellow]No[/yellow]'}")
|
|
33
|
+
|
|
34
|
+
if is_indexed:
|
|
35
|
+
console.print(f" Files: {status.get('file_count', 0)}")
|
|
36
|
+
console.print(f" Functions: {status.get('function_count', 0)}")
|
|
37
|
+
console.print(f" Classes: {status.get('class_count', 0)}")
|
|
38
|
+
console.print(f" Communities: {status.get('community_count', 0)}")
|
|
39
|
+
if status.get("last_indexed"):
|
|
40
|
+
console.print(f" Last indexed: {status.get('last_indexed')}")
|
|
41
|
+
if status.get("last_commit"):
|
|
42
|
+
console.print(f" Last commit: {status.get('last_commit')}")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
console.print(f" [red]Error fetching index status: {e}[/red]")
|
|
45
|
+
|
|
46
|
+
console.print()
|
|
47
|
+
|
|
48
|
+
# PROJECT.md status
|
|
49
|
+
console.print("[bold]PROJECT.md Status[/bold]")
|
|
50
|
+
projectmd_path = Path.cwd() / "PROJECT.md"
|
|
51
|
+
if projectmd_path.exists():
|
|
52
|
+
stat = projectmd_path.stat()
|
|
53
|
+
modified_time = datetime.fromtimestamp(stat.st_mtime)
|
|
54
|
+
size_kb = stat.st_size / 1024
|
|
55
|
+
console.print(f" Exists: [green]Yes[/green]")
|
|
56
|
+
console.print(f" Path: {projectmd_path}")
|
|
57
|
+
console.print(f" Size: {size_kb:.1f} KB")
|
|
58
|
+
console.print(f" Last modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
59
|
+
else:
|
|
60
|
+
console.print(f" Exists: [yellow]No[/yellow]")
|
|
61
|
+
console.print("[dim] Run /projectmd to generate it[/dim]")
|
|
62
|
+
|
|
63
|
+
console.print()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handle_pr(args: str, run_slash_command_task, client, renderer, model, max_iterations) -> None:
|
|
67
|
+
"""Handle /pr command.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
args: PR URL or number
|
|
71
|
+
run_slash_command_task: Function to run slash command tasks
|
|
72
|
+
client: EmdashClient instance
|
|
73
|
+
renderer: SSERenderer instance
|
|
74
|
+
model: Current model
|
|
75
|
+
max_iterations: Max iterations
|
|
76
|
+
"""
|
|
77
|
+
if not args:
|
|
78
|
+
console.print("[yellow]Usage: /pr <pr-url-or-number>[/yellow]")
|
|
79
|
+
console.print("[dim]Example: /pr 123 or /pr https://github.com/org/repo/pull/123[/dim]")
|
|
80
|
+
else:
|
|
81
|
+
console.print(f"[cyan]Reviewing PR: {args}[/cyan]")
|
|
82
|
+
run_slash_command_task(
|
|
83
|
+
client, renderer, model, max_iterations,
|
|
84
|
+
f"Review this pull request and provide feedback: {args}",
|
|
85
|
+
{"mode": "code"}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def handle_projectmd(run_slash_command_task, client, renderer, model, max_iterations) -> None:
|
|
90
|
+
"""Handle /projectmd command.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
run_slash_command_task: Function to run slash command tasks
|
|
94
|
+
client: EmdashClient instance
|
|
95
|
+
renderer: SSERenderer instance
|
|
96
|
+
model: Current model
|
|
97
|
+
max_iterations: Max iterations
|
|
98
|
+
"""
|
|
99
|
+
console.print("[cyan]Generating PROJECT.md...[/cyan]")
|
|
100
|
+
run_slash_command_task(
|
|
101
|
+
client, renderer, model, max_iterations,
|
|
102
|
+
"Analyze this codebase and generate a comprehensive PROJECT.md file that describes the architecture, main components, how to get started, and key design decisions.",
|
|
103
|
+
{"mode": "code"}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def handle_research(args: str, run_slash_command_task, client, renderer, model) -> None:
|
|
108
|
+
"""Handle /research command.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
args: Research goal
|
|
112
|
+
run_slash_command_task: Function to run slash command tasks
|
|
113
|
+
client: EmdashClient instance
|
|
114
|
+
renderer: SSERenderer instance
|
|
115
|
+
model: Current model
|
|
116
|
+
"""
|
|
117
|
+
if not args:
|
|
118
|
+
console.print("[yellow]Usage: /research <goal>[/yellow]")
|
|
119
|
+
console.print("[dim]Example: /research How does authentication work in this codebase?[/dim]")
|
|
120
|
+
else:
|
|
121
|
+
console.print(f"[cyan]Researching: {args}[/cyan]")
|
|
122
|
+
run_slash_command_task(
|
|
123
|
+
client, renderer, model, 50, # More iterations for research
|
|
124
|
+
f"Conduct deep research on: {args}\n\nExplore the codebase thoroughly, analyze relevant code, and provide a comprehensive answer with references to specific files and functions.",
|
|
125
|
+
{"mode": "plan"} # Use plan mode for research
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def handle_context(renderer) -> None:
|
|
130
|
+
"""Handle /context command.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
renderer: SSERenderer instance with _last_context_frame attribute
|
|
134
|
+
"""
|
|
135
|
+
context_data = getattr(renderer, '_last_context_frame', None)
|
|
136
|
+
if not context_data:
|
|
137
|
+
console.print("\n[dim]No context frame available yet. Run a query first.[/dim]\n")
|
|
138
|
+
else:
|
|
139
|
+
adding = context_data.get("adding") or {}
|
|
140
|
+
reading = context_data.get("reading") or {}
|
|
141
|
+
|
|
142
|
+
# Get stats
|
|
143
|
+
step_count = adding.get("step_count", 0)
|
|
144
|
+
entities_found = adding.get("entities_found", 0)
|
|
145
|
+
context_tokens = adding.get("context_tokens", 0)
|
|
146
|
+
context_breakdown = adding.get("context_breakdown", {})
|
|
147
|
+
|
|
148
|
+
console.print()
|
|
149
|
+
console.print("[bold cyan]Context Frame[/bold cyan]")
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
# Show total context
|
|
153
|
+
if context_tokens > 0:
|
|
154
|
+
console.print(f"[bold]Total:[/bold] {context_tokens:,} tokens")
|
|
155
|
+
|
|
156
|
+
# Show breakdown
|
|
157
|
+
if context_breakdown:
|
|
158
|
+
console.print(f"\n[bold]Breakdown:[/bold]")
|
|
159
|
+
for key, tokens in context_breakdown.items():
|
|
160
|
+
if tokens > 0:
|
|
161
|
+
console.print(f" {key}: {tokens:,}")
|
|
162
|
+
|
|
163
|
+
# Show stats
|
|
164
|
+
if step_count > 0 or entities_found > 0:
|
|
165
|
+
console.print(f"\n[bold]Stats:[/bold]")
|
|
166
|
+
if step_count > 0:
|
|
167
|
+
console.print(f" Steps: {step_count}")
|
|
168
|
+
if entities_found > 0:
|
|
169
|
+
console.print(f" Entities: {entities_found}")
|
|
170
|
+
|
|
171
|
+
# Show reranking query
|
|
172
|
+
query = reading.get("query")
|
|
173
|
+
if query:
|
|
174
|
+
console.print(f"\n[bold]Reranking Query:[/bold]")
|
|
175
|
+
console.print(f" [yellow]{query}[/yellow]")
|
|
176
|
+
|
|
177
|
+
# Show reranked items
|
|
178
|
+
items = reading.get("items", [])
|
|
179
|
+
if items:
|
|
180
|
+
console.print(f"\n[bold]Reranked Items ({len(items)}):[/bold]")
|
|
181
|
+
for i, item in enumerate(items, 1):
|
|
182
|
+
name = item.get("name", "?")
|
|
183
|
+
item_type = item.get("type", "?")
|
|
184
|
+
score = item.get("score")
|
|
185
|
+
file_path = item.get("file", "")
|
|
186
|
+
description = item.get("description", "")
|
|
187
|
+
touch_count = item.get("touch_count", 0)
|
|
188
|
+
neighbors = item.get("neighbors", [])
|
|
189
|
+
|
|
190
|
+
score_str = f"[cyan]{score:.3f}[/cyan]" if score is not None else "[dim]n/a[/dim]"
|
|
191
|
+
touch_str = f"[magenta]×{touch_count}[/magenta]" if touch_count > 1 else ""
|
|
192
|
+
|
|
193
|
+
console.print(f"\n [bold white]{i}.[/bold white] [dim]{item_type}[/dim] [bold]{name}[/bold]")
|
|
194
|
+
console.print(f" Score: {score_str} {touch_str}")
|
|
195
|
+
if file_path:
|
|
196
|
+
console.print(f" File: [dim]{file_path}[/dim]")
|
|
197
|
+
if description:
|
|
198
|
+
desc_preview = description[:100] + "..." if len(description) > 100 else description
|
|
199
|
+
console.print(f" Desc: [dim]{desc_preview}[/dim]")
|
|
200
|
+
if neighbors:
|
|
201
|
+
console.print(f" Neighbors: [dim]{', '.join(neighbors)}[/dim]")
|
|
202
|
+
else:
|
|
203
|
+
debug_info = reading.get("debug", "")
|
|
204
|
+
if debug_info:
|
|
205
|
+
console.print(f"\n[dim]No reranked items: {debug_info}[/dim]")
|
|
206
|
+
else:
|
|
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)
|
|
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")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Handler for /registry command."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_registry(args: str) -> None:
|
|
9
|
+
"""Handle /registry command.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
args: Command arguments (list, show, install, search)
|
|
13
|
+
"""
|
|
14
|
+
from emdash_cli.commands.registry import (
|
|
15
|
+
_show_registry_wizard,
|
|
16
|
+
_fetch_registry,
|
|
17
|
+
registry_list,
|
|
18
|
+
registry_show,
|
|
19
|
+
registry_install,
|
|
20
|
+
registry_search,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Parse subcommand
|
|
24
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
25
|
+
subcommand = subparts[0].lower() if subparts else ""
|
|
26
|
+
subargs = subparts[1] if len(subparts) > 1 else ""
|
|
27
|
+
|
|
28
|
+
if subcommand == "" or subcommand == "wizard":
|
|
29
|
+
# Show interactive wizard (default)
|
|
30
|
+
_show_registry_wizard()
|
|
31
|
+
|
|
32
|
+
elif subcommand == "list":
|
|
33
|
+
# List components
|
|
34
|
+
component_type = subargs if subargs else None
|
|
35
|
+
if component_type and component_type not in ["skills", "rules", "agents", "verifiers"]:
|
|
36
|
+
console.print(f"[yellow]Unknown type: {component_type}[/yellow]")
|
|
37
|
+
console.print("[dim]Types: skills, rules, agents, verifiers[/dim]")
|
|
38
|
+
return
|
|
39
|
+
# Invoke click command
|
|
40
|
+
registry_list.callback(component_type)
|
|
41
|
+
|
|
42
|
+
elif subcommand == "show":
|
|
43
|
+
if not subargs:
|
|
44
|
+
console.print("[yellow]Usage: /registry show type:name[/yellow]")
|
|
45
|
+
console.print("[dim]Example: /registry show skill:frontend-design[/dim]")
|
|
46
|
+
return
|
|
47
|
+
registry_show.callback(subargs)
|
|
48
|
+
|
|
49
|
+
elif subcommand == "install":
|
|
50
|
+
if not subargs:
|
|
51
|
+
console.print("[yellow]Usage: /registry install type:name [type:name ...][/yellow]")
|
|
52
|
+
console.print("[dim]Example: /registry install skill:frontend-design rule:typescript[/dim]")
|
|
53
|
+
return
|
|
54
|
+
component_ids = tuple(subargs.split())
|
|
55
|
+
registry_install.callback(component_ids)
|
|
56
|
+
|
|
57
|
+
elif subcommand == "search":
|
|
58
|
+
if not subargs:
|
|
59
|
+
console.print("[yellow]Usage: /registry search query[/yellow]")
|
|
60
|
+
console.print("[dim]Example: /registry search frontend[/dim]")
|
|
61
|
+
return
|
|
62
|
+
# Simple search without tag filtering from slash command
|
|
63
|
+
registry_search.callback(subargs, ())
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
67
|
+
console.print("[dim]Usage: /registry [list|show|install|search][/dim]")
|
|
68
|
+
console.print("[dim] /registry - Interactive wizard[/dim]")
|
|
69
|
+
console.print("[dim] /registry list - List all components[/dim]")
|
|
70
|
+
console.print("[dim] /registry show x:y - Show component details[/dim]")
|
|
71
|
+
console.print("[dim] /registry install x:y - Install components[/dim]")
|
|
72
|
+
console.print("[dim] /registry search q - Search registry[/dim]")
|