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,478 @@
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, scripts
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
+ "scripts": [str(s) for s in skill.scripts] if skill.scripts else [],
48
+ })
49
+
50
+ return skills
51
+
52
+
53
+ def show_skills_interactive_menu() -> tuple[str, str]:
54
+ """Show interactive skills menu.
55
+
56
+ Returns:
57
+ Tuple of (action, skill_name) where action is one of:
58
+ - 'view': View skill details
59
+ - 'create': Create new skill
60
+ - 'delete': Delete skill
61
+ - 'cancel': User cancelled
62
+ """
63
+ from prompt_toolkit import Application
64
+ from prompt_toolkit.key_binding import KeyBindings
65
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
66
+ from prompt_toolkit.styles import Style
67
+
68
+ skills = list_skills()
69
+
70
+ # Build menu items: (name, description, is_builtin, is_action)
71
+ menu_items = []
72
+
73
+ for skill in skills:
74
+ builtin_marker = " [built-in]" if skill["is_builtin"] else ""
75
+ scripts_marker = f" [{len(skill['scripts'])} scripts]" if skill.get("scripts") else ""
76
+ menu_items.append((skill["name"], skill["description"] + builtin_marker + scripts_marker, skill["is_builtin"], False))
77
+
78
+ # Add action items at the bottom
79
+ menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
80
+
81
+ if not menu_items:
82
+ menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
83
+
84
+ selected_index = [0]
85
+ result = [("cancel", "")]
86
+
87
+ kb = KeyBindings()
88
+
89
+ @kb.add("up")
90
+ @kb.add("k")
91
+ def move_up(event):
92
+ selected_index[0] = (selected_index[0] - 1) % len(menu_items)
93
+
94
+ @kb.add("down")
95
+ @kb.add("j")
96
+ def move_down(event):
97
+ selected_index[0] = (selected_index[0] + 1) % len(menu_items)
98
+
99
+ @kb.add("enter")
100
+ def select(event):
101
+ item = menu_items[selected_index[0]]
102
+ name, desc, is_builtin, is_action = item
103
+ if is_action:
104
+ if "Create" in name:
105
+ result[0] = ("create", "")
106
+ else:
107
+ result[0] = ("view", name)
108
+ event.app.exit()
109
+
110
+ @kb.add("d")
111
+ def delete_skill(event):
112
+ item = menu_items[selected_index[0]]
113
+ name, desc, is_builtin, is_action = item
114
+ if not is_action and not is_builtin:
115
+ result[0] = ("delete", name)
116
+ event.app.exit()
117
+ elif is_builtin:
118
+ # Can't delete built-in skills
119
+ pass
120
+
121
+ @kb.add("n")
122
+ def new_skill(event):
123
+ result[0] = ("create", "")
124
+ event.app.exit()
125
+
126
+ @kb.add("c-c")
127
+ @kb.add("escape")
128
+ @kb.add("q")
129
+ def cancel(event):
130
+ result[0] = ("cancel", "")
131
+ event.app.exit()
132
+
133
+ def get_formatted_menu():
134
+ lines = [("class:title", "Skills\n\n")]
135
+
136
+ if not skills:
137
+ lines.append(("class:dim", "No skills defined yet.\n\n"))
138
+
139
+ for i, (name, desc, is_builtin, is_action) in enumerate(menu_items):
140
+ is_selected = i == selected_index[0]
141
+ prefix = ">" if is_selected else " "
142
+
143
+ if is_action:
144
+ if is_selected:
145
+ lines.append(("class:action-selected", f"{prefix}{name}\n"))
146
+ else:
147
+ lines.append(("class:action", f"{prefix}{name}\n"))
148
+ else:
149
+ if is_selected:
150
+ lines.append(("class:skill-selected", f"{prefix}{name}"))
151
+ lines.append(("class:desc-selected", f" - {desc}\n"))
152
+ else:
153
+ lines.append(("class:skill", f"{prefix}{name}"))
154
+ lines.append(("class:desc", f" - {desc}\n"))
155
+
156
+ lines.append(("class:hint", "\n[up]/[down] navigate | Enter view | n new | d delete | q quit"))
157
+ return lines
158
+
159
+ style = Style.from_dict({
160
+ "title": "#00ccff bold",
161
+ "dim": "#666666",
162
+ "skill": "#00ccff",
163
+ "skill-selected": "#00cc66 bold",
164
+ "action": "#ffcc00",
165
+ "action-selected": "#ffcc00 bold",
166
+ "desc": "#666666",
167
+ "desc-selected": "#00cc66",
168
+ "hint": "#888888 italic",
169
+ })
170
+
171
+ height = len(menu_items) + 5 # items + title + hint + padding
172
+
173
+ layout = Layout(
174
+ HSplit([
175
+ Window(
176
+ FormattedTextControl(get_formatted_menu),
177
+ height=height,
178
+ ),
179
+ ])
180
+ )
181
+
182
+ app = Application(
183
+ layout=layout,
184
+ key_bindings=kb,
185
+ style=style,
186
+ full_screen=False,
187
+ )
188
+
189
+ console.print()
190
+
191
+ try:
192
+ app.run()
193
+ except (KeyboardInterrupt, EOFError):
194
+ result[0] = ("cancel", "")
195
+
196
+ # Clear menu visually with separator
197
+ console.print()
198
+
199
+ return result[0]
200
+
201
+
202
+ def show_skill_details(name: str) -> None:
203
+ """Show detailed view of a skill."""
204
+ from emdash_core.agent.skills import SkillRegistry
205
+
206
+ skills_dir = _get_skills_dir()
207
+ registry = SkillRegistry.get_instance()
208
+ registry.load_skills(skills_dir)
209
+
210
+ skill = registry.get_skill(name)
211
+
212
+ console.print()
213
+ console.print("[dim]" + "-" * 50 + "[/dim]")
214
+ console.print()
215
+
216
+ if skill:
217
+ builtin_marker = " [built-in]" if getattr(skill, "_builtin", False) else ""
218
+ invocable = f"Yes (/{skill.name})" if skill.user_invocable else "No"
219
+ tools = ", ".join(skill.tools) if skill.tools else "None"
220
+
221
+ console.print(f"[bold cyan]{skill.name}[/bold cyan]{builtin_marker}\n")
222
+ console.print(f"[bold]Description:[/bold] {skill.description}")
223
+ console.print(f"[bold]User Invocable:[/bold] {invocable}")
224
+ console.print(f"[bold]Tools:[/bold] {tools}")
225
+
226
+ # Show scripts
227
+ if skill.scripts:
228
+ console.print(f"[bold]Scripts:[/bold] {len(skill.scripts)}")
229
+ for script in skill.scripts:
230
+ console.print(f" [yellow]{script.name}[/yellow]: {script}")
231
+ else:
232
+ console.print(f"[bold]Scripts:[/bold] None")
233
+
234
+ console.print(f"[bold]File:[/bold] {skill.file_path}\n")
235
+ console.print("[bold]Instructions:[/bold]")
236
+ console.print(Panel(skill.instructions, border_style="dim"))
237
+ else:
238
+ console.print(f"[yellow]Skill '{name}' not found[/yellow]")
239
+
240
+ console.print()
241
+ console.print("[dim]" + "-" * 50 + "[/dim]")
242
+
243
+
244
+ def confirm_delete(skill_name: str) -> bool:
245
+ """Confirm skill deletion."""
246
+ from prompt_toolkit import PromptSession
247
+
248
+ console.print()
249
+ console.print(f"[yellow]Delete skill '{skill_name}'?[/yellow]")
250
+ console.print("[dim]This will remove the skill directory. Type 'yes' to confirm.[/dim]")
251
+
252
+ try:
253
+ session = PromptSession()
254
+ response = session.prompt("Confirm > ").strip().lower()
255
+ return response in ("yes", "y")
256
+ except (KeyboardInterrupt, EOFError):
257
+ return False
258
+
259
+
260
+ def delete_skill(name: str) -> bool:
261
+ """Delete a skill directory."""
262
+ import shutil
263
+
264
+ skills_dir = _get_skills_dir()
265
+ skill_dir = skills_dir / name
266
+
267
+ if not skill_dir.exists():
268
+ console.print(f"[yellow]Skill directory not found: {skill_dir}[/yellow]")
269
+ return False
270
+
271
+ # Check if it's a built-in skill
272
+ from emdash_core.agent.skills import SkillRegistry
273
+ registry = SkillRegistry.get_instance()
274
+ registry.load_skills(skills_dir)
275
+ skill = registry.get_skill(name)
276
+
277
+ if skill and getattr(skill, "_builtin", False):
278
+ console.print(f"[red]Cannot delete built-in skill '{name}'[/red]")
279
+ return False
280
+
281
+ if confirm_delete(name):
282
+ shutil.rmtree(skill_dir)
283
+ console.print(f"[green]Deleted skill: {name}[/green]")
284
+ return True
285
+ else:
286
+ console.print("[dim]Cancelled[/dim]")
287
+ return False
288
+
289
+
290
+ def chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt) -> None:
291
+ """Start a chat session to create a new skill with AI assistance."""
292
+ from prompt_toolkit import PromptSession
293
+ from prompt_toolkit.styles import Style
294
+
295
+ skills_dir = _get_skills_dir()
296
+
297
+ console.print()
298
+ console.print("[bold cyan]Create New Skill[/bold cyan]")
299
+ console.print("[dim]Describe what skill you want to create. The AI will help you write it.[/dim]")
300
+ console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
301
+ console.print()
302
+
303
+ chat_style = Style.from_dict({
304
+ "prompt": "#00cc66 bold",
305
+ })
306
+
307
+ ps = PromptSession(style=chat_style)
308
+ chat_session_id = None
309
+ first_message = True
310
+
311
+ # Ensure skills directory exists
312
+ skills_dir.mkdir(parents=True, exist_ok=True)
313
+
314
+ # Chat loop
315
+ while True:
316
+ try:
317
+ user_input = ps.prompt([("class:prompt", "> ")]).strip()
318
+
319
+ if not user_input:
320
+ continue
321
+
322
+ if user_input.lower() in ("done", "quit", "exit", "q"):
323
+ console.print("[dim]Finished[/dim]")
324
+ break
325
+
326
+ # First message includes context about skills
327
+ if first_message:
328
+ message_with_context = f"""I want to create a new skill for my AI agent.
329
+
330
+ **Skills directory:** `{skills_dir}`
331
+
332
+ Skills are markdown files (SKILL.md) with YAML frontmatter that define reusable instructions for the agent.
333
+
334
+ SKILL.md format:
335
+ ```markdown
336
+ ---
337
+ name: skill-name
338
+ description: When to use this skill
339
+ user_invocable: true
340
+ tools: [tool1, tool2]
341
+ ---
342
+
343
+ # Skill Title
344
+
345
+ Instructions for the skill...
346
+
347
+ ## Scripts (optional)
348
+
349
+ If scripts are included, document them here.
350
+ ```
351
+
352
+ **Frontmatter fields:**
353
+ - `name`: Unique skill identifier (lowercase, hyphens allowed)
354
+ - `description`: Brief description of when to use this skill
355
+ - `user_invocable`: Whether skill can be invoked with /name (true/false)
356
+ - `tools`: List of tools this skill needs (optional)
357
+
358
+ **Skill Scripts (optional):**
359
+ Skills can include executable bash scripts in the same directory as SKILL.md. These scripts:
360
+ - Must be self-contained bash executables (with shebang like `#!/bin/bash`)
361
+ - Are automatically discovered and made available to the agent
362
+ - Can be named anything (e.g., `run.sh`, `deploy.sh`, `validate.sh`)
363
+ - Are executed by the agent using the Bash tool when needed
364
+
365
+ Example script (`run.sh`):
366
+ ```bash
367
+ #!/bin/bash
368
+ set -e
369
+ echo "Running skill script..."
370
+ # Add script logic here
371
+ ```
372
+
373
+ **My request:** {user_input}
374
+
375
+ Please help me create a skill. Ask me questions if needed to understand what I want:
376
+ 1. What should the skill do?
377
+ 2. Does it need any scripts to execute code?
378
+
379
+ Then use the Write tool to create:
380
+ 1. The SKILL.md file at `{skills_dir}/<skill-name>/SKILL.md`
381
+ 2. Any scripts the user wants (e.g., `{skills_dir}/<skill-name>/run.sh`)
382
+
383
+ Remember to make scripts executable by including the shebang."""
384
+ stream = client.agent_chat_stream(
385
+ message=message_with_context,
386
+ model=model,
387
+ max_iterations=max_iterations,
388
+ options={"mode": "code"},
389
+ )
390
+ first_message = False
391
+ elif chat_session_id:
392
+ stream = client.agent_continue_stream(
393
+ chat_session_id, user_input
394
+ )
395
+ else:
396
+ stream = client.agent_chat_stream(
397
+ message=user_input,
398
+ model=model,
399
+ max_iterations=max_iterations,
400
+ options={"mode": "code"},
401
+ )
402
+
403
+ result = render_with_interrupt(renderer, stream)
404
+ if result and result.get("session_id"):
405
+ chat_session_id = result["session_id"]
406
+
407
+ except (KeyboardInterrupt, EOFError):
408
+ console.print()
409
+ console.print("[dim]Cancelled[/dim]")
410
+ break
411
+ except Exception as e:
412
+ console.print(f"[red]Error: {e}[/red]")
413
+
414
+
415
+ def handle_skills(args: str, client, renderer, model, max_iterations, render_with_interrupt) -> None:
416
+ """Handle /skills command."""
417
+ from prompt_toolkit import PromptSession
418
+
419
+ # Handle subcommands
420
+ if args:
421
+ subparts = args.split(maxsplit=1)
422
+ subcommand = subparts[0].lower()
423
+ subargs = subparts[1] if len(subparts) > 1 else ""
424
+
425
+ if subcommand == "list":
426
+ skills = list_skills()
427
+ if skills:
428
+ console.print("\n[bold cyan]Skills[/bold cyan]\n")
429
+ for skill in skills:
430
+ builtin = " [built-in]" if skill["is_builtin"] else ""
431
+ invocable = f" (/{skill['name']})" if skill["user_invocable"] else ""
432
+ console.print(f" [cyan]{skill['name']}[/cyan]{builtin}{invocable} - {skill['description']}")
433
+ console.print()
434
+ else:
435
+ console.print("\n[dim]No skills defined yet.[/dim]")
436
+ console.print(f"[dim]Skills directory: {_get_skills_dir()}[/dim]\n")
437
+ elif subcommand == "show" and subargs:
438
+ show_skill_details(subargs.strip())
439
+ elif subcommand == "delete" and subargs:
440
+ delete_skill(subargs.strip())
441
+ elif subcommand in ("add", "create", "new"):
442
+ chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt)
443
+ else:
444
+ console.print("[yellow]Usage: /skills [list|show|add|delete] [name][/yellow]")
445
+ console.print("[dim]Or just /skills for interactive menu[/dim]")
446
+ else:
447
+ # Interactive menu
448
+ while True:
449
+ action, skill_name = show_skills_interactive_menu()
450
+
451
+ if action == "cancel":
452
+ break
453
+ elif action == "view":
454
+ show_skill_details(skill_name)
455
+ # After viewing, show options
456
+ try:
457
+ # Check if it's a built-in skill
458
+ from emdash_core.agent.skills import SkillRegistry
459
+ registry = SkillRegistry.get_instance()
460
+ skill = registry.get_skill(skill_name)
461
+ is_builtin = skill and getattr(skill, "_builtin", False)
462
+
463
+ if is_builtin:
464
+ console.print("[dim]Enter to go back[/dim]", end="")
465
+ else:
466
+ console.print("[red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
467
+ ps = PromptSession()
468
+ resp = ps.prompt(" ").strip().lower()
469
+ if resp == 'd' and not is_builtin:
470
+ delete_skill(skill_name)
471
+ console.print()
472
+ except (KeyboardInterrupt, EOFError):
473
+ break
474
+ elif action == "create":
475
+ chat_create_skill(client, renderer, model, max_iterations, render_with_interrupt)
476
+ # Refresh menu after creating
477
+ elif action == "delete":
478
+ delete_skill(skill_name)