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.
- emdash_cli/client.py +12 -28
- emdash_cli/commands/__init__.py +2 -2
- emdash_cli/commands/agent/constants.py +10 -0
- emdash_cli/commands/agent/handlers/__init__.py +10 -0
- emdash_cli/commands/agent/handlers/agents.py +67 -39
- emdash_cli/commands/agent/handlers/index.py +183 -0
- emdash_cli/commands/agent/handlers/misc.py +119 -0
- emdash_cli/commands/agent/handlers/registry.py +72 -0
- emdash_cli/commands/agent/handlers/rules.py +48 -31
- emdash_cli/commands/agent/handlers/sessions.py +1 -1
- emdash_cli/commands/agent/handlers/setup.py +187 -54
- emdash_cli/commands/agent/handlers/skills.py +42 -4
- emdash_cli/commands/agent/handlers/telegram.py +475 -0
- emdash_cli/commands/agent/handlers/todos.py +55 -34
- emdash_cli/commands/agent/handlers/verify.py +10 -5
- emdash_cli/commands/agent/help.py +236 -0
- emdash_cli/commands/agent/interactive.py +222 -37
- emdash_cli/commands/agent/menus.py +116 -84
- emdash_cli/commands/agent/onboarding.py +619 -0
- emdash_cli/commands/agent/session_restore.py +210 -0
- emdash_cli/commands/index.py +111 -13
- emdash_cli/commands/registry.py +635 -0
- 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/sse_renderer.py +632 -171
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -2
- emdash_cli-0.1.67.dist-info/RECORD +63 -0
- emdash_cli/commands/swarm.py +0 -86
- emdash_cli-0.1.46.dist-info/RECORD +0 -49
- {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
- {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:
|
emdash_cli/commands/__init__.py
CHANGED
|
@@ -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(
|
|
56
|
-
console.print(f"[
|
|
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("[
|
|
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"[
|
|
84
|
+
console.print(f" [{Colors.DIM}]type[/{Colors.DIM}] [{Colors.MUTED}]built-in[/{Colors.MUTED}]")
|
|
71
85
|
if name == "Explore":
|
|
72
|
-
console.print("[
|
|
73
|
-
console.print("[
|
|
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("[
|
|
76
|
-
console.print("[
|
|
77
|
-
console.print(
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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"[
|
|
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(
|
|
108
|
+
console.print()
|
|
109
|
+
console.print(f" [{Colors.DIM}]mcp servers:[/{Colors.DIM}]")
|
|
98
110
|
for server in agent.mcp_servers:
|
|
99
|
-
status = "[
|
|
100
|
-
console.print(f"
|
|
101
|
-
console.print(f"
|
|
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(
|
|
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(
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
134
|
+
for line in preview.split('\n')[:6]:
|
|
135
|
+
console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
|
|
114
136
|
else:
|
|
115
|
-
console.print(f"[
|
|
137
|
+
console.print(f" [{Colors.WARNING}]Agent '{name}' not found[/{Colors.WARNING}]")
|
|
138
|
+
|
|
116
139
|
console.print()
|
|
117
|
-
console.print("[
|
|
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"[
|
|
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"[
|
|
188
|
-
console.print(
|
|
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": "
|
|
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("[
|
|
269
|
-
console.print(
|
|
270
|
-
console.print("[
|
|
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": "
|
|
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("[
|
|
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")
|