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,619 @@
1
+ """First-run onboarding wizard for emdash CLI.
2
+
3
+ Provides an animated, guided setup experience for new users with zen styling.
4
+ """
5
+
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+ from rich.live import Live
12
+ from rich.text import Text
13
+ from prompt_toolkit import PromptSession
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
16
+ from prompt_toolkit.styles import Style
17
+ from prompt_toolkit import Application
18
+
19
+ from ...design import (
20
+ Colors,
21
+ ANSI,
22
+ STATUS_ACTIVE,
23
+ STATUS_INACTIVE,
24
+ STATUS_ERROR,
25
+ DOT_BULLET,
26
+ DOT_WAITING,
27
+ DOT_ACTIVE,
28
+ ARROW_PROMPT,
29
+ ARROW_RIGHT,
30
+ EM_DASH,
31
+ header,
32
+ footer,
33
+ step_progress,
34
+ SEPARATOR_WIDTH,
35
+ SPINNER_FRAMES,
36
+ LOGO,
37
+ )
38
+
39
+ console = Console()
40
+
41
+
42
+ # ─────────────────────────────────────────────────────────────────────────────
43
+ # Animation Utilities
44
+ # ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ def typewriter(text: str, delay: float = 0.02, style: str = "") -> None:
47
+ """Print text with typewriter animation."""
48
+ for char in text:
49
+ if style:
50
+ console.print(char, end="", style=style)
51
+ else:
52
+ console.print(char, end="")
53
+ sys.stdout.flush()
54
+ time.sleep(delay)
55
+ console.print() # newline
56
+
57
+
58
+ def animate_line(text: str, style: str = "", delay: float = 0.03) -> None:
59
+ """Animate a single line appearing character by character."""
60
+ for i in range(len(text) + 1):
61
+ sys.stdout.write(f"\r{text[:i]}")
62
+ sys.stdout.flush()
63
+ time.sleep(delay)
64
+ sys.stdout.write("\n")
65
+ sys.stdout.flush()
66
+
67
+
68
+ def animate_dots(message: str, duration: float = 1.0, style: str = "") -> None:
69
+ """Show animated dots for a duration."""
70
+ frames = len(SPINNER_FRAMES)
71
+ start = time.time()
72
+ i = 0
73
+ while time.time() - start < duration:
74
+ spinner = SPINNER_FRAMES[i % frames]
75
+ sys.stdout.write(f"\r {spinner} {message}")
76
+ sys.stdout.flush()
77
+ time.sleep(0.1)
78
+ i += 1
79
+ sys.stdout.write("\r" + " " * (len(message) + 10) + "\r")
80
+ sys.stdout.flush()
81
+
82
+
83
+ def reveal_lines(lines: list[tuple[str, str]], delay: float = 0.15) -> None:
84
+ """Reveal lines one by one with a fade-in effect."""
85
+ for text, style in lines:
86
+ if style:
87
+ console.print(text, style=style)
88
+ else:
89
+ console.print(text)
90
+ time.sleep(delay)
91
+
92
+
93
+ def animated_progress_bar(steps: int, current: int, width: int = 30) -> str:
94
+ """Create an animated-style progress bar."""
95
+ filled = int(width * current / steps)
96
+ empty = width - filled
97
+ bar = f"{'█' * filled}{'░' * empty}"
98
+ return bar
99
+
100
+
101
+ def pulse_text(text: str, cycles: int = 3) -> None:
102
+ """Pulse text brightness."""
103
+ styles = [Colors.DIM, Colors.MUTED, Colors.PRIMARY, Colors.MUTED, Colors.DIM]
104
+ for _ in range(cycles):
105
+ for style in styles:
106
+ sys.stdout.write(f"\r [{style}]{text}[/{style}]")
107
+ sys.stdout.flush()
108
+ time.sleep(0.08)
109
+
110
+
111
+ # ─────────────────────────────────────────────────────────────────────────────
112
+ # First Run Detection
113
+ # ─────────────────────────────────────────────────────────────────────────────
114
+
115
+ def is_first_run() -> bool:
116
+ """Check if this is the first time running emdash."""
117
+ emdash_dir = Path.home() / ".emdash"
118
+ markers = [
119
+ emdash_dir / "cli_history",
120
+ emdash_dir / "config.json",
121
+ emdash_dir / "sessions",
122
+ ]
123
+ return not any(m.exists() for m in markers)
124
+
125
+
126
+ # ─────────────────────────────────────────────────────────────────────────────
127
+ # Animated Welcome Screen
128
+ # ─────────────────────────────────────────────────────────────────────────────
129
+
130
+ def show_welcome_screen() -> bool:
131
+ """Show clean welcome screen.
132
+
133
+ Returns:
134
+ True if user wants to proceed with onboarding, False to skip.
135
+ """
136
+ console.print()
137
+
138
+ # Simple, clean header
139
+ console.print(f"[{Colors.MUTED}]{header('emdash', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
140
+ console.print()
141
+ console.print(f" [{Colors.PRIMARY} bold]Welcome[/{Colors.PRIMARY} bold]")
142
+ console.print()
143
+ console.print(f" [{Colors.DIM}]Let's get you set up.[/{Colors.DIM}]")
144
+ console.print()
145
+
146
+ # Clean step list
147
+ console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Connect to GitHub [{Colors.DIM}](optional)[/{Colors.DIM}]")
148
+ console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Create your first rule")
149
+ console.print(f" [{Colors.MUTED}]{DOT_WAITING}[/{Colors.MUTED}] Start building")
150
+ console.print()
151
+
152
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
153
+ console.print()
154
+
155
+ # Prompt
156
+ console.print(f" [{Colors.DIM}]Enter to begin · s to skip[/{Colors.DIM}]")
157
+ console.print()
158
+
159
+ try:
160
+ session = PromptSession()
161
+ response = session.prompt(f" {ARROW_PROMPT} ").strip().lower()
162
+ return response != 's'
163
+ except (KeyboardInterrupt, EOFError):
164
+ return False
165
+
166
+
167
+ # ─────────────────────────────────────────────────────────────────────────────
168
+ # Step Headers
169
+ # ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ def show_step_header(step: int, total: int, title: str) -> None:
172
+ """Show step header with progress indicator."""
173
+ console.print()
174
+ console.print(f"[{Colors.MUTED}]{header(title, SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
175
+ console.print(f" [{Colors.DIM}]step {step} of {total}[/{Colors.DIM}]")
176
+ console.print()
177
+
178
+
179
+ def show_step_complete(message: str) -> None:
180
+ """Show step completion."""
181
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] {message}")
182
+
183
+
184
+ # ─────────────────────────────────────────────────────────────────────────────
185
+ # Step 1: GitHub Authentication
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+
188
+ def step_github_auth() -> bool:
189
+ """Step 1: GitHub authentication (optional).
190
+
191
+ Returns:
192
+ True if completed or skipped, False if cancelled.
193
+ """
194
+ show_step_header(1, 3, "Connect GitHub")
195
+
196
+ console.print(f" [{Colors.PRIMARY}]GitHub connection enables:[/{Colors.PRIMARY}]")
197
+ console.print()
198
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} PR reviews and creation[/{Colors.MUTED}]")
199
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Issue management[/{Colors.MUTED}]")
200
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Repository insights[/{Colors.MUTED}]")
201
+ console.print()
202
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
203
+ console.print()
204
+
205
+ # Interactive menu
206
+ selected_index = [0]
207
+ result = [None]
208
+
209
+ options = [
210
+ ("connect", "Connect GitHub account", "Opens browser for OAuth"),
211
+ ("skip", "Skip for now", "Can connect later with /auth"),
212
+ ]
213
+
214
+ kb = KeyBindings()
215
+
216
+ @kb.add("up")
217
+ @kb.add("k")
218
+ def move_up(event):
219
+ selected_index[0] = (selected_index[0] - 1) % len(options)
220
+
221
+ @kb.add("down")
222
+ @kb.add("j")
223
+ def move_down(event):
224
+ selected_index[0] = (selected_index[0] + 1) % len(options)
225
+
226
+ @kb.add("enter")
227
+ def select(event):
228
+ result[0] = options[selected_index[0]][0]
229
+ event.app.exit()
230
+
231
+ @kb.add("c")
232
+ def connect(event):
233
+ result[0] = "connect"
234
+ event.app.exit()
235
+
236
+ @kb.add("s")
237
+ def skip(event):
238
+ result[0] = "skip"
239
+ event.app.exit()
240
+
241
+ @kb.add("c-c")
242
+ @kb.add("escape")
243
+ def cancel(event):
244
+ result[0] = "cancel"
245
+ event.app.exit()
246
+
247
+ def get_formatted_options():
248
+ lines = []
249
+ for i, (key, desc, hint) in enumerate(options):
250
+ indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
251
+ if i == selected_index[0]:
252
+ lines.append(("class:selected", f" {indicator} {desc}\n"))
253
+ lines.append(("class:hint-selected", f" {hint}\n"))
254
+ else:
255
+ lines.append(("class:option", f" {indicator} {desc}\n"))
256
+ lines.append(("class:hint-dim", f" {hint}\n"))
257
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} c connect s skip Esc cancel"))
258
+ return lines
259
+
260
+ style = Style.from_dict({
261
+ "selected": f"{Colors.SUCCESS} bold",
262
+ "hint-selected": Colors.SUCCESS,
263
+ "option": Colors.MUTED,
264
+ "hint-dim": Colors.DIM,
265
+ "hint": f"{Colors.DIM} italic",
266
+ })
267
+
268
+ layout = Layout(
269
+ HSplit([
270
+ Window(
271
+ FormattedTextControl(get_formatted_options),
272
+ height=7,
273
+ ),
274
+ ])
275
+ )
276
+
277
+ app = Application(
278
+ layout=layout,
279
+ key_bindings=kb,
280
+ style=style,
281
+ full_screen=False,
282
+ )
283
+
284
+ try:
285
+ app.run()
286
+ except (KeyboardInterrupt, EOFError):
287
+ return False
288
+
289
+ if result[0] == "cancel":
290
+ return False
291
+
292
+ if result[0] == "connect":
293
+ from ..auth import auth_login
294
+ console.print()
295
+ try:
296
+ auth_login(no_browser=False)
297
+ console.print()
298
+ show_step_complete("GitHub connected successfully")
299
+ except Exception as e:
300
+ console.print(f" [{Colors.ERROR}]{STATUS_ERROR}[/{Colors.ERROR}] Connection failed: {e}")
301
+ console.print(f" [{Colors.DIM}]You can try again later with /auth login[/{Colors.DIM}]")
302
+ else:
303
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Skipped {ARROW_RIGHT} run /auth login anytime[/{Colors.MUTED}]")
304
+
305
+ return True
306
+
307
+
308
+ # ─────────────────────────────────────────────────────────────────────────────
309
+ # Step 2: Create Rule
310
+ # ─────────────────────────────────────────────────────────────────────────────
311
+
312
+ def step_create_rule() -> bool:
313
+ """Step 2: Create first rule (optional).
314
+
315
+ Returns:
316
+ True if completed or skipped, False if cancelled.
317
+ """
318
+ show_step_header(2, 3, "Create a Rule")
319
+
320
+ console.print(f" [{Colors.PRIMARY}]Rules guide the agent's behavior:[/{Colors.PRIMARY}]")
321
+ console.print()
322
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Coding style preferences[/{Colors.MUTED}]")
323
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Project conventions[/{Colors.MUTED}]")
324
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Testing requirements[/{Colors.MUTED}]")
325
+ console.print()
326
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
327
+ console.print()
328
+
329
+ selected_index = [0]
330
+ result = [None]
331
+
332
+ options = [
333
+ ("template", "Create from template", "Quick start with best practices"),
334
+ ("custom", "Write custom rule", "Define your own guidelines"),
335
+ ("skip", "Skip for now", "Can create later with /rules"),
336
+ ]
337
+
338
+ kb = KeyBindings()
339
+
340
+ @kb.add("up")
341
+ @kb.add("k")
342
+ def move_up(event):
343
+ selected_index[0] = (selected_index[0] - 1) % len(options)
344
+
345
+ @kb.add("down")
346
+ @kb.add("j")
347
+ def move_down(event):
348
+ selected_index[0] = (selected_index[0] + 1) % len(options)
349
+
350
+ @kb.add("enter")
351
+ def select(event):
352
+ result[0] = options[selected_index[0]][0]
353
+ event.app.exit()
354
+
355
+ @kb.add("s")
356
+ def skip(event):
357
+ result[0] = "skip"
358
+ event.app.exit()
359
+
360
+ @kb.add("c-c")
361
+ @kb.add("escape")
362
+ def cancel(event):
363
+ result[0] = "cancel"
364
+ event.app.exit()
365
+
366
+ def get_formatted_options():
367
+ lines = []
368
+ for i, (key, desc, hint) in enumerate(options):
369
+ indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
370
+ if i == selected_index[0]:
371
+ lines.append(("class:selected", f" {indicator} {desc}\n"))
372
+ lines.append(("class:hint-selected", f" {hint}\n"))
373
+ else:
374
+ lines.append(("class:option", f" {indicator} {desc}\n"))
375
+ lines.append(("class:hint-dim", f" {hint}\n"))
376
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} Enter select s skip Esc cancel"))
377
+ return lines
378
+
379
+ style = Style.from_dict({
380
+ "selected": f"{Colors.SUCCESS} bold",
381
+ "hint-selected": Colors.SUCCESS,
382
+ "option": Colors.MUTED,
383
+ "hint-dim": Colors.DIM,
384
+ "hint": f"{Colors.DIM} italic",
385
+ })
386
+
387
+ layout = Layout(
388
+ HSplit([
389
+ Window(
390
+ FormattedTextControl(get_formatted_options),
391
+ height=9,
392
+ ),
393
+ ])
394
+ )
395
+
396
+ app = Application(
397
+ layout=layout,
398
+ key_bindings=kb,
399
+ style=style,
400
+ full_screen=False,
401
+ )
402
+
403
+ try:
404
+ app.run()
405
+ except (KeyboardInterrupt, EOFError):
406
+ return False
407
+
408
+ if result[0] == "cancel":
409
+ return False
410
+
411
+ if result[0] == "template":
412
+ console.print()
413
+ create_rule_from_template()
414
+ elif result[0] == "custom":
415
+ console.print()
416
+ create_custom_rule()
417
+ else:
418
+ console.print(f" [{Colors.MUTED}]{DOT_BULLET} Skipped {ARROW_RIGHT} run /rules anytime[/{Colors.MUTED}]")
419
+
420
+ return True
421
+
422
+
423
+ def create_rule_from_template() -> None:
424
+ """Create a rule from a template."""
425
+ templates = [
426
+ ("python", "Python best practices", "Type hints, docstrings, PEP 8"),
427
+ ("typescript", "TypeScript standards", "Strict types, ESLint, modern syntax"),
428
+ ("testing", "Testing requirements", "Tests for features, 80% coverage"),
429
+ ("minimal", "Minimal rule", "Concise, focused responses"),
430
+ ]
431
+
432
+ selected_index = [0]
433
+ result = [None]
434
+
435
+ kb = KeyBindings()
436
+
437
+ @kb.add("up")
438
+ @kb.add("k")
439
+ def move_up(event):
440
+ selected_index[0] = (selected_index[0] - 1) % len(templates)
441
+
442
+ @kb.add("down")
443
+ @kb.add("j")
444
+ def move_down(event):
445
+ selected_index[0] = (selected_index[0] + 1) % len(templates)
446
+
447
+ @kb.add("enter")
448
+ def select(event):
449
+ result[0] = templates[selected_index[0]]
450
+ event.app.exit()
451
+
452
+ @kb.add("c-c")
453
+ @kb.add("escape")
454
+ def cancel(event):
455
+ result[0] = None
456
+ event.app.exit()
457
+
458
+ def get_formatted_templates():
459
+ lines = [("class:title", " Select a template:\n\n")]
460
+ for i, (key, title, desc) in enumerate(templates):
461
+ indicator = STATUS_ACTIVE if i == selected_index[0] else STATUS_INACTIVE
462
+ if i == selected_index[0]:
463
+ lines.append(("class:selected", f" {indicator} {title}\n"))
464
+ lines.append(("class:selected-desc", f" {desc}\n"))
465
+ else:
466
+ lines.append(("class:option", f" {indicator} {title}\n"))
467
+ lines.append(("class:desc", f" {desc}\n"))
468
+ lines.append(("class:hint", f"\n{ARROW_PROMPT} ↑↓ select Enter confirm Esc cancel"))
469
+ return lines
470
+
471
+ style = Style.from_dict({
472
+ "title": f"{Colors.PRIMARY} bold",
473
+ "selected": f"{Colors.SUCCESS} bold",
474
+ "selected-desc": Colors.SUCCESS,
475
+ "option": Colors.MUTED,
476
+ "desc": Colors.DIM,
477
+ "hint": f"{Colors.DIM} italic",
478
+ })
479
+
480
+ layout = Layout(
481
+ HSplit([
482
+ Window(
483
+ FormattedTextControl(get_formatted_templates),
484
+ height=len(templates) * 2 + 4,
485
+ ),
486
+ ])
487
+ )
488
+
489
+ app = Application(
490
+ layout=layout,
491
+ key_bindings=kb,
492
+ style=style,
493
+ full_screen=False,
494
+ )
495
+
496
+ try:
497
+ app.run()
498
+ except (KeyboardInterrupt, EOFError):
499
+ return
500
+
501
+ if result[0]:
502
+ key, title, desc = result[0]
503
+
504
+ rules_dir = Path.cwd() / ".emdash" / "rules"
505
+ rules_dir.mkdir(parents=True, exist_ok=True)
506
+ rule_file = rules_dir / f"{key}.md"
507
+
508
+ rule_content = f"""# {title}
509
+
510
+ {desc}
511
+
512
+ ## Guidelines
513
+
514
+ - Follow project conventions
515
+ - Write clean, maintainable code
516
+ - Add appropriate documentation
517
+ """
518
+ rule_file.write_text(rule_content)
519
+ show_step_complete(f"Created rule: {rule_file.relative_to(Path.cwd())}")
520
+
521
+
522
+ def create_custom_rule() -> None:
523
+ """Create a custom rule with user input."""
524
+ console.print(f" [{Colors.DIM}]Enter a name for your rule:[/{Colors.DIM}]")
525
+
526
+ try:
527
+ session = PromptSession()
528
+ name = session.prompt(f" {ARROW_PROMPT} ").strip()
529
+ if not name:
530
+ return
531
+
532
+ name = name.lower().replace(" ", "-")
533
+
534
+ console.print()
535
+ console.print(f" [{Colors.DIM}]Describe the rule (one line):[/{Colors.DIM}]")
536
+ desc = session.prompt(f" {ARROW_PROMPT} ").strip()
537
+
538
+ rules_dir = Path.cwd() / ".emdash" / "rules"
539
+ rules_dir.mkdir(parents=True, exist_ok=True)
540
+ rule_file = rules_dir / f"{name}.md"
541
+
542
+ rule_content = f"""# {name.replace("-", " ").title()}
543
+
544
+ {desc or "Custom rule"}
545
+
546
+ ## Guidelines
547
+
548
+ - Add your specific guidelines here
549
+ """
550
+ rule_file.write_text(rule_content)
551
+ show_step_complete(f"Created rule: {rule_file.relative_to(Path.cwd())}")
552
+ console.print(f" [{Colors.DIM}]Edit the file to add more details[/{Colors.DIM}]")
553
+ except (KeyboardInterrupt, EOFError):
554
+ return
555
+
556
+
557
+ # ─────────────────────────────────────────────────────────────────────────────
558
+ # Step 3: Completion
559
+ # ─────────────────────────────────────────────────────────────────────────────
560
+
561
+ def step_quick_command() -> bool:
562
+ """Step 3: Show quick commands.
563
+
564
+ Returns:
565
+ True to complete onboarding.
566
+ """
567
+ show_step_header(3, 3, "You're Ready")
568
+
569
+ console.print(f" [{Colors.SUCCESS}]Setup complete![/{Colors.SUCCESS}]")
570
+ console.print()
571
+
572
+ console.print(f" [{Colors.DIM}]Quick commands:[/{Colors.DIM}]")
573
+ console.print()
574
+ console.print(f" [{Colors.PRIMARY}]/help [/{Colors.PRIMARY}] [{Colors.DIM}]Show all commands[/{Colors.DIM}]")
575
+ console.print(f" [{Colors.PRIMARY}]/plan [/{Colors.PRIMARY}] [{Colors.DIM}]Switch to plan mode[/{Colors.DIM}]")
576
+ console.print(f" [{Colors.PRIMARY}]/agents [/{Colors.PRIMARY}] [{Colors.DIM}]Manage custom agents[/{Colors.DIM}]")
577
+ console.print(f" [{Colors.PRIMARY}]/rules [/{Colors.PRIMARY}] [{Colors.DIM}]Configure rules[/{Colors.DIM}]")
578
+ console.print()
579
+
580
+ console.print(f" [{Colors.MUTED}]Or just type your question to get started.[/{Colors.MUTED}]")
581
+ console.print()
582
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
583
+ console.print()
584
+
585
+ return True
586
+
587
+
588
+ # ─────────────────────────────────────────────────────────────────────────────
589
+ # Main Onboarding Flow
590
+ # ─────────────────────────────────────────────────────────────────────────────
591
+
592
+ def run_onboarding() -> bool:
593
+ """Run the complete animated onboarding flow.
594
+
595
+ Returns:
596
+ True if onboarding completed, False if cancelled.
597
+ """
598
+ if not show_welcome_screen():
599
+ console.print(f" [{Colors.DIM}]Skipped onboarding. Run /setup anytime.[/{Colors.DIM}]")
600
+ console.print()
601
+ return False
602
+
603
+ # Step 1: GitHub auth
604
+ if not step_github_auth():
605
+ return False
606
+
607
+ # Step 2: Create rule
608
+ if not step_create_rule():
609
+ return False
610
+
611
+ # Step 3: Quick commands
612
+ step_quick_command()
613
+
614
+ # Mark onboarding complete
615
+ emdash_dir = Path.home() / ".emdash"
616
+ emdash_dir.mkdir(parents=True, exist_ok=True)
617
+ (emdash_dir / ".onboarding_complete").touch()
618
+
619
+ return True