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,715 @@
1
+ """Setup wizard for configuring rules, agents, skills, and verifiers.
2
+
3
+ This is a dedicated flow separate from the main agent interaction,
4
+ specialized for configuration management with its own permissions.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from enum import Enum
10
+
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+ from prompt_toolkit import PromptSession
15
+ from prompt_toolkit.history import InMemoryHistory
16
+
17
+ console = Console()
18
+
19
+
20
+ class SetupMode(Enum):
21
+ """Available setup modes."""
22
+ RULES = "rules"
23
+ AGENTS = "agents"
24
+ SKILLS = "skills"
25
+ VERIFIERS = "verifiers"
26
+
27
+
28
+ # Templates for each config type
29
+ TEMPLATES = {
30
+ SetupMode.RULES: {
31
+ "file": ".emdash/rules.md",
32
+ "example": """# Project Rules
33
+
34
+ ## Code Style
35
+ - Use TypeScript for all new code
36
+ - Follow the existing patterns in the codebase
37
+
38
+ ## Testing
39
+ - Write tests for all new features
40
+ - Maintain >80% code coverage
41
+ """,
42
+ "description": "Rules guide the agent's behavior and coding standards",
43
+ },
44
+ SetupMode.AGENTS: {
45
+ "dir": ".emdash/agents",
46
+ "example": """---
47
+ name: {name}
48
+ description: {description}
49
+ tools:
50
+ - read_file
51
+ - edit_file
52
+ - bash
53
+ ---
54
+
55
+ You are a specialized agent for {purpose}.
56
+
57
+ ## Your Role
58
+ {role_description}
59
+
60
+ ## Guidelines
61
+ - Follow project conventions
62
+ - Be concise and accurate
63
+ """,
64
+ "description": "Custom agents with specialized system prompts and tools",
65
+ },
66
+ SetupMode.SKILLS: {
67
+ "dir": ".emdash/skills",
68
+ "example": """---
69
+ name: {name}
70
+ description: {description}
71
+ ---
72
+
73
+ ## Skill: {name}
74
+
75
+ When this skill is invoked, you should:
76
+
77
+ 1. {step1}
78
+ 2. {step2}
79
+ 3. {step3}
80
+
81
+ ## Output Format
82
+ {output_format}
83
+ """,
84
+ "description": "Reusable skills that can be invoked with /skill-name",
85
+ },
86
+ SetupMode.VERIFIERS: {
87
+ "file": ".emdash/verifiers.json",
88
+ "example": {
89
+ "verifiers": [
90
+ {"type": "command", "name": "tests", "command": "npm test", "timeout": 120},
91
+ {"type": "command", "name": "lint", "command": "npm run lint"},
92
+ {"type": "llm", "name": "review", "prompt": "Review for bugs and issues", "model": "haiku"}
93
+ ],
94
+ "max_retries": 3
95
+ },
96
+ "description": "Verification checks (commands or LLM reviews) to validate work",
97
+ },
98
+ }
99
+
100
+
101
+ def show_setup_menu() -> SetupMode | None:
102
+ """Show the main setup menu and return selected mode."""
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
107
+
108
+ options = [
109
+ (SetupMode.RULES, "Rules", "Define coding standards and guidelines for the agent"),
110
+ (SetupMode.AGENTS, "Agents", "Create custom agents with specialized prompts"),
111
+ (SetupMode.SKILLS, "Skills", "Add reusable skills invokable via slash commands"),
112
+ (SetupMode.VERIFIERS, "Verifiers", "Set up verification checks for your work"),
113
+ (None, "Quit", "Exit setup wizard"),
114
+ ]
115
+
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
+ )
196
+
197
+ console.print()
198
+
199
+ try:
200
+ app.run()
201
+ except (KeyboardInterrupt, EOFError):
202
+ result[0] = None
203
+
204
+ console.print()
205
+ return result[0]
206
+
207
+
208
+ def show_action_menu(mode: SetupMode) -> str | None:
209
+ """Show action menu for a mode (add/edit/list/delete)."""
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
+
302
+ console.print()
303
+
304
+ try:
305
+ app.run()
306
+ except (KeyboardInterrupt, EOFError):
307
+ result[0] = "back"
308
+
309
+ console.print()
310
+ return result[0]
311
+
312
+
313
+ def get_existing_items(mode: SetupMode) -> list[str]:
314
+ """Get list of existing items for a mode."""
315
+ cwd = Path.cwd()
316
+
317
+ if mode == SetupMode.RULES:
318
+ rules_file = cwd / ".emdash" / "rules.md"
319
+ return ["rules.md"] if rules_file.exists() else []
320
+
321
+ elif mode == SetupMode.AGENTS:
322
+ agents_dir = cwd / ".emdash" / "agents"
323
+ if agents_dir.exists():
324
+ return [f.stem for f in agents_dir.glob("*.md")]
325
+ return []
326
+
327
+ elif mode == SetupMode.SKILLS:
328
+ skills_dir = cwd / ".emdash" / "skills"
329
+ if skills_dir.exists():
330
+ return [f.stem for f in skills_dir.glob("*.md")]
331
+ return []
332
+
333
+ elif mode == SetupMode.VERIFIERS:
334
+ verifiers_file = cwd / ".emdash" / "verifiers.json"
335
+ if verifiers_file.exists():
336
+ try:
337
+ data = json.loads(verifiers_file.read_text())
338
+ return [v.get("name", "unnamed") for v in data.get("verifiers", [])]
339
+ except Exception:
340
+ pass
341
+ return []
342
+
343
+ return []
344
+
345
+
346
+ def list_items(mode: SetupMode) -> None:
347
+ """List existing items for a mode."""
348
+ items = get_existing_items(mode)
349
+
350
+ console.print()
351
+ if not items:
352
+ console.print(f"[yellow]No {mode.value} configured yet.[/yellow]")
353
+ else:
354
+ console.print(f"[bold]Existing {mode.value}:[/bold]")
355
+ for item in items:
356
+ console.print(f" • {item}")
357
+ console.print()
358
+
359
+
360
+ def run_ai_assisted_setup(
361
+ mode: SetupMode,
362
+ action: str,
363
+ client,
364
+ renderer,
365
+ model: str,
366
+ item_name: str | None = None,
367
+ ) -> bool:
368
+ """Run AI-assisted setup flow for creating/editing config.
369
+
370
+ Args:
371
+ mode: The setup mode (rules, agents, skills, verifiers)
372
+ action: The action (add, edit)
373
+ client: EmDash client
374
+ renderer: SSE renderer
375
+ model: Model to use
376
+ item_name: Name of item to edit (for edit action)
377
+
378
+ Returns:
379
+ True if successful, False otherwise
380
+ """
381
+ cwd = Path.cwd()
382
+ template = TEMPLATES[mode]
383
+
384
+ # Build the system context for the AI
385
+ if mode == SetupMode.RULES:
386
+ target_file = cwd / ".emdash" / "rules.md"
387
+ current_content = target_file.read_text() if target_file.exists() else None
388
+ file_info = f"File: `{target_file}`"
389
+
390
+ elif mode == SetupMode.AGENTS:
391
+ if action == 'add':
392
+ # Prompt for agent name
393
+ ps = PromptSession()
394
+ console.print()
395
+ item_name = ps.prompt("Agent name: ").strip()
396
+ if not item_name:
397
+ console.print("[yellow]Agent name is required[/yellow]")
398
+ return False
399
+
400
+ target_file = cwd / ".emdash" / "agents" / f"{item_name}.md"
401
+ current_content = target_file.read_text() if target_file.exists() else None
402
+ file_info = f"File: `{target_file}`"
403
+
404
+ elif mode == SetupMode.SKILLS:
405
+ if action == 'add':
406
+ ps = PromptSession()
407
+ console.print()
408
+ item_name = ps.prompt("Skill name: ").strip()
409
+ if not item_name:
410
+ console.print("[yellow]Skill name is required[/yellow]")
411
+ return False
412
+
413
+ target_file = cwd / ".emdash" / "skills" / f"{item_name}.md"
414
+ current_content = target_file.read_text() if target_file.exists() else None
415
+ file_info = f"File: `{target_file}`"
416
+
417
+ elif mode == SetupMode.VERIFIERS:
418
+ target_file = cwd / ".emdash" / "verifiers.json"
419
+ current_content = target_file.read_text() if target_file.exists() else None
420
+ file_info = f"File: `{target_file}`"
421
+
422
+ # Build initial message for AI
423
+ example = template["example"]
424
+ if isinstance(example, dict):
425
+ example = json.dumps(example, indent=2)
426
+
427
+ if action == 'add' and current_content:
428
+ action_desc = "add to or modify"
429
+ elif action == 'add':
430
+ action_desc = "create"
431
+ else:
432
+ action_desc = "modify"
433
+
434
+ initial_message = f"""I want to {action_desc} my {mode.value} configuration.
435
+
436
+ {file_info}
437
+
438
+ **What {mode.value} do:** {template['description']}
439
+
440
+ **Example format:**
441
+ ```
442
+ {example}
443
+ ```
444
+ """
445
+
446
+ if current_content:
447
+ initial_message += f"""
448
+ **Current content:**
449
+ ```
450
+ {current_content}
451
+ ```
452
+ """
453
+
454
+ initial_message += """
455
+ Help me configure this. Ask me what I want to achieve, then create/update the file using the Edit or Write tool.
456
+
457
+ IMPORTANT: You have permission to write to the .emdash/ directory. Use the Write tool to create the file."""
458
+
459
+ # Run interactive AI session
460
+ console.print()
461
+ console.print(Panel(
462
+ f"[bold cyan]AI-Assisted {mode.value.title()} Setup[/bold cyan]\n\n"
463
+ f"Chat with the AI to configure your {mode.value}.\n"
464
+ "Type [bold]done[/bold] when finished, [bold]cancel[/bold] to abort.",
465
+ border_style="cyan",
466
+ ))
467
+ console.print()
468
+
469
+ # Start the AI conversation
470
+ session_id = None
471
+ ps = PromptSession(history=InMemoryHistory())
472
+
473
+ # Send initial message
474
+ try:
475
+ stream = client.agent_chat_stream(
476
+ message=initial_message,
477
+ model=model,
478
+ max_iterations=10,
479
+ options={"mode": "code"},
480
+ )
481
+
482
+ # Import render function
483
+ from . import render_with_interrupt
484
+ result = render_with_interrupt(renderer, stream)
485
+
486
+ if result and result.get("session_id"):
487
+ session_id = result["session_id"]
488
+
489
+ except Exception as e:
490
+ console.print(f"[red]Error: {e}[/red]")
491
+ return False
492
+
493
+ # Interactive loop
494
+ while True:
495
+ try:
496
+ console.print()
497
+ user_input = ps.prompt("[setup] > ").strip()
498
+
499
+ if not user_input:
500
+ continue
501
+
502
+ if user_input.lower() in ('done', 'finish', 'exit'):
503
+ console.print()
504
+ console.print("[green]Setup complete![/green]")
505
+ return True
506
+
507
+ if user_input.lower() in ('cancel', 'abort', 'quit'):
508
+ console.print()
509
+ console.print("[yellow]Setup cancelled.[/yellow]")
510
+ return False
511
+
512
+ # Continue the conversation
513
+ if session_id:
514
+ stream = client.agent_continue_stream(session_id, user_input)
515
+ else:
516
+ stream = client.agent_chat_stream(
517
+ message=user_input,
518
+ model=model,
519
+ max_iterations=10,
520
+ options={"mode": "code"},
521
+ )
522
+
523
+ result = render_with_interrupt(renderer, stream)
524
+
525
+ if result and result.get("session_id"):
526
+ session_id = result["session_id"]
527
+
528
+ except KeyboardInterrupt:
529
+ console.print()
530
+ console.print("[yellow]Setup interrupted.[/yellow]")
531
+ return False
532
+ except EOFError:
533
+ break
534
+
535
+ return True
536
+
537
+
538
+ def select_item_to_edit(mode: SetupMode) -> str | None:
539
+ """Let user select an item to edit."""
540
+ items = get_existing_items(mode)
541
+
542
+ if not items:
543
+ console.print(f"[yellow]No {mode.value} to edit. Create one first.[/yellow]")
544
+ return None
545
+
546
+ console.print()
547
+ console.print(f"[bold]Select {mode.value[:-1]} to edit:[/bold]")
548
+ for i, item in enumerate(items, 1):
549
+ console.print(f" [cyan]{i}[/cyan]. {item}")
550
+ console.print()
551
+
552
+ try:
553
+ ps = PromptSession()
554
+ choice = ps.prompt(f"Select [1-{len(items)}]: ").strip()
555
+
556
+ idx = int(choice) - 1
557
+ if 0 <= idx < len(items):
558
+ return items[idx]
559
+ else:
560
+ console.print("[yellow]Invalid choice[/yellow]")
561
+ return None
562
+ except (ValueError, KeyboardInterrupt, EOFError):
563
+ return None
564
+
565
+
566
+ def select_item_to_delete(mode: SetupMode) -> str | None:
567
+ """Let user select an item to delete."""
568
+ items = get_existing_items(mode)
569
+
570
+ if not items:
571
+ console.print(f"[yellow]No {mode.value} to delete.[/yellow]")
572
+ return None
573
+
574
+ console.print()
575
+ console.print(f"[bold]Select {mode.value[:-1]} to delete:[/bold]")
576
+ for i, item in enumerate(items, 1):
577
+ console.print(f" [cyan]{i}[/cyan]. {item}")
578
+ console.print()
579
+
580
+ try:
581
+ ps = PromptSession()
582
+ choice = ps.prompt(f"Select [1-{len(items)}]: ").strip()
583
+
584
+ idx = int(choice) - 1
585
+ if 0 <= idx < len(items):
586
+ return items[idx]
587
+ else:
588
+ console.print("[yellow]Invalid choice[/yellow]")
589
+ return None
590
+ except (ValueError, KeyboardInterrupt, EOFError):
591
+ return None
592
+
593
+
594
+ def delete_item(mode: SetupMode, item_name: str) -> bool:
595
+ """Delete an item."""
596
+ cwd = Path.cwd()
597
+
598
+ try:
599
+ if mode == SetupMode.RULES:
600
+ target = cwd / ".emdash" / "rules.md"
601
+ if target.exists():
602
+ target.unlink()
603
+ console.print(f"[green]Deleted rules.md[/green]")
604
+ return True
605
+
606
+ elif mode == SetupMode.AGENTS:
607
+ target = cwd / ".emdash" / "agents" / f"{item_name}.md"
608
+ if target.exists():
609
+ target.unlink()
610
+ console.print(f"[green]Deleted agent: {item_name}[/green]")
611
+ return True
612
+
613
+ elif mode == SetupMode.SKILLS:
614
+ target = cwd / ".emdash" / "skills" / f"{item_name}.md"
615
+ if target.exists():
616
+ target.unlink()
617
+ console.print(f"[green]Deleted skill: {item_name}[/green]")
618
+ return True
619
+
620
+ elif mode == SetupMode.VERIFIERS:
621
+ target = cwd / ".emdash" / "verifiers.json"
622
+ if target.exists():
623
+ data = json.loads(target.read_text())
624
+ data["verifiers"] = [
625
+ v for v in data.get("verifiers", [])
626
+ if v.get("name") != item_name
627
+ ]
628
+ target.write_text(json.dumps(data, indent=2))
629
+ console.print(f"[green]Deleted verifier: {item_name}[/green]")
630
+ return True
631
+
632
+ console.print(f"[yellow]Item not found: {item_name}[/yellow]")
633
+ return False
634
+
635
+ except Exception as e:
636
+ console.print(f"[red]Error deleting: {e}[/red]")
637
+ return False
638
+
639
+
640
+ def render_with_interrupt(renderer, stream) -> dict:
641
+ """Render stream with interrupt support.
642
+
643
+ This is a simplified version for the setup wizard.
644
+ """
645
+ import threading
646
+ from ....keyboard import KeyListener
647
+
648
+ interrupt_event = threading.Event()
649
+
650
+ def on_escape():
651
+ interrupt_event.set()
652
+
653
+ listener = KeyListener(on_escape)
654
+
655
+ try:
656
+ listener.start()
657
+ result = renderer.render_stream(stream, interrupt_event=interrupt_event)
658
+ return result
659
+ finally:
660
+ listener.stop()
661
+
662
+
663
+ def handle_setup(
664
+ args: str,
665
+ client,
666
+ renderer,
667
+ model: str,
668
+ ) -> None:
669
+ """Handle /setup command - open the setup wizard.
670
+
671
+ Args:
672
+ args: Command arguments (unused currently)
673
+ client: EmDash client for AI interactions
674
+ renderer: SSE renderer
675
+ model: Model to use for AI assistance
676
+ """
677
+ console.print()
678
+ console.print("[bold cyan]━━━ Setup Wizard ━━━[/bold cyan]")
679
+
680
+ while True:
681
+ # Show main menu
682
+ mode = show_setup_menu()
683
+ if mode is None:
684
+ console.print()
685
+ console.print("[dim]Exiting setup wizard.[/dim]")
686
+ break
687
+
688
+ # Show action menu
689
+ while True:
690
+ action = show_action_menu(mode)
691
+
692
+ if action is None or action == 'back':
693
+ break
694
+
695
+ if action == 'list':
696
+ list_items(mode)
697
+
698
+ elif action == 'add':
699
+ run_ai_assisted_setup(mode, 'add', client, renderer, model)
700
+
701
+ elif action == 'edit':
702
+ item = select_item_to_edit(mode)
703
+ if item:
704
+ run_ai_assisted_setup(mode, 'edit', client, renderer, model, item)
705
+
706
+ elif action == 'delete':
707
+ item = select_item_to_delete(mode)
708
+ if item:
709
+ # Confirm deletion
710
+ ps = PromptSession()
711
+ confirm = ps.prompt(f"Delete '{item}'? [y/N]: ").strip().lower()
712
+ if confirm in ('y', 'yes'):
713
+ delete_item(mode, item)
714
+
715
+ console.print()