emdash-cli 0.1.46__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 (39) hide show
  1. emdash_cli/client.py +12 -28
  2. emdash_cli/commands/__init__.py +2 -2
  3. emdash_cli/commands/agent/constants.py +10 -0
  4. emdash_cli/commands/agent/handlers/__init__.py +10 -0
  5. emdash_cli/commands/agent/handlers/agents.py +67 -39
  6. emdash_cli/commands/agent/handlers/index.py +183 -0
  7. emdash_cli/commands/agent/handlers/misc.py +119 -0
  8. emdash_cli/commands/agent/handlers/registry.py +72 -0
  9. emdash_cli/commands/agent/handlers/rules.py +48 -31
  10. emdash_cli/commands/agent/handlers/sessions.py +1 -1
  11. emdash_cli/commands/agent/handlers/setup.py +187 -54
  12. emdash_cli/commands/agent/handlers/skills.py +42 -4
  13. emdash_cli/commands/agent/handlers/telegram.py +475 -0
  14. emdash_cli/commands/agent/handlers/todos.py +55 -34
  15. emdash_cli/commands/agent/handlers/verify.py +10 -5
  16. emdash_cli/commands/agent/help.py +236 -0
  17. emdash_cli/commands/agent/interactive.py +222 -37
  18. emdash_cli/commands/agent/menus.py +116 -84
  19. emdash_cli/commands/agent/onboarding.py +619 -0
  20. emdash_cli/commands/agent/session_restore.py +210 -0
  21. emdash_cli/commands/index.py +111 -13
  22. emdash_cli/commands/registry.py +635 -0
  23. emdash_cli/commands/skills.py +72 -6
  24. emdash_cli/design.py +328 -0
  25. emdash_cli/diff_renderer.py +438 -0
  26. emdash_cli/integrations/__init__.py +1 -0
  27. emdash_cli/integrations/telegram/__init__.py +15 -0
  28. emdash_cli/integrations/telegram/bot.py +402 -0
  29. emdash_cli/integrations/telegram/bridge.py +865 -0
  30. emdash_cli/integrations/telegram/config.py +155 -0
  31. emdash_cli/integrations/telegram/formatter.py +385 -0
  32. emdash_cli/main.py +52 -2
  33. emdash_cli/sse_renderer.py +632 -171
  34. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/METADATA +2 -2
  35. emdash_cli-0.1.67.dist-info/RECORD +63 -0
  36. emdash_cli/commands/swarm.py +0 -86
  37. emdash_cli-0.1.46.dist-info/RECORD +0 -49
  38. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/WHEEL +0 -0
  39. {emdash_cli-0.1.46.dist-info → emdash_cli-0.1.67.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,72 @@
1
+ """Handler for /registry command."""
2
+
3
+ from rich.console import Console
4
+
5
+ console = Console()
6
+
7
+
8
+ def handle_registry(args: str) -> None:
9
+ """Handle /registry command.
10
+
11
+ Args:
12
+ args: Command arguments (list, show, install, search)
13
+ """
14
+ from emdash_cli.commands.registry import (
15
+ _show_registry_wizard,
16
+ _fetch_registry,
17
+ registry_list,
18
+ registry_show,
19
+ registry_install,
20
+ registry_search,
21
+ )
22
+
23
+ # Parse subcommand
24
+ subparts = args.split(maxsplit=1) if args else []
25
+ subcommand = subparts[0].lower() if subparts else ""
26
+ subargs = subparts[1] if len(subparts) > 1 else ""
27
+
28
+ if subcommand == "" or subcommand == "wizard":
29
+ # Show interactive wizard (default)
30
+ _show_registry_wizard()
31
+
32
+ elif subcommand == "list":
33
+ # List components
34
+ component_type = subargs if subargs else None
35
+ if component_type and component_type not in ["skills", "rules", "agents", "verifiers"]:
36
+ console.print(f"[yellow]Unknown type: {component_type}[/yellow]")
37
+ console.print("[dim]Types: skills, rules, agents, verifiers[/dim]")
38
+ return
39
+ # Invoke click command
40
+ registry_list.callback(component_type)
41
+
42
+ elif subcommand == "show":
43
+ if not subargs:
44
+ console.print("[yellow]Usage: /registry show type:name[/yellow]")
45
+ console.print("[dim]Example: /registry show skill:frontend-design[/dim]")
46
+ return
47
+ registry_show.callback(subargs)
48
+
49
+ elif subcommand == "install":
50
+ if not subargs:
51
+ console.print("[yellow]Usage: /registry install type:name [type:name ...][/yellow]")
52
+ console.print("[dim]Example: /registry install skill:frontend-design rule:typescript[/dim]")
53
+ return
54
+ component_ids = tuple(subargs.split())
55
+ registry_install.callback(component_ids)
56
+
57
+ elif subcommand == "search":
58
+ if not subargs:
59
+ console.print("[yellow]Usage: /registry search query[/yellow]")
60
+ console.print("[dim]Example: /registry search frontend[/dim]")
61
+ return
62
+ # Simple search without tag filtering from slash command
63
+ registry_search.callback(subargs, ())
64
+
65
+ else:
66
+ console.print(f"[yellow]Unknown subcommand: {subcommand}[/yellow]")
67
+ console.print("[dim]Usage: /registry [list|show|install|search][/dim]")
68
+ console.print("[dim] /registry - Interactive wizard[/dim]")
69
+ console.print("[dim] /registry list - List all components[/dim]")
70
+ console.print("[dim] /registry show x:y - Show component details[/dim]")
71
+ console.print("[dim] /registry install x:y - Install components[/dim]")
72
+ console.print("[dim] /registry search q - Search registry[/dim]")
@@ -5,6 +5,15 @@ from pathlib import Path
5
5
  from rich.console import Console
6
6
  from rich.panel import Panel
7
7
 
8
+ from ....design import (
9
+ Colors,
10
+ header,
11
+ footer,
12
+ SEPARATOR_WIDTH,
13
+ STATUS_ACTIVE,
14
+ ARROW_PROMPT,
15
+ )
16
+
8
17
  console = Console()
9
18
 
10
19
 
@@ -126,41 +135,41 @@ def show_rules_interactive_menu() -> tuple[str, str]:
126
135
  event.app.exit()
127
136
 
128
137
  def get_formatted_menu():
129
- lines = [("class:title", "Rules\n\n")]
138
+ lines = [("class:title", f"─── Rules {'─' * 35}\n\n")]
130
139
 
131
140
  if not rules:
132
- lines.append(("class:dim", "No rules defined yet.\n\n"))
141
+ lines.append(("class:dim", " No rules defined yet.\n\n"))
133
142
 
134
143
  for i, (name, preview, is_action) in enumerate(menu_items):
135
144
  is_selected = i == selected_index[0]
136
- prefix = " " if is_selected else " "
145
+ prefix = " " if is_selected else " "
137
146
 
138
147
  if is_action:
139
148
  if is_selected:
140
- lines.append(("class:action-selected", f"{prefix}{name}\n"))
149
+ lines.append(("class:action-selected", f" {prefix}{name}\n"))
141
150
  else:
142
- lines.append(("class:action", f"{prefix}{name}\n"))
151
+ lines.append(("class:action", f" {prefix}{name}\n"))
143
152
  else:
144
153
  if is_selected:
145
- lines.append(("class:rule-selected", f"{prefix}{name}"))
146
- lines.append(("class:preview-selected", f" - {preview}\n"))
154
+ lines.append(("class:rule-selected", f" {prefix}{name}"))
155
+ lines.append(("class:preview-selected", f" {preview}\n"))
147
156
  else:
148
- lines.append(("class:rule", f"{prefix}{name}"))
149
- lines.append(("class:preview", f" - {preview}\n"))
157
+ lines.append(("class:rule", f" {prefix}{name}"))
158
+ lines.append(("class:preview", f" {preview}\n"))
150
159
 
151
- lines.append(("class:hint", "\n↑/↓ navigate Enter viewn newd deleteq quit"))
160
+ lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Enter view n new d delete q quit"))
152
161
  return lines
153
162
 
154
163
  style = Style.from_dict({
155
- "title": "#00ccff bold",
156
- "dim": "#666666",
157
- "rule": "#00ccff",
158
- "rule-selected": "#00cc66 bold",
159
- "action": "#ffcc00",
160
- "action-selected": "#ffcc00 bold",
161
- "preview": "#666666",
162
- "preview-selected": "#00cc66",
163
- "hint": "#888888 italic",
164
+ "title": f"{Colors.MUTED}",
165
+ "dim": f"{Colors.DIM}",
166
+ "rule": f"{Colors.PRIMARY}",
167
+ "rule-selected": f"{Colors.SUCCESS} bold",
168
+ "action": f"{Colors.WARNING}",
169
+ "action-selected": f"{Colors.WARNING} bold",
170
+ "preview": f"{Colors.DIM}",
171
+ "preview-selected": f"{Colors.SUCCESS}",
172
+ "hint": f"{Colors.DIM}",
164
173
  })
165
174
 
166
175
  height = len(menu_items) + 5 # items + title + hint + padding
@@ -200,22 +209,29 @@ def show_rule_details(name: str) -> None:
200
209
  rule_file = rules_dir / f"{name}.md"
201
210
 
202
211
  console.print()
203
- console.print("[dim]─" * 50 + "[/dim]")
212
+ console.print(f"[{Colors.MUTED}]{header(name, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
204
213
  console.print()
205
214
 
206
215
  if rule_file.exists():
207
216
  try:
208
217
  content = rule_file.read_text()
209
- console.print(f"[bold cyan]{name}[/bold cyan]\n")
210
- console.print(f"[bold]File:[/bold] {rule_file}\n")
211
- console.print(Panel(content, border_style="dim"))
218
+ console.print(f" [{Colors.DIM}]file[/{Colors.DIM}] {rule_file}")
219
+ console.print()
220
+
221
+ # Show content with indentation
222
+ for line in content.split('\n'):
223
+ if line.startswith('#'):
224
+ console.print(f" [{Colors.PRIMARY}]{line}[/{Colors.PRIMARY}]")
225
+ else:
226
+ console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
227
+
212
228
  except Exception as e:
213
- console.print(f"[red]Error reading rule: {e}[/red]")
229
+ console.print(f" [{Colors.ERROR}]Error reading rule: {e}[/{Colors.ERROR}]")
214
230
  else:
215
- console.print(f"[yellow]Rule '{name}' not found[/yellow]")
231
+ console.print(f" [{Colors.WARNING}]Rule '{name}' not found[/{Colors.WARNING}]")
216
232
 
217
233
  console.print()
218
- console.print("[dim]─" * 50 + "[/dim]")
234
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
219
235
 
220
236
 
221
237
  def confirm_delete(rule_name: str) -> bool:
@@ -260,13 +276,14 @@ def chat_create_rule(client, renderer, model, max_iterations, render_with_interr
260
276
  rules_dir = get_rules_dir()
261
277
 
262
278
  console.print()
263
- console.print("[bold cyan]Create New Rule[/bold cyan]")
264
- console.print("[dim]Describe what rule you want to create. The AI will help you write it.[/dim]")
265
- console.print("[dim]Type 'done' to finish, Ctrl+C to cancel[/dim]")
279
+ console.print(f"[{Colors.MUTED}]{header('Create Rule', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
280
+ console.print()
281
+ console.print(f" [{Colors.DIM}]Describe your rule. AI will help write it.[/{Colors.DIM}]")
282
+ console.print(f" [{Colors.DIM}]Type 'done' to finish.[/{Colors.DIM}]")
266
283
  console.print()
267
284
 
268
285
  chat_style = Style.from_dict({
269
- "prompt": "#00cc66 bold",
286
+ "prompt": f"{Colors.PRIMARY} bold",
270
287
  })
271
288
 
272
289
  ps = PromptSession(style=chat_style)
@@ -379,7 +396,7 @@ def handle_rules(args: str, client, renderer, model, max_iterations, render_with
379
396
  show_rule_details(rule_name)
380
397
  # After viewing, show options
381
398
  try:
382
- console.print("[dim]'d' delete • Enter back[/dim]", end="")
399
+ console.print("[red]'d'[/red] delete • [dim]Enter back[/dim]", end="")
383
400
  ps = PromptSession()
384
401
  resp = ps.prompt(" ").strip().lower()
385
402
  if resp == 'd':
@@ -30,7 +30,7 @@ def handle_session(
30
30
  current_mode_ref: Reference to current_mode (list wrapper for mutation)
31
31
  loaded_messages_ref: Reference to loaded_messages (list wrapper for mutation)
32
32
  """
33
- from ...session_store import SessionStore
33
+ from ....session_store import SessionStore
34
34
 
35
35
  store = SessionStore(Path.cwd())
36
36
 
@@ -100,81 +100,214 @@ When this skill is invoked, you should:
100
100
 
101
101
  def show_setup_menu() -> SetupMode | None:
102
102
  """Show the main setup menu and return selected mode."""
103
- console.print()
104
- console.print(Panel(
105
- "[bold cyan]Emdash Setup Wizard[/bold cyan]\n\n"
106
- "Configure your project's rules, agents, skills, and verifiers.",
107
- border_style="cyan",
108
- ))
109
- console.print()
103
+ from prompt_toolkit import Application
104
+ from prompt_toolkit.key_binding import KeyBindings
105
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
106
+ from prompt_toolkit.styles import Style
110
107
 
111
108
  options = [
112
109
  (SetupMode.RULES, "Rules", "Define coding standards and guidelines for the agent"),
113
110
  (SetupMode.AGENTS, "Agents", "Create custom agents with specialized prompts"),
114
111
  (SetupMode.SKILLS, "Skills", "Add reusable skills invokable via slash commands"),
115
112
  (SetupMode.VERIFIERS, "Verifiers", "Set up verification checks for your work"),
113
+ (None, "Quit", "Exit setup wizard"),
116
114
  ]
117
115
 
118
- for i, (mode, name, desc) in enumerate(options, 1):
119
- console.print(f" [cyan]{i}[/cyan]. [bold]{name}[/bold]")
120
- console.print(f" [dim]{desc}[/dim]")
121
- console.print()
116
+ selected_index = [0]
117
+ result = [None]
118
+
119
+ kb = KeyBindings()
120
+
121
+ @kb.add("up")
122
+ @kb.add("k")
123
+ def move_up(event):
124
+ selected_index[0] = (selected_index[0] - 1) % len(options)
125
+
126
+ @kb.add("down")
127
+ @kb.add("j")
128
+ def move_down(event):
129
+ selected_index[0] = (selected_index[0] + 1) % len(options)
130
+
131
+ @kb.add("enter")
132
+ def select(event):
133
+ result[0] = options[selected_index[0]][0]
134
+ event.app.exit()
135
+
136
+ @kb.add("c-c")
137
+ @kb.add("escape")
138
+ @kb.add("q")
139
+ def cancel(event):
140
+ result[0] = None
141
+ event.app.exit()
142
+
143
+ def get_formatted_menu():
144
+ lines = [
145
+ ("class:title", "Emdash Setup Wizard\n"),
146
+ ("class:subtitle", "Configure your project's rules, agents, skills, and verifiers.\n\n"),
147
+ ]
148
+
149
+ for i, (mode, name, desc) in enumerate(options):
150
+ is_selected = i == selected_index[0]
151
+ prefix = "❯ " if is_selected else " "
152
+
153
+ if mode is None: # Quit option
154
+ if is_selected:
155
+ lines.append(("class:quit-selected", f"{prefix}{name}\n"))
156
+ else:
157
+ lines.append(("class:quit", f"{prefix}{name}\n"))
158
+ else:
159
+ if is_selected:
160
+ lines.append(("class:item-selected", f"{prefix}{name}"))
161
+ lines.append(("class:desc-selected", f" {desc}\n"))
162
+ else:
163
+ lines.append(("class:item", f"{prefix}{name}"))
164
+ lines.append(("class:desc", f" {desc}\n"))
165
+
166
+ lines.append(("class:hint", "\n↑/↓ navigate • Enter select • q quit"))
167
+ return lines
168
+
169
+ style = Style.from_dict({
170
+ "title": "#00ccff bold",
171
+ "subtitle": "#888888",
172
+ "item": "#00ccff",
173
+ "item-selected": "#00cc66 bold",
174
+ "desc": "#666666",
175
+ "desc-selected": "#00cc66",
176
+ "quit": "#888888",
177
+ "quit-selected": "#ff6666 bold",
178
+ "hint": "#888888 italic",
179
+ })
180
+
181
+ layout = Layout(
182
+ HSplit([
183
+ Window(
184
+ FormattedTextControl(get_formatted_menu),
185
+ height=len(options) + 5,
186
+ ),
187
+ ])
188
+ )
189
+
190
+ app = Application(
191
+ layout=layout,
192
+ key_bindings=kb,
193
+ style=style,
194
+ full_screen=False,
195
+ )
122
196
 
123
- console.print(" [dim]q[/dim]. Quit setup wizard")
124
197
  console.print()
125
198
 
126
199
  try:
127
- ps = PromptSession()
128
- choice = ps.prompt("Select [1-4, q]: ").strip().lower()
129
-
130
- if choice == 'q' or choice == 'quit':
131
- return None
132
- elif choice == '1':
133
- return SetupMode.RULES
134
- elif choice == '2':
135
- return SetupMode.AGENTS
136
- elif choice == '3':
137
- return SetupMode.SKILLS
138
- elif choice == '4':
139
- return SetupMode.VERIFIERS
140
- else:
141
- console.print("[yellow]Invalid choice[/yellow]")
142
- return show_setup_menu()
200
+ app.run()
143
201
  except (KeyboardInterrupt, EOFError):
144
- return None
202
+ result[0] = None
203
+
204
+ console.print()
205
+ return result[0]
145
206
 
146
207
 
147
208
  def show_action_menu(mode: SetupMode) -> str | None:
148
209
  """Show action menu for a mode (add/edit/list/delete)."""
149
- console.print()
150
- console.print(f"[bold cyan]{mode.value.title()} Configuration[/bold cyan]")
151
- console.print()
152
- console.print(" [cyan]1[/cyan]. Add new")
153
- console.print(" [cyan]2[/cyan]. Edit existing")
154
- console.print(" [cyan]3[/cyan]. List all")
155
- console.print(" [cyan]4[/cyan]. Delete")
156
- console.print(" [dim]b[/dim]. Back to main menu")
210
+ from prompt_toolkit import Application
211
+ from prompt_toolkit.key_binding import KeyBindings
212
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
213
+ from prompt_toolkit.styles import Style
214
+
215
+ options = [
216
+ ("add", "Add new", f"Create a new {mode.value[:-1]}"),
217
+ ("edit", "Edit existing", f"Modify an existing {mode.value[:-1]}"),
218
+ ("list", "List all", f"Show all configured {mode.value}"),
219
+ ("delete", "Delete", f"Remove a {mode.value[:-1]}"),
220
+ ("back", "Back", "Return to main menu"),
221
+ ]
222
+
223
+ selected_index = [0]
224
+ result = [None]
225
+
226
+ kb = KeyBindings()
227
+
228
+ @kb.add("up")
229
+ @kb.add("k")
230
+ def move_up(event):
231
+ selected_index[0] = (selected_index[0] - 1) % len(options)
232
+
233
+ @kb.add("down")
234
+ @kb.add("j")
235
+ def move_down(event):
236
+ selected_index[0] = (selected_index[0] + 1) % len(options)
237
+
238
+ @kb.add("enter")
239
+ def select(event):
240
+ result[0] = options[selected_index[0]][0]
241
+ event.app.exit()
242
+
243
+ @kb.add("c-c")
244
+ @kb.add("escape")
245
+ @kb.add("b")
246
+ def go_back(event):
247
+ result[0] = "back"
248
+ event.app.exit()
249
+
250
+ def get_formatted_menu():
251
+ lines = [
252
+ ("class:title", f"{mode.value.title()} Configuration\n\n"),
253
+ ]
254
+
255
+ for i, (action, name, desc) in enumerate(options):
256
+ is_selected = i == selected_index[0]
257
+ prefix = "❯ " if is_selected else " "
258
+
259
+ if action == "back":
260
+ if is_selected:
261
+ lines.append(("class:back-selected", f"{prefix}{name}\n"))
262
+ else:
263
+ lines.append(("class:back", f"{prefix}{name}\n"))
264
+ else:
265
+ if is_selected:
266
+ lines.append(("class:item-selected", f"{prefix}{name}"))
267
+ lines.append(("class:desc-selected", f" {desc}\n"))
268
+ else:
269
+ lines.append(("class:item", f"{prefix}{name}"))
270
+ lines.append(("class:desc", f" {desc}\n"))
271
+
272
+ lines.append(("class:hint", "\n↑/↓ navigate • Enter select • b back"))
273
+ return lines
274
+
275
+ style = Style.from_dict({
276
+ "title": "#00ccff bold",
277
+ "item": "#00ccff",
278
+ "item-selected": "#00cc66 bold",
279
+ "desc": "#666666",
280
+ "desc-selected": "#00cc66",
281
+ "back": "#888888",
282
+ "back-selected": "#ffcc00 bold",
283
+ "hint": "#888888 italic",
284
+ })
285
+
286
+ layout = Layout(
287
+ HSplit([
288
+ Window(
289
+ FormattedTextControl(get_formatted_menu),
290
+ height=len(options) + 4,
291
+ ),
292
+ ])
293
+ )
294
+
295
+ app = Application(
296
+ layout=layout,
297
+ key_bindings=kb,
298
+ style=style,
299
+ full_screen=False,
300
+ )
301
+
157
302
  console.print()
158
303
 
159
304
  try:
160
- ps = PromptSession()
161
- choice = ps.prompt("Select [1-4, b]: ").strip().lower()
162
-
163
- if choice == 'b' or choice == 'back':
164
- return 'back'
165
- elif choice == '1':
166
- return 'add'
167
- elif choice == '2':
168
- return 'edit'
169
- elif choice == '3':
170
- return 'list'
171
- elif choice == '4':
172
- return 'delete'
173
- else:
174
- console.print("[yellow]Invalid choice[/yellow]")
175
- return show_action_menu(mode)
305
+ app.run()
176
306
  except (KeyboardInterrupt, EOFError):
177
- return None
307
+ result[0] = "back"
308
+
309
+ console.print()
310
+ return result[0]
178
311
 
179
312
 
180
313
  def get_existing_items(mode: SetupMode) -> list[str]:
@@ -26,7 +26,7 @@ def list_skills() -> list[dict]:
26
26
  """List all skills (both user and built-in).
27
27
 
28
28
  Returns:
29
- List of dicts with name, description, user_invocable, is_builtin, file_path
29
+ List of dicts with name, description, user_invocable, is_builtin, file_path, scripts
30
30
  """
31
31
  from emdash_core.agent.skills import SkillRegistry
32
32
 
@@ -44,6 +44,7 @@ def list_skills() -> list[dict]:
44
44
  "user_invocable": skill.user_invocable,
45
45
  "is_builtin": getattr(skill, "_builtin", False),
46
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 [],
47
48
  })
48
49
 
49
50
  return skills
@@ -71,7 +72,8 @@ def show_skills_interactive_menu() -> tuple[str, str]:
71
72
 
72
73
  for skill in skills:
73
74
  builtin_marker = " [built-in]" if skill["is_builtin"] else ""
74
- menu_items.append((skill["name"], skill["description"] + builtin_marker, skill["is_builtin"], False))
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))
75
77
 
76
78
  # Add action items at the bottom
77
79
  menu_items.append(("+ Create New Skill", "Create a new skill with AI assistance", False, True))
@@ -220,6 +222,15 @@ def show_skill_details(name: str) -> None:
220
222
  console.print(f"[bold]Description:[/bold] {skill.description}")
221
223
  console.print(f"[bold]User Invocable:[/bold] {invocable}")
222
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
+
223
234
  console.print(f"[bold]File:[/bold] {skill.file_path}\n")
224
235
  console.print("[bold]Instructions:[/bold]")
225
236
  console.print(Panel(skill.instructions, border_style="dim"))
@@ -332,6 +343,10 @@ tools: [tool1, tool2]
332
343
  # Skill Title
333
344
 
334
345
  Instructions for the skill...
346
+
347
+ ## Scripts (optional)
348
+
349
+ If scripts are included, document them here.
335
350
  ```
336
351
 
337
352
  **Frontmatter fields:**
@@ -340,9 +355,32 @@ Instructions for the skill...
340
355
  - `user_invocable`: Whether skill can be invoked with /name (true/false)
341
356
  - `tools`: List of tools this skill needs (optional)
342
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
+
343
373
  **My request:** {user_input}
344
374
 
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`."""
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."""
346
384
  stream = client.agent_chat_stream(
347
385
  message=message_with_context,
348
386
  model=model,
@@ -425,7 +463,7 @@ def handle_skills(args: str, client, renderer, model, max_iterations, render_wit
425
463
  if is_builtin:
426
464
  console.print("[dim]Enter to go back[/dim]", end="")
427
465
  else:
428
- console.print("[dim]'d' delete | Enter back[/dim]", end="")
466
+ console.print("[red]'d'[/red] delete [dim]Enter back[/dim]", end="")
429
467
  ps = PromptSession()
430
468
  resp = ps.prompt(" ").strip().lower()
431
469
  if resp == 'd' and not is_builtin: