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
@@ -0,0 +1,210 @@
1
+ """Session restore prompt for emdash CLI.
2
+
3
+ Detects recent sessions and offers to restore them with zen styling.
4
+ """
5
+
6
+ from datetime import datetime, timedelta
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from rich.console import Console
11
+ from prompt_toolkit import Application
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
14
+ from prompt_toolkit.styles import Style
15
+
16
+ from ...design import (
17
+ Colors,
18
+ STATUS_ACTIVE,
19
+ STATUS_INACTIVE,
20
+ DOT_BULLET,
21
+ ARROW_PROMPT,
22
+ header,
23
+ footer,
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ def get_recent_session(client, max_age_hours: int = 24) -> Optional[dict]:
30
+ """Get the most recent session if within max_age.
31
+
32
+ Args:
33
+ client: Emdash client instance
34
+ max_age_hours: Maximum age in hours for session to be considered recent
35
+
36
+ Returns:
37
+ Session info dict or None
38
+ """
39
+ try:
40
+ sessions = client.list_sessions()
41
+ if not sessions:
42
+ return None
43
+
44
+ # Sort by updated_at (most recent first)
45
+ sessions = sorted(sessions, key=lambda s: s.updated_at or "", reverse=True)
46
+
47
+ if not sessions:
48
+ return None
49
+
50
+ recent = sessions[0]
51
+
52
+ # Check if session is recent enough
53
+ if recent.updated_at:
54
+ try:
55
+ updated = datetime.fromisoformat(recent.updated_at.replace("Z", "+00:00"))
56
+ cutoff = datetime.now(updated.tzinfo) - timedelta(hours=max_age_hours)
57
+ if updated < cutoff:
58
+ return None
59
+ except (ValueError, TypeError):
60
+ pass
61
+
62
+ # Only offer to restore if it has messages
63
+ if recent.message_count and recent.message_count > 0:
64
+ return {
65
+ "name": recent.name,
66
+ "summary": recent.summary,
67
+ "mode": recent.mode,
68
+ "message_count": recent.message_count,
69
+ "updated_at": recent.updated_at,
70
+ }
71
+
72
+ return None
73
+ except Exception:
74
+ return None
75
+
76
+
77
+ def format_relative_time(iso_time: str) -> str:
78
+ """Format ISO time as relative time (e.g., '2 hours ago')."""
79
+ try:
80
+ dt = datetime.fromisoformat(iso_time.replace("Z", "+00:00"))
81
+ now = datetime.now(dt.tzinfo)
82
+ delta = now - dt
83
+
84
+ if delta.days > 0:
85
+ return f"{delta.days} day{'s' if delta.days > 1 else ''} ago"
86
+ elif delta.seconds >= 3600:
87
+ hours = delta.seconds // 3600
88
+ return f"{hours} hour{'s' if hours > 1 else ''} ago"
89
+ elif delta.seconds >= 60:
90
+ minutes = delta.seconds // 60
91
+ return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
92
+ else:
93
+ return "just now"
94
+ except (ValueError, TypeError):
95
+ return ""
96
+
97
+
98
+ def show_session_restore_prompt(session_info: dict) -> tuple[str, Optional[dict]]:
99
+ """Show prompt to restore a recent session.
100
+
101
+ Args:
102
+ session_info: Dict with session details
103
+
104
+ Returns:
105
+ Tuple of (choice, session_data) where choice is:
106
+ - 'restore': Restore the session
107
+ - 'new': Start new session
108
+ - 'view': View session details first
109
+ """
110
+ name = session_info.get("name", "unnamed")
111
+ summary = session_info.get("summary", "")
112
+ mode = session_info.get("mode", "code")
113
+ msg_count = session_info.get("message_count", 0)
114
+ updated = session_info.get("updated_at", "")
115
+
116
+ relative_time = format_relative_time(updated) if updated else ""
117
+
118
+ console.print()
119
+ console.print(f"[{Colors.MUTED}]{header('Session Found', 40)}[/{Colors.MUTED}]")
120
+ console.print()
121
+ console.print(f" [{Colors.DIM}]Previous session from {relative_time}:[/{Colors.DIM}]")
122
+ console.print()
123
+ console.print(f" {DOT_BULLET} [{Colors.MUTED}]{msg_count} messages[/{Colors.MUTED}]")
124
+ if summary:
125
+ truncated = summary[:60] + "..." if len(summary) > 60 else summary
126
+ console.print(f" {DOT_BULLET} [{Colors.MUTED}]{truncated}[/{Colors.MUTED}]")
127
+ console.print(f" {DOT_BULLET} [{Colors.MUTED}]Mode: {mode}[/{Colors.MUTED}]")
128
+ console.print()
129
+ console.print(f"[{Colors.MUTED}]{footer(40)}[/{Colors.MUTED}]")
130
+
131
+ selected_index = [0]
132
+ result = [("new", None)]
133
+
134
+ options = [
135
+ ("restore", "Restore this session"),
136
+ ("new", "Start new session"),
137
+ ]
138
+
139
+ kb = KeyBindings()
140
+
141
+ @kb.add("up")
142
+ @kb.add("k")
143
+ def move_up(event):
144
+ selected_index[0] = (selected_index[0] - 1) % len(options)
145
+
146
+ @kb.add("down")
147
+ @kb.add("j")
148
+ def move_down(event):
149
+ selected_index[0] = (selected_index[0] + 1) % len(options)
150
+
151
+ @kb.add("enter")
152
+ def select(event):
153
+ result[0] = (options[selected_index[0]][0], session_info if options[selected_index[0]][0] == "restore" else None)
154
+ event.app.exit()
155
+
156
+ @kb.add("r")
157
+ def restore(event):
158
+ result[0] = ("restore", session_info)
159
+ event.app.exit()
160
+
161
+ @kb.add("n")
162
+ def new(event):
163
+ result[0] = ("new", None)
164
+ event.app.exit()
165
+
166
+ @kb.add("c-c")
167
+ @kb.add("escape")
168
+ def cancel(event):
169
+ result[0] = ("new", None)
170
+ event.app.exit()
171
+
172
+ def get_formatted_options():
173
+ lines = []
174
+ for i, (key, desc) in enumerate(options):
175
+ indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
176
+ style_class = "selected" if i == selected_index[0] else "option"
177
+ lines.append((f"class:{style_class}", f" {indicator} {desc}\n"))
178
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} r restore n new Esc skip"))
179
+ return lines
180
+
181
+ style = Style.from_dict({
182
+ "selected": f"{Colors.SUCCESS} bold",
183
+ "option": Colors.MUTED,
184
+ "hint": f"{Colors.DIM} italic",
185
+ })
186
+
187
+ layout = Layout(
188
+ HSplit([
189
+ Window(
190
+ FormattedTextControl(get_formatted_options),
191
+ height=5,
192
+ ),
193
+ ])
194
+ )
195
+
196
+ app = Application(
197
+ layout=layout,
198
+ key_bindings=kb,
199
+ style=style,
200
+ full_screen=False,
201
+ )
202
+
203
+ console.print()
204
+
205
+ try:
206
+ app.run()
207
+ except (KeyboardInterrupt, EOFError):
208
+ return ("new", None)
209
+
210
+ return result[0]