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,319 @@
|
|
|
1
|
+
"""Handler for /doctor command - environment diagnostics."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Minimum required Python version
|
|
16
|
+
MIN_PYTHON_VERSION = (3, 10)
|
|
17
|
+
|
|
18
|
+
# Required packages
|
|
19
|
+
REQUIRED_PACKAGES = [
|
|
20
|
+
"emdash-cli",
|
|
21
|
+
"emdash-core",
|
|
22
|
+
"click",
|
|
23
|
+
"rich",
|
|
24
|
+
"httpx",
|
|
25
|
+
"prompt_toolkit",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Servers directory for per-repo servers
|
|
29
|
+
SERVERS_DIR = Path.home() / ".emdash" / "servers"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_python_version() -> tuple[bool, str, str]:
|
|
33
|
+
"""Check Python version.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (passed, current_version, message)
|
|
37
|
+
"""
|
|
38
|
+
current = sys.version_info[:2]
|
|
39
|
+
version_str = f"{current[0]}.{current[1]}"
|
|
40
|
+
|
|
41
|
+
if current >= MIN_PYTHON_VERSION:
|
|
42
|
+
return True, version_str, f"Python {version_str} (>= {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]} required)"
|
|
43
|
+
else:
|
|
44
|
+
return False, version_str, f"Python {version_str} is below minimum {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_package_installed(package: str) -> tuple[bool, str]:
|
|
48
|
+
"""Check if a package is installed.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (installed, version_or_error)
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
from importlib.metadata import version
|
|
55
|
+
ver = version(package)
|
|
56
|
+
return True, ver
|
|
57
|
+
except Exception:
|
|
58
|
+
return False, "not installed"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_git() -> tuple[bool, str]:
|
|
62
|
+
"""Check if git is available.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (available, version_or_error)
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["git", "--version"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
check=True,
|
|
73
|
+
)
|
|
74
|
+
version = result.stdout.strip().replace("git version ", "")
|
|
75
|
+
return True, version
|
|
76
|
+
except Exception:
|
|
77
|
+
return False, "not found"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_git_repo() -> tuple[bool, str]:
|
|
81
|
+
"""Check if current directory is a git repo.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Tuple of (is_repo, message)
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
check=True,
|
|
92
|
+
)
|
|
93
|
+
repo_path = result.stdout.strip()
|
|
94
|
+
return True, repo_path
|
|
95
|
+
except Exception:
|
|
96
|
+
return False, "not a git repository"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def check_server_status() -> tuple[bool, str, list[dict]]:
|
|
100
|
+
"""Check emdash server status.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tuple of (any_running, message, server_list)
|
|
104
|
+
"""
|
|
105
|
+
import httpx
|
|
106
|
+
|
|
107
|
+
servers = []
|
|
108
|
+
|
|
109
|
+
# Check per-repo servers
|
|
110
|
+
if SERVERS_DIR.exists():
|
|
111
|
+
for port_file in SERVERS_DIR.glob("*.port"):
|
|
112
|
+
try:
|
|
113
|
+
port = int(port_file.read_text().strip())
|
|
114
|
+
hash_prefix = port_file.stem
|
|
115
|
+
|
|
116
|
+
# Get repo path
|
|
117
|
+
repo_file = SERVERS_DIR / f"{hash_prefix}.repo"
|
|
118
|
+
repo_path = repo_file.read_text().strip() if repo_file.exists() else "unknown"
|
|
119
|
+
|
|
120
|
+
# Check health
|
|
121
|
+
try:
|
|
122
|
+
response = httpx.get(f"http://localhost:{port}/api/health", timeout=2.0)
|
|
123
|
+
healthy = response.status_code == 200
|
|
124
|
+
except Exception:
|
|
125
|
+
healthy = False
|
|
126
|
+
|
|
127
|
+
servers.append({
|
|
128
|
+
"port": port,
|
|
129
|
+
"repo": repo_path,
|
|
130
|
+
"healthy": healthy,
|
|
131
|
+
})
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
if servers:
|
|
136
|
+
healthy_count = sum(1 for s in servers if s["healthy"])
|
|
137
|
+
return True, f"{healthy_count}/{len(servers)} healthy", servers
|
|
138
|
+
else:
|
|
139
|
+
return False, "no servers running", []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_api_keys() -> list[tuple[str, bool, str]]:
|
|
143
|
+
"""Check for common API keys.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of (key_name, is_set, hint)
|
|
147
|
+
"""
|
|
148
|
+
keys = [
|
|
149
|
+
("ANTHROPIC_API_KEY", "Required for Claude models"),
|
|
150
|
+
("OPENAI_API_KEY", "Required for OpenAI models"),
|
|
151
|
+
("FIREWORKS_API_KEY", "Required for Fireworks models"),
|
|
152
|
+
("GITHUB_TOKEN", "Required for GitHub integration"),
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
results = []
|
|
156
|
+
for key, hint in keys:
|
|
157
|
+
is_set = bool(os.environ.get(key))
|
|
158
|
+
results.append((key, is_set, hint))
|
|
159
|
+
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_disk_space() -> tuple[bool, str]:
|
|
164
|
+
"""Check available disk space.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Tuple of (sufficient, message)
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
usage = shutil.disk_usage(Path.home())
|
|
171
|
+
free_gb = usage.free / (1024 ** 3)
|
|
172
|
+
if free_gb < 1:
|
|
173
|
+
return False, f"{free_gb:.1f} GB free (low!)"
|
|
174
|
+
else:
|
|
175
|
+
return True, f"{free_gb:.1f} GB free"
|
|
176
|
+
except Exception:
|
|
177
|
+
return True, "unknown"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check_path_config() -> list[tuple[str, bool, str]]:
|
|
181
|
+
"""Check if common bin directories are in PATH.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of (path, in_path, description)
|
|
185
|
+
"""
|
|
186
|
+
path_env = os.environ.get("PATH", "")
|
|
187
|
+
paths_to_check = [
|
|
188
|
+
(Path.home() / ".local" / "bin", "pipx installs"),
|
|
189
|
+
(Path.home() / ".pyenv" / "shims", "pyenv"),
|
|
190
|
+
(Path("/opt/homebrew/bin"), "Homebrew (Apple Silicon)"),
|
|
191
|
+
(Path("/usr/local/bin"), "Homebrew (Intel) / system"),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
results = []
|
|
195
|
+
for path, desc in paths_to_check:
|
|
196
|
+
in_path = str(path) in path_env
|
|
197
|
+
results.append((str(path), in_path, desc))
|
|
198
|
+
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def handle_doctor(args: str) -> None:
|
|
203
|
+
"""Handle /doctor command - run environment diagnostics."""
|
|
204
|
+
console.print()
|
|
205
|
+
console.print("[bold cyan]Emdash Doctor[/bold cyan] - Environment Diagnostics")
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
issues = []
|
|
209
|
+
|
|
210
|
+
# Python Version
|
|
211
|
+
console.print("[bold]Python Environment[/bold]")
|
|
212
|
+
py_ok, py_ver, py_msg = check_python_version()
|
|
213
|
+
if py_ok:
|
|
214
|
+
console.print(f" [green]✓[/green] {py_msg}")
|
|
215
|
+
else:
|
|
216
|
+
console.print(f" [red]✗[/red] {py_msg}")
|
|
217
|
+
issues.append(("Python version", f"Upgrade to Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}+"))
|
|
218
|
+
|
|
219
|
+
console.print(f" [dim] Executable: {sys.executable}[/dim]")
|
|
220
|
+
console.print()
|
|
221
|
+
|
|
222
|
+
# Packages
|
|
223
|
+
console.print("[bold]Required Packages[/bold]")
|
|
224
|
+
for pkg in REQUIRED_PACKAGES:
|
|
225
|
+
installed, ver = check_package_installed(pkg)
|
|
226
|
+
if installed:
|
|
227
|
+
console.print(f" [green]✓[/green] {pkg} ({ver})")
|
|
228
|
+
else:
|
|
229
|
+
console.print(f" [red]✗[/red] {pkg} - {ver}")
|
|
230
|
+
issues.append((f"Package {pkg}", f"pip install {pkg}"))
|
|
231
|
+
console.print()
|
|
232
|
+
|
|
233
|
+
# Git
|
|
234
|
+
console.print("[bold]Git[/bold]")
|
|
235
|
+
git_ok, git_ver = check_git()
|
|
236
|
+
if git_ok:
|
|
237
|
+
console.print(f" [green]✓[/green] git ({git_ver})")
|
|
238
|
+
else:
|
|
239
|
+
console.print(f" [red]✗[/red] git not found")
|
|
240
|
+
issues.append(("Git", "Install git"))
|
|
241
|
+
|
|
242
|
+
repo_ok, repo_path = check_git_repo()
|
|
243
|
+
if repo_ok:
|
|
244
|
+
console.print(f" [green]✓[/green] In git repo: {repo_path}")
|
|
245
|
+
else:
|
|
246
|
+
console.print(f" [yellow]![/yellow] Not in a git repository")
|
|
247
|
+
console.print()
|
|
248
|
+
|
|
249
|
+
# Server Status
|
|
250
|
+
console.print("[bold]Emdash Server[/bold]")
|
|
251
|
+
srv_ok, srv_msg, servers = check_server_status()
|
|
252
|
+
if srv_ok:
|
|
253
|
+
console.print(f" [green]✓[/green] Servers: {srv_msg}")
|
|
254
|
+
for srv in servers:
|
|
255
|
+
status = "[green]healthy[/green]" if srv["healthy"] else "[red]unhealthy[/red]"
|
|
256
|
+
repo_short = srv["repo"].split("/")[-1] if "/" in srv["repo"] else srv["repo"]
|
|
257
|
+
console.print(f" Port {srv['port']}: {status} ({repo_short})")
|
|
258
|
+
else:
|
|
259
|
+
console.print(f" [dim]-[/dim] {srv_msg}")
|
|
260
|
+
console.print()
|
|
261
|
+
|
|
262
|
+
# API Keys
|
|
263
|
+
console.print("[bold]API Keys[/bold]")
|
|
264
|
+
api_keys = check_api_keys()
|
|
265
|
+
for key, is_set, hint in api_keys:
|
|
266
|
+
if is_set:
|
|
267
|
+
console.print(f" [green]✓[/green] {key} [dim]({hint})[/dim]")
|
|
268
|
+
else:
|
|
269
|
+
console.print(f" [dim]-[/dim] {key} not set [dim]({hint})[/dim]")
|
|
270
|
+
console.print()
|
|
271
|
+
|
|
272
|
+
# System
|
|
273
|
+
console.print("[bold]System[/bold]")
|
|
274
|
+
disk_ok, disk_msg = check_disk_space()
|
|
275
|
+
if disk_ok:
|
|
276
|
+
console.print(f" [green]✓[/green] Disk space: {disk_msg}")
|
|
277
|
+
else:
|
|
278
|
+
console.print(f" [yellow]![/yellow] Disk space: {disk_msg}")
|
|
279
|
+
issues.append(("Disk space", "Free up disk space"))
|
|
280
|
+
|
|
281
|
+
# PATH check
|
|
282
|
+
path_results = check_path_config()
|
|
283
|
+
local_bin = Path.home() / ".local" / "bin"
|
|
284
|
+
local_bin_in_path = str(local_bin) in os.environ.get("PATH", "")
|
|
285
|
+
if local_bin_in_path:
|
|
286
|
+
console.print(f" [green]✓[/green] ~/.local/bin in PATH (pipx)")
|
|
287
|
+
else:
|
|
288
|
+
console.print(f" [red]✗[/red] ~/.local/bin not in PATH")
|
|
289
|
+
issues.append(("PATH config", "Run: pipx ensurepath && source ~/.zshrc"))
|
|
290
|
+
console.print()
|
|
291
|
+
|
|
292
|
+
# Summary
|
|
293
|
+
if issues:
|
|
294
|
+
console.print("[bold red]Issues Found:[/bold red]")
|
|
295
|
+
console.print()
|
|
296
|
+
for issue, fix in issues:
|
|
297
|
+
console.print(f" [red]•[/red] {issue}")
|
|
298
|
+
console.print(f" [dim]Fix: {fix}[/dim]")
|
|
299
|
+
console.print()
|
|
300
|
+
|
|
301
|
+
# Python upgrade instructions if needed
|
|
302
|
+
if not py_ok:
|
|
303
|
+
console.print("[bold]To upgrade Python:[/bold]")
|
|
304
|
+
console.print(" [cyan]macOS:[/cyan] brew install python@3.12")
|
|
305
|
+
console.print(" [cyan]Ubuntu:[/cyan] sudo apt install python3.12")
|
|
306
|
+
console.print(" [cyan]Windows:[/cyan] winget install Python.Python.3.12")
|
|
307
|
+
console.print(" [cyan]pyenv:[/cyan] pyenv install 3.12 && pyenv global 3.12")
|
|
308
|
+
console.print()
|
|
309
|
+
|
|
310
|
+
# PATH fix instructions if needed
|
|
311
|
+
if not local_bin_in_path:
|
|
312
|
+
console.print("[bold]To fix PATH (for pipx/em command):[/bold]")
|
|
313
|
+
console.print(" [cyan]Option 1:[/cyan] pipx ensurepath")
|
|
314
|
+
console.print(" [cyan]Option 2:[/cyan] echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.zshrc")
|
|
315
|
+
console.print(" [dim]Then restart your terminal or run: source ~/.zshrc[/dim]")
|
|
316
|
+
console.print()
|
|
317
|
+
else:
|
|
318
|
+
console.print("[bold green]All checks passed![/bold green]")
|
|
319
|
+
console.print()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Handler for /hooks command."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_hooks(args: str) -> None:
|
|
12
|
+
"""Handle /hooks command.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
args: Command arguments
|
|
16
|
+
"""
|
|
17
|
+
from emdash_core.agent.hooks import HookManager, HookConfig, HookEventType
|
|
18
|
+
|
|
19
|
+
manager = HookManager(Path.cwd())
|
|
20
|
+
|
|
21
|
+
# Parse subcommand
|
|
22
|
+
subparts = args.split(maxsplit=1) if args else []
|
|
23
|
+
subcommand = subparts[0].lower() if subparts else "list"
|
|
24
|
+
subargs = subparts[1].strip() if len(subparts) > 1 else ""
|
|
25
|
+
|
|
26
|
+
if subcommand == "list" or subcommand == "":
|
|
27
|
+
# List all hooks
|
|
28
|
+
hooks = manager.get_hooks()
|
|
29
|
+
if hooks:
|
|
30
|
+
console.print("\n[bold cyan]Configured Hooks[/bold cyan]\n")
|
|
31
|
+
for hook in hooks:
|
|
32
|
+
status = "[green]enabled[/green]" if hook.enabled else "[dim]disabled[/dim]"
|
|
33
|
+
console.print(f" [cyan]{hook.id}[/cyan] ({status})")
|
|
34
|
+
console.print(f" Event: [yellow]{hook.event.value}[/yellow]")
|
|
35
|
+
console.print(f" Command: [dim]{hook.command}[/dim]")
|
|
36
|
+
console.print()
|
|
37
|
+
console.print(f"[dim]Config file: {manager.hooks_file_path}[/dim]\n")
|
|
38
|
+
else:
|
|
39
|
+
console.print("\n[dim]No hooks configured.[/dim]")
|
|
40
|
+
console.print("[dim]Add with: /hooks add <event> <command>[/dim]")
|
|
41
|
+
console.print(f"[dim]Events: {', '.join(e.value for e in HookEventType)}[/dim]\n")
|
|
42
|
+
|
|
43
|
+
elif subcommand == "add":
|
|
44
|
+
# Add a new hook: /hooks add <event> <command>
|
|
45
|
+
if not subargs:
|
|
46
|
+
console.print("[yellow]Usage: /hooks add <event> <command>[/yellow]")
|
|
47
|
+
console.print(f"[dim]Events: {', '.join(e.value for e in HookEventType)}[/dim]")
|
|
48
|
+
console.print("[dim]Example: /hooks add session_end notify-send 'Agent done'[/dim]")
|
|
49
|
+
else:
|
|
50
|
+
# Parse event and command
|
|
51
|
+
add_parts = subargs.split(maxsplit=1)
|
|
52
|
+
if len(add_parts) < 2:
|
|
53
|
+
console.print("[yellow]Usage: /hooks add <event> <command>[/yellow]")
|
|
54
|
+
else:
|
|
55
|
+
event_str = add_parts[0].lower()
|
|
56
|
+
hook_command = add_parts[1]
|
|
57
|
+
|
|
58
|
+
# Validate event type
|
|
59
|
+
try:
|
|
60
|
+
event_type = HookEventType(event_str)
|
|
61
|
+
except ValueError:
|
|
62
|
+
console.print(f"[red]Invalid event: {event_str}[/red]")
|
|
63
|
+
console.print(f"[dim]Valid events: {', '.join(e.value for e in HookEventType)}[/dim]")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# Generate a unique ID
|
|
67
|
+
hook_id = f"hook-{hashlib.md5(f'{event_str}{hook_command}'.encode()).hexdigest()[:8]}"
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
hook = HookConfig(
|
|
71
|
+
id=hook_id,
|
|
72
|
+
event=event_type,
|
|
73
|
+
command=hook_command,
|
|
74
|
+
enabled=True,
|
|
75
|
+
)
|
|
76
|
+
manager.add_hook(hook)
|
|
77
|
+
console.print(f"[green]Added hook '{hook_id}'[/green]")
|
|
78
|
+
console.print(f"[dim]Event: {event_type.value}, Command: {hook_command}[/dim]")
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
81
|
+
|
|
82
|
+
elif subcommand == "remove":
|
|
83
|
+
if not subargs:
|
|
84
|
+
console.print("[yellow]Usage: /hooks remove <hook-id>[/yellow]")
|
|
85
|
+
else:
|
|
86
|
+
if manager.remove_hook(subargs):
|
|
87
|
+
console.print(f"[green]Removed hook '{subargs}'[/green]")
|
|
88
|
+
else:
|
|
89
|
+
console.print(f"[yellow]Hook '{subargs}' not found[/yellow]")
|
|
90
|
+
|
|
91
|
+
elif subcommand == "toggle":
|
|
92
|
+
if not subargs:
|
|
93
|
+
console.print("[yellow]Usage: /hooks toggle <hook-id>[/yellow]")
|
|
94
|
+
else:
|
|
95
|
+
new_state = manager.toggle_hook(subargs)
|
|
96
|
+
if new_state is not None:
|
|
97
|
+
state_str = "[green]enabled[/green]" if new_state else "[dim]disabled[/dim]"
|
|
98
|
+
console.print(f"Hook '{subargs}' is now {state_str}")
|
|
99
|
+
else:
|
|
100
|
+
console.print(f"[yellow]Hook '{subargs}' not found[/yellow]")
|
|
101
|
+
|
|
102
|
+
elif subcommand == "events":
|
|
103
|
+
# List available event types
|
|
104
|
+
console.print("\n[bold cyan]Available Hook Events[/bold cyan]\n")
|
|
105
|
+
event_descriptions = {
|
|
106
|
+
HookEventType.TOOL_START: "Triggered before a tool executes",
|
|
107
|
+
HookEventType.TOOL_RESULT: "Triggered after a tool completes",
|
|
108
|
+
HookEventType.SESSION_START: "Triggered when agent session starts",
|
|
109
|
+
HookEventType.SESSION_END: "Triggered when agent session ends",
|
|
110
|
+
HookEventType.RESPONSE: "Triggered when agent produces a response",
|
|
111
|
+
HookEventType.ERROR: "Triggered when an error occurs",
|
|
112
|
+
}
|
|
113
|
+
for event_type in HookEventType:
|
|
114
|
+
desc = event_descriptions.get(event_type, "")
|
|
115
|
+
console.print(f" [cyan]{event_type.value}[/cyan]")
|
|
116
|
+
console.print(f" [dim]{desc}[/dim]")
|
|
117
|
+
console.print()
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
|
|
121
|
+
console.print("[dim]Usage: /hooks [list|add|remove|toggle|events][/dim]")
|
|
@@ -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]")
|