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.
Files changed (50) hide show
  1. emdash_cli/client.py +41 -22
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/__init__.py +2 -2
  4. emdash_cli/commands/agent/__init__.py +14 -0
  5. emdash_cli/commands/agent/cli.py +100 -0
  6. emdash_cli/commands/agent/constants.py +63 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +51 -0
  9. emdash_cli/commands/agent/handlers/agents.py +449 -0
  10. emdash_cli/commands/agent/handlers/auth.py +69 -0
  11. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  12. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  13. emdash_cli/commands/agent/handlers/index.py +183 -0
  14. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  15. emdash_cli/commands/agent/handlers/misc.py +319 -0
  16. emdash_cli/commands/agent/handlers/registry.py +72 -0
  17. emdash_cli/commands/agent/handlers/rules.py +411 -0
  18. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  19. emdash_cli/commands/agent/handlers/setup.py +715 -0
  20. emdash_cli/commands/agent/handlers/skills.py +478 -0
  21. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  22. emdash_cli/commands/agent/handlers/todos.py +119 -0
  23. emdash_cli/commands/agent/handlers/verify.py +653 -0
  24. emdash_cli/commands/agent/help.py +236 -0
  25. emdash_cli/commands/agent/interactive.py +842 -0
  26. emdash_cli/commands/agent/menus.py +760 -0
  27. emdash_cli/commands/agent/onboarding.py +619 -0
  28. emdash_cli/commands/agent/session_restore.py +210 -0
  29. emdash_cli/commands/agent.py +7 -1321
  30. emdash_cli/commands/index.py +111 -13
  31. emdash_cli/commands/registry.py +635 -0
  32. emdash_cli/commands/server.py +99 -40
  33. emdash_cli/commands/skills.py +72 -6
  34. emdash_cli/design.py +328 -0
  35. emdash_cli/diff_renderer.py +438 -0
  36. emdash_cli/integrations/__init__.py +1 -0
  37. emdash_cli/integrations/telegram/__init__.py +15 -0
  38. emdash_cli/integrations/telegram/bot.py +402 -0
  39. emdash_cli/integrations/telegram/bridge.py +865 -0
  40. emdash_cli/integrations/telegram/config.py +155 -0
  41. emdash_cli/integrations/telegram/formatter.py +385 -0
  42. emdash_cli/main.py +52 -2
  43. emdash_cli/server_manager.py +70 -10
  44. emdash_cli/sse_renderer.py +659 -167
  45. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -4
  46. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  47. emdash_cli/commands/swarm.py +0 -86
  48. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  49. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  50. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  """Server lifecycle management for emdash-core."""
2
2
 
3
3
  import atexit
4
+ import hashlib
4
5
  import os
5
6
  import signal
6
7
  import socket
@@ -17,15 +18,15 @@ class ServerManager:
17
18
  """Manages FastAPI server lifecycle for CLI.
18
19
 
19
20
  The ServerManager handles:
21
+ - Per-repo server instances (each repo gets its own server)
20
22
  - Discovering running servers via port file
21
23
  - Starting new servers when needed
22
24
  - Health checking servers
23
25
  - Graceful shutdown on CLI exit
26
+ - Cleanup of stale servers
24
27
  """
25
28
 
26
- DEFAULT_PORT = 8765
27
- PORT_FILE = Path.home() / ".emdash" / "server.port"
28
- PID_FILE = Path.home() / ".emdash" / "server.pid"
29
+ SERVERS_DIR = Path.home() / ".emdash" / "servers"
29
30
  STARTUP_TIMEOUT = 30.0 # seconds
30
31
  HEALTH_TIMEOUT = 2.0 # seconds
31
32
 
@@ -40,8 +41,35 @@ class ServerManager:
40
41
  self.port: Optional[int] = None
41
42
  self._started_by_us = False
42
43
 
