emdash-cli 0.1.35__py3-none-any.whl → 0.1.46__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 (30) hide show
  1. emdash_cli/client.py +35 -0
  2. emdash_cli/clipboard.py +30 -61
  3. emdash_cli/commands/agent/__init__.py +14 -0
  4. emdash_cli/commands/agent/cli.py +100 -0
  5. emdash_cli/commands/agent/constants.py +53 -0
  6. emdash_cli/commands/agent/file_utils.py +178 -0
  7. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  8. emdash_cli/commands/agent/handlers/agents.py +421 -0
  9. emdash_cli/commands/agent/handlers/auth.py +69 -0
  10. emdash_cli/commands/agent/handlers/doctor.py +319 -0
  11. emdash_cli/commands/agent/handlers/hooks.py +121 -0
  12. emdash_cli/commands/agent/handlers/mcp.py +183 -0
  13. emdash_cli/commands/agent/handlers/misc.py +200 -0
  14. emdash_cli/commands/agent/handlers/rules.py +394 -0
  15. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  16. emdash_cli/commands/agent/handlers/setup.py +582 -0
  17. emdash_cli/commands/agent/handlers/skills.py +440 -0
  18. emdash_cli/commands/agent/handlers/todos.py +98 -0
  19. emdash_cli/commands/agent/handlers/verify.py +648 -0
  20. emdash_cli/commands/agent/interactive.py +657 -0
  21. emdash_cli/commands/agent/menus.py +728 -0
  22. emdash_cli/commands/agent.py +7 -1321
  23. emdash_cli/commands/server.py +99 -40
  24. emdash_cli/server_manager.py +70 -10
  25. emdash_cli/sse_renderer.py +36 -5
  26. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  27. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  28. emdash_cli-0.1.35.dist-info/RECORD +0 -30
  29. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  30. {emdash_cli-0.1.35.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,440 @@
1
+ """Handler for /skills command."""
2
+
3
+ from pathlib import Path
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ console = Console()
9
+
10
+
11
+ def _get_skills_dir() -> Path:
12
+ """Get the skills directory path."""
13
+ return Path.cwd() / ".emdash" / "skills"
14
+
15
+
16
+ def _get_builtin_skills_dir() -> Path:
17
+ """Get the built-in skills directory."""
18
+ try:
19
+ from emdash_core.agent.skills import _get_builtin_skills_dir as get_builtin
20
+ return get_builtin()
21
+ except ImportError:
22
+ return Path()
23
+
24
+
25
+ def list_skills() -> list[dict]:
26
+ """List all skills (both user and built-in).
27
+
28
+ Returns:
29
+ List of dicts with name, description, user_invocable, is_builtin, file_path
30
+ """
31
+ from emdash_core.agent.skills import SkillRegistry
32
+
33
+ skills_dir = _get_skills_dir()
34
+ registry = SkillRegistry.get_instance()
35
+ registry.load_skills(skills_dir)
36
+
37
+ all_skills = registry.get_all_skills()
38
+ skills = []
39
+
40
+ for skill in all_skills.values():
41
+ skills.append({
42
+ "name": skill.name,
43
+ "description": skill.description,
44
+ "user_invocable": skill.user_invocable,
45
+ "is_builtin": getattr(skill, "_builtin", False),
46
+ "file_path": str(skill.file_path) if skill.file_path else None,
47
+ })
48
+
49
+ return skills
50
+
51
+
52
+ def show_skills_interactive_menu() -> tuple[str, str]:
53
+ """Show interactive skills menu.
54
+
55
+ Returns:
56
+ Tuple of (action, skill_name) where action is one of:
57
+ - 'view': View skill details
58
+ - 'create': Create new skill
59
+ - 'delete': Delete skill
60
+ - 'cancel': User cancelled
61
+ """
62
+ from prompt_toolkit import Application
63
+ from prompt_toolkit.key_binding import KeyBindings
64
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
65
+ from prompt_toolkit.styles import Style
66
+
67
+ skills = list_skills()
68
+
69
+ # Build menu items: (name, description, is_builtin, is_action)
70
+ menu_items = []
71
+
72
+ for skill in skills:
73
+ builtin_marker = " [built-in]" if skill["is_builtin"] else ""
74
+ menu_items.append((skill["name"], skill["description"] + builtin_marker, skill["is_builtin"], False))
75
+
76
+ # Add action items at the bottom
77
+ menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
78
+
79
+ if not menu_items:
80
+ menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
81
+
82
+ selected_index = [0]
83
+ result = [("cancel", "")]
84
+
85
+ kb = KeyBindings()
86
+
87
+ @kb.add("up")
88
+ @kb.add("k")
89
+ def move_up(event):
90
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
91
+
92
+ @kb.add("down")
93
+ @kb.add("j")
94
+ def move_down(event):
95
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
96
+
97
+ @kb.add("enter")
98
+ def select(event):
99
+ item = menu_items[selected_index[0]]
100
+ name, desc, is_builtin, is_action = item
101
+ if is_action:
102
+ if "Create" in name:
103
+ result[0] = ("create", "")
104
+ else:
105
+ result[0] = ("view", name)
106
+ event.app.exit()
107
+
108
+ @kb.add("d")
109
+ def delete_skill(event):
110
+ item = menu_items[selected_index[0]]
111
+ name, desc, is_builtin, is_action = item
112
+ if not is_action and not is_builtin:
113
+ result[0] = ("delete", name)
114
+ event.app.exit()
115
+ elif is_builtin:
116
+ # Can't delete built-in skills
117
+ pass
118
+
119
+ @kb.add("n")
120
+ def new_skill(event):
121
+ result[0] = ("create", "")
122
+ event.app.exit()
123
+
124
+ @kb.add("c-c")
125
+ @kb.add("escape")
126
+ @kb.add("q")
127
+ def cancel(event):
128
+ result[0] = ("cancel", "")
129
+ event.app.exit()
130
+
131
+ def get_formatted_menu():
132
+ lines = [("class:title", "Skills\n\n")]
133
+
134
+ if not skills:
135
+ lines.append(("class:dim", "No skills defined yet.\n\n"))
136
+
137
+ for i, (name, desc, is_builtin, is_action) in enumerate(menu_items):
138
+ is_selected = i == selected_index[0]
139
+ prefix = ">" if is_selected else " "
140
+
141
+ if is_action:
142
+ if is_selected:
143
+ lines.append(("class:action-selected", f"{prefix}{name}\n"))
144
+ else:
145
+ lines.append(("class:action", f"{prefix}{name}\n"))
146
+ else:
147
+ if is_selected:
148
+ lines.append(("class:skill-selected", f"{prefix}{name}"))
149
+ lines.append(("class:desc-selected", f" - {desc}\n"))
150
+ else:
151
+ lines.append(("class:skill", f"{prefix}{name}"))
152
+ lines.append(("class:desc", f" - {desc}\n"))
153
+
154
+ lines.append(("class:hint", "\n[up]/[down] navigate | Enter view | n new | d delete | q quit"))
155
+ return lines
156
+
157
+ style = Style.from_dict({
158
+ "title": "#00ccff bold",
159
+ "dim": "#666666",
160
+ "skill": "#00ccff",
161
+ "skill-selected": "#00cc66 bold",
162
+ "action": "#ffcc00",
163
+ "action-selected": "#ffcc00 bold",
164
+ "desc": "#666666",
165
+ "desc-selected": "#00cc66",
166
+ "hint": "#888888 italic",
167
+ })
168
+
169
+ height = len(menu_items) + 5 # items + title + hint + padding
170
+
171
+ layout = Layout(
172
+ HSplit([
173
+ Window(
174
+ FormattedTextControl(get_formatted_menu),
175
+ height=height,
176
+ ),
177
+ ])
178
+ )
179
+
180
+ app = Application(
181
+ layout=layout,
182
+ key_bindings=kb,
183
+ style=style,
184
+ full_screen=False,
185
+ )
186
+
187
+ console.print()
188
+
189
+ try:
190
+ app.run()
191
+ except (KeyboardInterrupt, EOFError):
192
+ result[0] = ("cancel", "")
193
+
194
+ # Clear menu visually with separator
195
+ console.print()
196
+
197
+ return result[0]
198
+
199
+
200
+ def show_skill_details(name: str) -> None:
201
+ """Show detailed view of a skill."""
202
+ from emdash_core.agent.skills import SkillRegistry
203
+
204
+ skills_dir = _get_skills_dir()
205
+ registry = SkillRegistry.get_instance()
206
+ registry.load_skills(skills_dir)
207
+
208
+ skill = registry.get_skill(name)
209
+
210
+ console.print()
211
+ console.print("[dim]" + "-" * 50 + "[/dim]")
212
+ console.print()
213
+
214
+ if skill:
215
+ builtin_marker = " [built-in]" if getattr(skill, "_builtin", False) else ""
216
+ invocable = f"Yes (/{skill.name})" if skill.user_invocable else "No"
217
+ tools = ", ".join(skill.tools) if skill.tools else "None"
218
+
219
+ console.print(f"[bold cyan]{skill.name}[/bold cyan]{builtin_marker}\n")
220
+ console.print(f"[bold]Description:[/bold] {skill.description}")
221
+ console.print(f"[bold]User Invocable:[/bold] {invocable}")
222
+ console.print(f"[bold]Tools:[/bold] {tools}")
223
+ console.print(f"[bold]File:[/bold] {skill.file_path}\n")
224
+ console.print("[bold]Instructions:[/bold]")
225
+ console.print(Panel(skill.instructions, border_style="dim"))
226
+ else:
227
+ console.print(f"[yellow]Skill '{name}' not found[/yellow]")
228
+
229
+ console.print()
230
+ console.print("[dim]" + "-" * 50 + "[/dim]")
231
+
232
+
233
+ def confirm_delete(skill_name: str) -> bool:
234
+ """Confirm skill deletion."""
235
+ from prompt_toolkit import PromptSession
236
+
237
+ console.print()
238
+ console.print(f"[yellow]Delete skill '{skill_name}'?[/yellow]")
239
+ console.print("[dim]This will remove the skill directory. Type 'yes' to confirm.[/dim]")
240
+
241
+ try:
242
+ session = PromptSession()
243
+ response = session.prompt("Confirm > ").strip().lower()
244
+ return response in ("yes", "y")
245
+ except (KeyboardInterrupt, EOFError):
246
+ return False
247
+
248
+
249
+ def delete_skill(name: str) -> bool:
250
+ """Delete a skill directory."""
251
+ import shutil
252
+
253
+ skills_dir = _get_skills_dir()
254
+ skill_dir = skills_dir / name
255
+
256
+ if not skill_dir.exists():
257
+ console.print(f"[yellow]Skill directory not found: {skill_dir}[/yellow]")
258
+ return False
259
+
260
+ # Check if it's a built-in skill
261
+ from emdash_core.agent.skills import SkillRegistry
262
+ registry = SkillRegistry.get_instance()
263
+ registry.load_skills(skills_dir)
264
+ skill = registry.get_skill(name)
265
+
266
+ if skill and getattr(skill, "_builtin", False):
267
+ console.print(f"[red]Cannot delete built-in skill '{name}'[/red]")
268
+ return False
269
+
270
+ if confirm_delete(name):
271
+ shutil.rmtree(skill_dir)
272
+ console.print(f"[green]Deleted skill: {name}[/green]")
273
+ return True
274
+ else:
275
+ console.print("[dim]Cancelled[/dim]")
276
+ return False
277
+
278
+
279
+ def chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt) -> None:
280
+ """Start a chat session to create a new skill with AI assistance."""
281
+ from prompt_toolkit import PromptSession
282
+ from prompt_toolkit.styles import Style
283
+
284
+ skills_dir = _get_skills_dir()
285
+
286
+ console.print()
287
+ console.print("[bold cyan]Create New Skill[/bold cyan]")
288
+ console.print("[dim]Describe what skill you want to create. The AI will help you write it.[/dim]")
289
+ console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
290
+ console.print()
291
+
292
+ chat_style = Style.from_dict({
293
+ "prompt": "#00cc66 bold",
294
+ })
295
+
296
+ ps = PromptSession(style=chat_style)
297
+ chat_session_id = None
298
+ first_message = True
299
+
300
+ # Ensure skills directory exists
301
+ skills_dir.mkdir(parents=True, exist_ok=True)
302
+
303
+ # Chat loop
304
+ while True:
305
+ try:
306
+ user_input = ps.prompt([("class:prompt", "> ")]).strip()
307
+
308
+ if not user_input:
309
+ continue
310
+
311
+ if user_input.lower() in ("done", "quit", "exit", "q"):
312
+ console.print("[dim]Finished[/dim]")
313
+ break
314
+
315
+ # First message includes context about skills
316
+ if first_message:
317
+ message_with_context = f"""I want to create a new skill for my AI agent.
318
+
319
+ **Skills directory:** `{skills_dir}`
320
+
321
+ Skills are markdown files (SKILL.md) with YAML frontmatter that define reusable instructions for the agent.
322
+
323
+ SKILL.md format:
324
+ ```markdown
325
+ ---
326
+ name: skill-name
327
+ description: When to use this skill
328
+ user_invocable: true
329
+ tools: [tool1, tool2]
330
+ ---
331
+
332
+ # Skill Title
333
+
334
+ Instructions for the skill...
335
+ ```
336
+
337
+ **Frontmatter fields:**
338
+ - `name`: Unique skill identifier (lowercase, hyphens allowed)
339
+ - `description`: Brief description of when to use this skill
340
+ - `user_invocable`: Whether skill can be invoked with /name (true/false)
341
+ - `tools`: List of tools this skill needs (optional)
342
+
343
+ **My request:** {user_input}
344
+
345
+ Please help me create a skill. Ask me questions if needed to understand what I want, then use the Write tool to create the file at `{skills_dir}/<skill-name>/SKILL.md`."""
346
+ stream = client.agent_chat_stream(
347
+ message=message_with_context,
348
+ model=model,
349
+ max_iterations=max_iterations,
350
+ options={"mode": "code"},
351
+ )
352
+ first_message = False
353
+ elif chat_session_id:
354
+ stream = client.agent_continue_stream(
355
+ chat_session_id, user_input
356
+ )
357
+ else:
358
+ stream = client.agent_chat_stream(
359
+ message=user_input,
360
+ model=model,
361
+ max_iterations=max_iterations,
362
+ options={"mode": "code"},
363
+ )
364
+
365
+ result = render_with_interrupt(renderer, stream)
366
+ if result and result.get("session_id"):
367
+ chat_session_id = result["session_id"]
368
+
369
+ except (KeyboardInterrupt, EOFError):
370
+ console.print()
371
+ console.print("[dim]Cancelled[/dim]")
372
+ break
373
+ except Exception as e:
374
+ console.print(f"[red]Error: {e}[/red]")
375
+
376
+
377
+ def handle_skills(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
378
+ """Handle /skills command."""
379
+ from prompt_toolkit import PromptSession
380
+
381
+ # Handle subcommands
382
+ if args:
383
+ subparts = args.split(maxsplit=1)
384
+ subcommand = subparts[0].lower()
385
+ subargs = subparts[1] if len(subparts) > 1 else ""
386
+
387
+ if subcommand == "list":
388
+ skills = list_skills()
389
+ if skills:
390
+ console.print("\n[bold cyan]Skills[/bold cyan]\n")
391
+ for skill in skills:
392
+ builtin = " [built-in]" if skill["is_builtin"] else ""
393
+ invocable = f" (/{skill['name']})" if skill["user_invocable"] else ""
394
+ console.print(f" [cyan]{skill['name']}[/cyan]{builtin}{invocable} - {skill['description']}")
395
+ console.print()
396
+ else:
397
+ console.print("\n[dim]No skills defined yet.[/dim]")
398
+ console.print(f"[dim]Skills directory: {_get_skills_dir()}[/dim]\n")
399
+ elif subcommand == "show" and subargs:
400
+ show_skill_details(subargs.strip())
401
+ elif subcommand == "delete" and subargs:
402
+ delete_skill(subargs.strip())
403
+ elif subcommand in ("add", "create", "new"):
404
+ chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt)
405
+ else:
406
+ console.print("[yellow]Usage: /skills [list|show|add|delete] [name][/yellow]")
407
+ console.print("[dim]Or just /skills for interactive menu[/dim]")
408
+ else:
409
+ # Interactive menu
410
+ while True:
411
+ action, skill_name = show_skills_interactive_menu()
412
+
413
+ if action == "cancel":
414
+ break
415
+ elif action == "view":
416
+ show_skill_details(skill_name)
417
+ # After viewing, show options
418
+ try:
419
+ # Check if it's a built-in skill
420
+ from emdash_core.agent.skills import SkillRegistry
421
+ registry = SkillRegistry.get_instance()
422
+ skill = registry.get_skill(skill_name)
423
+ is_builtin = skill and getattr(skill, "_builtin", False)
424
+
425
+ if is_builtin:
426
+ console.print("[dim]Enter to go back[/dim]", end="")
427
+ else:
428
+ console.print("[dim]'d' delete | Enter back[/dim]", end="")
429
+ ps = PromptSession()
430
+ resp = ps.prompt(" ").strip().lower()
431
+ if resp == 'd' and not is_builtin:
432
+ delete_skill(skill_name)
433
+ console.print()
434
+ except (KeyboardInterrupt, EOFError):
435
+ break
436
+ elif action == "create":
437
+ chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt)
438
+ # Refresh menu after creating
439
+ elif action == "delete":
440
+ delete_skill(skill_name)
@@ -0,0 +1,98 @@
1
+ """Handler for /todos and /todo-add commands."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ def handle_todos(args: str, client, session_id: str | None, pending_todos: list[str]) -> None:
9
+ """Handle /todos command.
10
+
11
+ Args:
12
+ args: Command arguments (unused)
13
+ client: EmdashClient instance
14
+ session_id: Current session ID
15
+ pending_todos: List of pending todos (before session starts)
16
+ """
17
+ if not session_id:
18
+ if pending_todos:
19
+ console.print("\n[bold cyan]Pending Todos[/bold cyan] [dim](will be added when session starts)[/dim]\n")
20
+ for i, todo in enumerate(pending_todos, 1):
21
+ console.print(f" [dim]⬚[/dim] {todo}")
22
+ console.print()
23
+ else:
24
+ console.print("\n[dim]No todos yet. Start a conversation and the agent will track its tasks here.[/dim]\n")
25
+ else:
26
+ try:
27
+ result = client.get_todos(session_id)
28
+ todos = result.get("todos", [])
29
+ summary = result.get("summary", {})
30
+
31
+ if not todos:
32
+ console.print("\n[dim]No todos in current session.[/dim]\n")
33
+ else:
34
+ console.print("\n[bold cyan]Agent Todo List[/bold cyan]\n")
35
+
36
+ # Status icons
37
+ icons = {
38
+ "pending": "[dim]⬚[/dim]",
39
+ "in_progress": "[yellow]🔄[/yellow]",
40
+ "completed": "[green]✓[/green]",
41
+ }
42
+
43
+ for todo in todos:
44
+ icon = icons.get(todo["status"], "?")
45
+ title = todo["title"]
46
+ status = todo["status"]
47
+
48
+ if status == "completed":
49
+ console.print(f" {icon} [dim strikethrough]{title}[/dim strikethrough]")
50
+ elif status == "in_progress":
51
+ console.print(f" {icon} [bold]{title}[/bold]")
52
+ else:
53
+ console.print(f" {icon} {title}")
54
+
55
+ if todo.get("description"):
56
+ desc = todo["description"]
57
+ if len(desc) > 60:
58
+ console.print(f" [dim]{desc[:60]}...[/dim]")
59
+ else:
60
+ console.print(f" [dim]{desc}[/dim]")
61
+
62
+ console.print()
63
+ console.print(
64
+ f"[dim]Total: {summary.get('total', 0)} | "
65
+ f"Pending: {summary.get('pending', 0)} | "
66
+ f"In Progress: {summary.get('in_progress', 0)} | "
67
+ f"Completed: {summary.get('completed', 0)}[/dim]"
68
+ )
69
+ console.print()
70
+
71
+ except Exception as e:
72
+ console.print(f"[red]Error fetching todos: {e}[/red]")
73
+
74
+
75
+ def handle_todo_add(args: str, client, session_id: str | None, pending_todos: list[str]) -> None:
76
+ """Handle /todo-add command.
77
+
78
+ Args:
79
+ args: Todo title to add
80
+ client: EmdashClient instance
81
+ session_id: Current session ID
82
+ pending_todos: List of pending todos (before session starts)
83
+ """
84
+ if not args:
85
+ console.print("[yellow]Usage: /todo-add <title>[/yellow]")
86
+ console.print("[dim]Example: /todo-add Fix the failing tests[/dim]")
87
+ elif not session_id:
88
+ pending_todos.append(args)
89
+ console.print(f"[green]Todo noted:[/green] {args}")
90
+ console.print("[dim]This will be added to the agent's context when you start a conversation.[/dim]")
91
+ else:
92
+ try:
93
+ result = client.add_todo(session_id, args)
94
+ task = result.get("task", {})
95
+ console.print(f"[green]Added todo #{task.get('id')}: {task.get('title')}[/green]")
96
+ console.print(f"[dim]Total tasks: {result.get('total_tasks', 0)}[/dim]")
97
+ except Exception as e:
98
+ console.print(f"[red]Error adding todo: {e}[/red]")