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,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]
|