44
+ # Create servers directory
45
+ self.SERVERS_DIR.mkdir(parents=True, exist_ok=True)
46
+
47
+ # Cleanup stale servers on init
48
+ self._cleanup_stale_servers()
49
+
50
+ @property
51
+ def _repo_hash(self) -> str:
52
+ """Get a short hash of the repo root path for unique file naming."""
53
+ path_str = str(self.repo_root.resolve())
54
+ return hashlib.sha256(path_str.encode()).hexdigest()[:12]
55
+
56
+ @property
57
+ def _port_file(self) -> Path:
58
+ """Get the port file path for this repo."""
59
+ return self.SERVERS_DIR / f"{self._repo_hash}.port"
60
+
61
+ @property
62
+ def _pid_file(self) -> Path:
63
+ """Get the PID file path for this repo."""
64
+ return self.SERVERS_DIR / f"{self._repo_hash}.pid"
65
+
66
+ @property
67
+ def _repo_file(self) -> Path:
68
+ """Get the repo path file for this repo (for debugging)."""
69
+ return self.SERVERS_DIR / f"{self._repo_hash}.repo"
70
+
43
71
  def get_server_url(self) -> str:
44
- """Get URL of running server, starting one if needed.
72
+ """Get URL of running server for this repo, starting one if needed.
45
73
 
46
74
  Returns:
47
75
  Base URL of the running server (e.g., "http://localhost:8765")
@@ -49,17 +77,19 @@ class ServerManager:
49
77
  Raises:
50
78
  RuntimeError: If server fails to start
51
79
  """
52
- # Check if server already running
53
- if self.PORT_FILE.exists():
80
+ # Check if server already running for THIS repo
81
+ if self._port_file.exists():
54
82
  try:
55
- port = int(self.PORT_FILE.read_text().strip())
83
+ port = int(self._port_file.read_text().strip())
56
84
  if self._check_health(port):
57
85
  self.port = port
58
86
  return f"http://localhost:{port}"
59
87
  except (ValueError, IOError):
60
88
  pass
89
+ # Server not healthy, clean up stale files
90
+ self._cleanup_files()
61
91
 
62
- # Start new server
92
+ # Start new server for this repo
63
93
  self.port = self._find_free_port()
64
94
  self._spawn_server()
65
95
  return f"http://localhost:{self.port}"
@@ -133,6 +163,11 @@ class ServerManager:
133
163
 
134
164
  self._started_by_us = True
135
165
 
166
+ # Write port, PID, and repo files
167
+ self._port_file.write_text(str(self.port))
168
+ self._pid_file.write_text(str(self.process.pid))
169
+ self._repo_file.write_text(str(self.repo_root))
170
+
136
171
  # Register cleanup for normal exit
137
172
  atexit.register(self.shutdown)
138
173
 
@@ -203,14 +238,39 @@ class ServerManager:
203
238
  )
204
239
 
205
240
  def _cleanup_files(self) -> None:
206
- """Clean up port and PID files."""
207
- for file in [self.PORT_FILE, self.PID_FILE]:
241
+ """Clean up port, PID, and repo files for this repo."""
242
+ for file in [self._port_file, self._pid_file, self._repo_file]:
208
243
  try:
209
244
  if file.exists():
210
245
  file.unlink()
211
246
  except IOError:
212
247
  pass
213
248
 
249
+ def _cleanup_stale_servers(self) -> None:
250
+ """Clean up stale server files where process no longer exists."""
251
+ if not self.SERVERS_DIR.exists():
252
+ return
253
+
254
+ for pid_file in self.SERVERS_DIR.glob("*.pid"):
255
+ try:
256
+ pid = int(pid_file.read_text().strip())
257
+ # Check if process exists
258
+ try:
259
+ os.kill(pid, 0) # Signal 0 checks if process exists
260
+ except OSError:
261
+ # Process doesn't exist, clean up files
262
+ hash_prefix = pid_file.stem
263
+ for ext in [".port", ".pid", ".repo"]:
264
+ stale_file = self.SERVERS_DIR / f"{hash_prefix}{ext}"
265
+ if stale_file.exists():
266
+ stale_file.unlink()
267
+ except (ValueError, IOError):
268
+ # Invalid PID file, remove it
269
+ try:
270
+ pid_file.unlink()
271
+ except IOError:
272
+ pass
273
+
214
274
 
215
275
  # Global singleton for CLI commands
216
276
  _server_manager: Optional[ServerManager] = None