emdash-cli 0.1.30__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 (32) hide show
  1. emdash_cli/__init__.py +15 -0
  2. emdash_cli/client.py +156 -0
  3. emdash_cli/clipboard.py +30 -61
  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 +53 -0
  7. emdash_cli/commands/agent/file_utils.py +178 -0
  8. emdash_cli/commands/agent/handlers/__init__.py +41 -0
  9. emdash_cli/commands/agent/handlers/agents.py +421 -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/mcp.py +183 -0
  14. emdash_cli/commands/agent/handlers/misc.py +200 -0
  15. emdash_cli/commands/agent/handlers/rules.py +394 -0
  16. emdash_cli/commands/agent/handlers/sessions.py +168 -0
  17. emdash_cli/commands/agent/handlers/setup.py +582 -0
  18. emdash_cli/commands/agent/handlers/skills.py +440 -0
  19. emdash_cli/commands/agent/handlers/todos.py +98 -0
  20. emdash_cli/commands/agent/handlers/verify.py +648 -0
  21. emdash_cli/commands/agent/interactive.py +657 -0
  22. emdash_cli/commands/agent/menus.py +728 -0
  23. emdash_cli/commands/agent.py +7 -856
  24. emdash_cli/commands/server.py +99 -40
  25. emdash_cli/server_manager.py +70 -10
  26. emdash_cli/session_store.py +321 -0
  27. emdash_cli/sse_renderer.py +256 -110
  28. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/METADATA +2 -4
  29. emdash_cli-0.1.46.dist-info/RECORD +49 -0
  30. emdash_cli-0.1.30.dist-info/RECORD +0 -29
  31. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/WHEEL +0 -0
  32. {emdash_cli-0.1.30.dist-info → emdash_cli-0.1.46.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,582 @@
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
+ 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()
110
+
111
+ options = [
112
+ (SetupMode.RULES, "Rules", "Define coding standards and guidelines for the agent"),
113
+ (SetupMode.AGENTS, "Agents", "Create custom agents with specialized prompts"),
114
+ (SetupMode.SKILLS, "Skills", "Add reusable skills invokable via slash commands"),
115
+ (SetupMode.VERIFIERS, "Verifiers", "Set up verification checks for your work"),
116
+ ]
117
+
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()
122
+
123
+ console.print(" [dim]q[/dim]. Quit setup wizard")
124
+ console.print()
125
+
126
+ 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()
143
+ except (KeyboardInterrupt, EOFError):
144
+ return None
145
+
146
+
147
+ def show_action_menu(mode: SetupMode) -> str | None:
148
+ """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")
157
+ console.print()
158
+
159
+ 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)
176
+ except (KeyboardInterrupt, EOFError):
177
+ return None
178
+
179
+
180
+ def get_existing_items(mode: SetupMode) -> list[str]:
181
+ """Get list of existing items for a mode."""
182
+ cwd = Path.cwd()
183
+
184
+ if mode == SetupMode.RULES:
185
+ rules_file = cwd / ".emdash" / "rules.md"
186
+ return ["rules.md"] if rules_file.exists() else []
187
+
188
+ elif mode == SetupMode.AGENTS:
189
+ agents_dir = cwd / ".emdash" / "agents"
190
+ if agents_dir.exists():
191
+ return [f.stem for f in agents_dir.glob("*.md")]
192
+ return []
193
+
194
+ elif mode == SetupMode.SKILLS:
195
+ skills_dir = cwd / ".emdash" / "skills"
196
+ if skills_dir.exists():
197
+ return [f.stem for f in skills_dir.glob("*.md")]
198
+ return []
199
+
200
+ elif mode == SetupMode.VERIFIERS:
201
+ verifiers_file = cwd / ".emdash" / "verifiers.json"
202
+ if verifiers_file.exists():
203
+ try:
204
+ data = json.loads(verifiers_file.read_text())
205
+ return [v.get("name", "unnamed") for v in data.get("verifiers", [])]
206
+ except Exception:
207
+ pass
208
+ return []
209
+
210
+ return []
211
+
212
+
213
+ def list_items(mode: SetupMode) -> None:
214
+ """List existing items for a mode."""
215
+ items = get_existing_items(mode)
216
+
217
+ console.print()
218
+ if not items:
219
+ console.print(f"[yellow]No {mode.value} configured yet.[/yellow]")
220
+ else:
221
+ console.print(f"[bold]Existing {mode.value}:[/bold]")
222
+ for item in items:
223
+ console.print(f" • {item}")
224
+ console.print()
225
+
226
+
227
+ def run_ai_assisted_setup(
228
+ mode: SetupMode,
229
+ action: str,
230
+ client,
231
+ renderer,
232
+ model: str,
233
+ item_name: str | None = None,
234
+ ) -> bool:
235
+ """Run AI-assisted setup flow for creating/editing config.
236
+
237
+ Args:
238
+ mode: The setup mode (rules, agents, skills, verifiers)
239
+ action: The action (add, edit)
240
+ client: EmDash client
241
+ renderer: SSE renderer
242
+ model: Model to use
243
+ item_name: Name of item to edit (for edit action)
244
+
245
+ Returns:
246
+ True if successful, False otherwise
247
+ """
248
+ cwd = Path.cwd()
249
+ template = TEMPLATES[mode]
250
+
251
+ # Build the system context for the AI
252
+ if mode == SetupMode.RULES:
253
+ target_file = cwd / ".emdash" / "rules.md"
254
+ current_content = target_file.read_text() if target_file.exists() else None
255
+ file_info = f"File: `{target_file}`"
256
+
257
+ elif mode == SetupMode.AGENTS:
258
+ if action == 'add':
259
+ # Prompt for agent name
260
+ ps = PromptSession()
261
+ console.print()
262
+ item_name = ps.prompt("Agent name: ").strip()
263
+ if not item_name:
264
+ console.print("[yellow]Agent name is required[/yellow]")
265
+ return False
266
+
267
+ target_file = cwd / ".emdash" / "agents" / f"{item_name}.md"
268
+ current_content = target_file.read_text() if target_file.exists() else None
269
+ file_info = f"File: `{target_file}`"
270
+
271
+ elif mode == SetupMode.SKILLS:
272
+ if action == 'add':
273
+ ps = PromptSession()
274
+ console.print()
275
+ item_name = ps.prompt("Skill name: ").strip()
276
+ if not item_name:
277
+ console.print("[yellow]Skill name is required[/yellow]")
278
+ return False
279
+
280
+ target_file = cwd / ".emdash" / "skills" / f"{item_name}.md"
281
+ current_content = target_file.read_text() if target_file.exists() else None
282
+ file_info = f"File: `{target_file}`"
283
+
284
+ elif mode == SetupMode.VERIFIERS:
285
+ target_file = cwd / ".emdash" / "verifiers.json"
286
+ current_content = target_file.read_text() if target_file.exists() else None
287
+ file_info = f"File: `{target_file}`"
288
+
289
+ # Build initial message for AI
290
+ example = template["example"]
291
+ if isinstance(example, dict):
292
+ example = json.dumps(example, indent=2)
293
+
294
+ if action == 'add' and current_content:
295
+ action_desc = "add to or modify"
296
+ elif action == 'add':
297
+ action_desc = "create"
298
+ else:
299
+ action_desc = "modify"
300
+
301
+ initial_message = f"""I want to {action_desc} my {mode.value} configuration.
302
+
303
+ {file_info}
304
+
305
+ **What {mode.value} do:** {template['description']}
306
+
307
+ **Example format:**
308
+ ```
309
+ {example}
310
+ ```
311
+ """
312
+
313
+ if current_content:
314
+ initial_message += f"""
315
+ **Current content:**
316
+ ```
317
+ {current_content}
318
+ ```
319
+ """
320
+
321
+ initial_message += """
322
+ Help me configure this. Ask me what I want to achieve, then create/update the file using the Edit or Write tool.
323
+
324
+ IMPORTANT: You have permission to write to the .emdash/ directory. Use the Write tool to create the file."""
325
+
326
+ # Run interactive AI session
327
+ console.print()
328
+ console.print(Panel(
329
+ f"[bold cyan]AI-Assisted {mode.value.title()} Setup[/bold cyan]\n\n"
330
+ f"Chat with the AI to configure your {mode.value}.\n"
331
+ "Type [bold]done[/bold] when finished, [bold]cancel[/bold] to abort.",
332
+ border_style="cyan",
333
+ ))
334
+ console.print()
335
+
336
+ # Start the AI conversation
337
+ session_id = None
338
+ ps = PromptSession(history=InMemoryHistory())
339
+
340
+ # Send initial message
341
+ try:
342
+ stream = client.agent_chat_stream(
343
+ message=initial_message,
344
+ model=model,
345
+ max_iterations=10,
346
+ options={"mode": "code"},
347
+ )
348
+
349
+ # Import render function
350
+ from . import render_with_interrupt
351
+ result = render_with_interrupt(renderer, stream)
352
+
353
+ if result and result.get("session_id"):
354
+ session_id = result["session_id"]
355
+
356
+ except Exception as e:
357
+ console.print(f"[red]Error: {e}[/red]")
358
+ return False
359
+
360
+ # Interactive loop
361
+ while True:
362
+ try:
363
+ console.print()
364
+ user_input = ps.prompt("[setup] > ").strip()
365
+
366
+ if not user_input:
367
+ continue
368
+
369
+ if user_input.lower() in ('done', 'finish', 'exit'):
370
+ console.print()
371
+ console.print("[green]Setup complete![/green]")
372
+ return True
373
+
374
+ if user_input.lower() in ('cancel', 'abort', 'quit'):
375
+ console.print()
376
+ console.print("[yellow]Setup cancelled.[/yellow]")
377
+ return False
378
+
379
+ # Continue the conversation
380
+ if session_id:
381
+ stream = client.agent_continue_stream(session_id, user_input)
382
+ else:
383
+ stream = client.agent_chat_stream(
384
+ message=user_input,
385
+ model=model,
386
+ max_iterations=10,
387
+ options={"mode": "code"},
388
+ )
389
+
390
+ result = render_with_interrupt(renderer, stream)
391
+
392
+ if result and result.get("session_id"):
393
+ session_id = result["session_id"]
394
+
395
+ except KeyboardInterrupt:
396
+ console.print()
397
+ console.print("[yellow]Setup interrupted.[/yellow]")
398
+ return False
399
+ except EOFError:
400
+ break
401
+
402
+ return True
403
+
404
+
405
+ def select_item_to_edit(mode: SetupMode) -> str | None:
406
+ """Let user select an item to edit."""
407
+ items = get_existing_items(mode)
408
+
409
+ if not items:
410
+ console.print(f"[yellow]No {mode.value} to edit. Create one first.[/yellow]")
411
+ return None
412
+
413
+ console.print()
414
+ console.print(f"[bold]Select {mode.value[:-1]} to edit:[/bold]")
415
+ for i, item in enumerate(items, 1):
416
+ console.print(f" [cyan]{i}[/cyan]. {item}")
417
+ console.print()
418
+
419
+ try:
420
+ ps = PromptSession()
421
+ choice = ps.prompt(f"Select [1-{len(items)}]: ").strip()
422
+
423
+ idx = int(choice) - 1
424
+ if 0 <= idx < len(items):
425
+ return items[idx]
426
+ else:
427
+ console.print("[yellow]Invalid choice[/yellow]")
428
+ return None
429
+ except (ValueError, KeyboardInterrupt, EOFError):
430
+ return None
431
+
432
+
433
+ def select_item_to_delete(mode: SetupMode) -> str | None:
434
+ """Let user select an item to delete."""
435
+ items = get_existing_items(mode)
436
+
437
+ if not items:
438
+ console.print(f"[yellow]No {mode.value} to delete.[/yellow]")
439
+ return None
440
+
441
+ console.print()
442
+ console.print(f"[bold]Select {mode.value[:-1]} to delete:[/bold]")
443
+ for i, item in enumerate(items, 1):
444
+ console.print(f" [cyan]{i}[/cyan]. {item}")
445
+ console.print()
446
+
447
+ try:
448
+ ps = PromptSession()
449
+ choice = ps.prompt(f"Select [1-{len(items)}]: ").strip()
450
+
451
+ idx = int(choice) - 1
452
+ if 0 <= idx < len(items):
453
+ return items[idx]
454
+ else:
455
+ console.print("[yellow]Invalid choice[/yellow]")
456
+ return None
457
+ except (ValueError, KeyboardInterrupt, EOFError):
458
+ return None
459
+
460
+
461
+ def delete_item(mode: SetupMode, item_name: str) -> bool:
462
+ """Delete an item."""
463
+ cwd = Path.cwd()
464
+
465
+ try:
466
+ if mode == SetupMode.RULES:
467
+ target = cwd / ".emdash" / "rules.md"
468
+ if target.exists():
469
+ target.unlink()
470
+ console.print(f"[green]Deleted rules.md[/green]")
471
+ return True
472
+
473
+ elif mode == SetupMode.AGENTS:
474
+ target = cwd / ".emdash" / "agents" / f"{item_name}.md"
475
+ if target.exists():
476
+ target.unlink()
477
+ console.print(f"[green]Deleted agent: {item_name}[/green]")
478
+ return True
479
+
480
+ elif mode == SetupMode.SKILLS:
481
+ target = cwd / ".emdash" / "skills" / f"{item_name}.md"
482
+ if target.exists():
483
+ target.unlink()
484
+ console.print(f"[green]Deleted skill: {item_name}[/green]")
485
+ return True
486
+
487
+ elif mode == SetupMode.VERIFIERS:
488
+ target = cwd / ".emdash" / "verifiers.json"
489
+ if target.exists():
490
+ data = json.loads(target.read_text())
491
+ data["verifiers"] = [
492
+ v for v in data.get("verifiers", [])
493
+ if v.get("name") != item_name
494
+ ]
495
+ target.write_text(json.dumps(data, indent=2))
496
+ console.print(f"[green]Deleted verifier: {item_name}[/green]")
497
+ return True
498
+
499
+ console.print(f"[yellow]Item not found: {item_name}[/yellow]")
500
+ return False
501
+
502
+ except Exception as e:
503
+ console.print(f"[red]Error deleting: {e}[/red]")
504
+ return False
505
+
506
+
507
+ def render_with_interrupt(renderer, stream) -> dict:
508
+ """Render stream with interrupt support.
509
+
510
+ This is a simplified version for the setup wizard.
511
+ """
512
+ import threading
513
+ from ....keyboard import KeyListener
514
+
515
+ interrupt_event = threading.Event()
516
+
517
+ def on_escape():
518
+ interrupt_event.set()
519
+
520
+ listener = KeyListener(on_escape)
521
+
522
+ try:
523
+ listener.start()
524
+ result = renderer.render_stream(stream, interrupt_event=interrupt_event)
525
+ return result
526
+ finally:
527
+ listener.stop()
528
+
529
+
530
+ def handle_setup(
531
+ args: str,
532
+ client,
533
+ renderer,
534
+ model: str,
535
+ ) -> None:
536
+ """Handle /setup command - open the setup wizard.
537
+
538
+ Args:
539
+ args: Command arguments (unused currently)
540
+ client: EmDash client for AI interactions
541
+ renderer: SSE renderer
542
+ model: Model to use for AI assistance
543
+ """
544
+ console.print()
545
+ console.print("[bold cyan]━━━ Setup Wizard ━━━[/bold cyan]")
546
+
547
+ while True:
548
+ # Show main menu
549
+ mode = show_setup_menu()
550
+ if mode is None:
551
+ console.print()
552
+ console.print("[dim]Exiting setup wizard.[/dim]")
553
+ break
554
+
555
+ # Show action menu
556
+ while True:
557
+ action = show_action_menu(mode)
558
+
559
+ if action is None or action == 'back':
560
+ break
561
+
562
+ if action == 'list':
563
+ list_items(mode)
564
+
565
+ elif action == 'add':
566
+ run_ai_assisted_setup(mode, 'add', client, renderer, model)
567
+
568
+ elif action == 'edit':
569
+ item = select_item_to_edit(mode)
570
+ if item:
571
+ run_ai_assisted_setup(mode, 'edit', client, renderer, model, item)
572
+
573
+ elif action == 'delete':
574
+ item = select_item_to_delete(mode)
575
+ if item:
576
+ # Confirm deletion
577
+ ps = PromptSession()
578
+ confirm = ps.prompt(f"Delete '{item}'? [y/N]: ").strip().lower()
579
+ if confirm in ('y', 'yes'):
580
+ delete_item(mode, item)
581
+
582
+ console.print()