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,635 @@
1
+ """Registry CLI commands for browsing and installing community components."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ import click
9
+ import httpx
10
+ from rich.console import Console
11
+
12
+ from ..design import (
13
+ Colors,
14
+ header,
15
+ footer,
16
+ SEPARATOR_WIDTH,
17
+ STATUS_ACTIVE,
18
+ )
19
+
20
+ console = Console()
21
+
22
+ # GitHub raw URL base for the registry
23
+ GITHUB_REPO = "mendyEdri/emdash-registry"
24
+ GITHUB_BRANCH = "main"
25
+ REGISTRY_BASE_URL = f"https://raw.githubusercontent.com/{GITHUB_REPO}/{GITHUB_BRANCH}"
26
+
27
+
28
+ ComponentType = Literal["skill", "rule", "agent", "verifier"]
29
+
30
+
31
+ def _fetch_registry() -> dict | None:
32
+ """Fetch the registry.json from GitHub."""
33
+ url = f"{REGISTRY_BASE_URL}/registry.json"
34
+ try:
35
+ response = httpx.get(url, timeout=10)
36
+ response.raise_for_status()
37
+ return response.json()
38
+ except Exception as e:
39
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Failed to fetch registry: {e}")
40
+ return None
41
+
42
+
43
+ def _fetch_component(path: str) -> str | None:
44
+ """Fetch a component file from GitHub."""
45
+ url = f"{REGISTRY_BASE_URL}/{path}"
46
+ try:
47
+ response = httpx.get(url, timeout=10)
48
+ response.raise_for_status()
49
+ return response.text
50
+ except Exception as e:
51
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Failed to fetch component: {e}")
52
+ return None
53
+
54
+
55
+ def _get_emdash_dir() -> Path:
56
+ """Get the .emdash directory."""
57
+ return Path.cwd() / ".emdash"
58
+
59
+
60
+ def _install_skill(name: str, content: str) -> bool:
61
+ """Install a skill to .emdash/skills/."""
62
+ skill_dir = _get_emdash_dir() / "skills" / name
63
+ skill_dir.mkdir(parents=True, exist_ok=True)
64
+ skill_file = skill_dir / "SKILL.md"
65
+ skill_file.write_text(content)
66
+ return True
67
+
68
+
69
+ def _install_rule(name: str, content: str) -> bool:
70
+ """Install a rule to .emdash/rules/."""
71
+ rules_dir = _get_emdash_dir() / "rules"
72
+ rules_dir.mkdir(parents=True, exist_ok=True)
73
+ rule_file = rules_dir / f"{name}.md"
74
+ rule_file.write_text(content)
75
+ return True
76
+
77
+
78
+ def _install_agent(name: str, content: str) -> bool:
79
+ """Install an agent to .emdash/agents/."""
80
+ agents_dir = _get_emdash_dir() / "agents"
81
+ agents_dir.mkdir(parents=True, exist_ok=True)
82
+ agent_file = agents_dir / f"{name}.md"
83
+ agent_file.write_text(content)
84
+ return True
85
+
86
+
87
+ def _install_verifier(name: str, content: str) -> bool:
88
+ """Install a verifier to .emdash/verifiers.json."""
89
+ verifiers_file = _get_emdash_dir() / "verifiers.json"
90
+
91
+ # Load or create verifiers config
92
+ if verifiers_file.exists():
93
+ existing = json.loads(verifiers_file.read_text())
94
+ else:
95
+ _get_emdash_dir().mkdir(parents=True, exist_ok=True)
96
+ existing = {"verifiers": [], "max_attempts": 3}
97
+
98
+ # Parse new verifier
99
+ new_verifier = json.loads(content)
100
+
101
+ # Check if already exists
102
+ existing_names = [v.get("name") for v in existing.get("verifiers", [])]
103
+ if name in existing_names:
104
+ # Update existing
105
+ existing["verifiers"] = [
106
+ new_verifier if v.get("name") == name else v
107
+ for v in existing["verifiers"]
108
+ ]
109
+ else:
110
+ existing["verifiers"].append(new_verifier)
111
+
112
+ verifiers_file.write_text(json.dumps(existing, indent=2))
113
+ return True
114
+
115
+
116
+ @click.group(invoke_without_command=True)
117
+ @click.pass_context
118
+ def registry(ctx):
119
+ """Browse and install community skills, rules, agents, and verifiers.
120
+
121
+ Run without arguments to open the interactive wizard.
122
+ """
123
+ if ctx.invoked_subcommand is None:
124
+ # Interactive wizard mode
125
+ _show_registry_wizard()
126
+
127
+
128
+ @registry.command("list")
129
+ @click.argument("component_type", required=False,
130
+ type=click.Choice(["skills", "rules", "agents", "verifiers"]))
131
+ def registry_list(component_type: str | None):
132
+ """List available components from the registry."""
133
+ reg = _fetch_registry()
134
+ if not reg:
135
+ return
136
+
137
+ types_to_show = [component_type] if component_type else ["skills", "rules", "agents", "verifiers"]
138
+
139
+ for ctype in types_to_show:
140
+ components = reg.get(ctype, {})
141
+ if not components:
142
+ continue
143
+
144
+ console.print()
145
+ console.print(f"[{Colors.MUTED}]{header(ctype.title(), SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
146
+ console.print()
147
+
148
+ for name, info in components.items():
149
+ tags = ", ".join(info.get("tags", []))
150
+ desc = info.get("description", "")
151
+ console.print(f" [{Colors.PRIMARY}]{name}[/{Colors.PRIMARY}]")
152
+ if desc:
153
+ console.print(f" [{Colors.MUTED}]{desc}[/{Colors.MUTED}]")
154
+ if tags:
155
+ console.print(f" [{Colors.DIM}]{tags}[/{Colors.DIM}]")
156
+
157
+ console.print()
158
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
159
+ console.print()
160
+
161
+
162
+ @registry.command("show")
163
+ @click.argument("component_id")
164
+ def registry_show(component_id: str):
165
+ """Show details of a component.
166
+
167
+ COMPONENT_ID format: type:name (e.g., skill:frontend-design)
168
+ """
169
+ if ":" not in component_id:
170
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Invalid format. Use type:name (e.g., skill:frontend-design)")
171
+ return
172
+
173
+ ctype, name = component_id.split(":", 1)
174
+ type_plural = ctype + "s" if not ctype.endswith("s") else ctype
175
+
176
+ reg = _fetch_registry()
177
+ if not reg:
178
+ return
179
+
180
+ components = reg.get(type_plural, {})
181
+ if name not in components:
182
+ console.print(f" [{Colors.WARNING}]{ctype.title()} '{name}' not found in registry.[/{Colors.WARNING}]")
183
+ return
184
+
185
+ info = components[name]
186
+
187
+ # Fetch the content
188
+ content = _fetch_component(info["path"])
189
+ if not content:
190
+ return
191
+
192
+ console.print()
193
+ console.print(f"[{Colors.MUTED}]{header(f'{ctype}:{name}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
194
+ console.print()
195
+
196
+ if info.get('description'):
197
+ console.print(f" [{Colors.DIM}]desc[/{Colors.DIM}] {info.get('description', '')}")
198
+ if info.get('tags'):
199
+ console.print(f" [{Colors.DIM}]tags[/{Colors.DIM}] {', '.join(info.get('tags', []))}")
200
+ if info.get('path'):
201
+ console.print(f" [{Colors.DIM}]path[/{Colors.DIM}] {info.get('path', '')}")
202
+
203
+ console.print()
204
+ console.print(f" [{Colors.DIM}]content:[/{Colors.DIM}]")
205
+ console.print()
206
+
207
+ # Show content with indentation
208
+ for line in content.split('\n')[:30]: # Limit preview lines
209
+ if line.startswith('#'):
210
+ console.print(f" [{Colors.PRIMARY}]{line}[/{Colors.PRIMARY}]")
211
+ else:
212
+ console.print(f" [{Colors.MUTED}]{line}[/{Colors.MUTED}]")
213
+
214
+ if len(content.split('\n')) > 30:
215
+ console.print(f" [{Colors.DIM}]... ({len(content.split(chr(10))) - 30} more lines)[/{Colors.DIM}]")
216
+
217
+ console.print()
218
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
219
+
220
+
221
+ @registry.command("install")
222
+ @click.argument("component_ids", nargs=-1)
223
+ def registry_install(component_ids: tuple[str, ...]):
224
+ """Install components from the registry.
225
+
226
+ COMPONENT_IDS format: type:name (e.g., skill:frontend-design rule:typescript)
227
+ """
228
+ if not component_ids:
229
+ console.print()
230
+ console.print(f" [{Colors.WARNING}]usage:[/{Colors.WARNING}] emdash registry install type:name")
231
+ console.print(f" [{Colors.DIM}]example: emdash registry install skill:frontend-design rule:typescript[/{Colors.DIM}]")
232
+ console.print()
233
+ return
234
+
235
+ reg = _fetch_registry()
236
+ if not reg:
237
+ return
238
+
239
+ console.print()
240
+ for component_id in component_ids:
241
+ if ":" not in component_id:
242
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Invalid format: {component_id}. Use type:name")
243
+ continue
244
+
245
+ ctype, name = component_id.split(":", 1)
246
+ type_plural = ctype + "s" if not ctype.endswith("s") else ctype
247
+
248
+ components = reg.get(type_plural, {})
249
+ if name not in components:
250
+ console.print(f" [{Colors.WARNING}]not found:[/{Colors.WARNING}] {ctype}:{name}")
251
+ continue
252
+
253
+ info = components[name]
254
+ content = _fetch_component(info["path"])
255
+ if not content:
256
+ continue
257
+
258
+ # Install based on type
259
+ installers = {
260
+ "skill": _install_skill,
261
+ "rule": _install_rule,
262
+ "agent": _install_agent,
263
+ "verifier": _install_verifier,
264
+ }
265
+
266
+ installer = installers.get(ctype)
267
+ if not installer:
268
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] Unknown component type: {ctype}")
269
+ continue
270
+
271
+ try:
272
+ installer(name, content)
273
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]installed:[/{Colors.MUTED}] {ctype}:{name}")
274
+ except Exception as e:
275
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] {ctype}:{name} - {e}")
276
+ console.print()
277
+
278
+
279
+ @registry.command("search")
280
+ @click.argument("query")
281
+ @click.option("--tag", "-t", multiple=True, help="Filter by tag")
282
+ def registry_search(query: str, tag: tuple[str, ...]):
283
+ """Search the registry by name or description."""
284
+ reg = _fetch_registry()
285
+ if not reg:
286
+ return
287
+
288
+ query_lower = query.lower()
289
+ tags_lower = [t.lower() for t in tag]
290
+
291
+ results = []
292
+
293
+ for ctype in ["skills", "rules", "agents", "verifiers"]:
294
+ components = reg.get(ctype, {})
295
+ for name, info in components.items():
296
+ # Match query
297
+ matches_query = (
298
+ query_lower in name.lower() or
299
+ query_lower in info.get("description", "").lower()
300
+ )
301
+
302
+ # Match tags
303
+ component_tags = [t.lower() for t in info.get("tags", [])]
304
+ matches_tags = not tags_lower or any(t in component_tags for t in tags_lower)
305
+
306
+ if matches_query and matches_tags:
307
+ results.append((ctype[:-1], name, info)) # Remove 's' from type
308
+
309
+ console.print()
310
+ if not results:
311
+ console.print(f" [{Colors.DIM}]no results for '{query}'[/{Colors.DIM}]")
312
+ console.print()
313
+ return
314
+
315
+ console.print(f"[{Colors.MUTED}]{header(f'Search: {query}', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
316
+ console.print()
317
+
318
+ for ctype, name, info in results:
319
+ tags = ", ".join(info.get("tags", []))
320
+ desc = info.get("description", "")
321
+ console.print(f" [{Colors.PRIMARY}]{ctype}:{name}[/{Colors.PRIMARY}]")
322
+ if desc:
323
+ console.print(f" [{Colors.MUTED}]{desc}[/{Colors.MUTED}]")
324
+ if tags:
325
+ console.print(f" [{Colors.DIM}]{tags}[/{Colors.DIM}]")
326
+
327
+ console.print()
328
+ console.print(f"[{Colors.MUTED}]{footer(SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
329
+ console.print(f" [{Colors.DIM}]{len(results)} result{'s' if len(results) != 1 else ''}[/{Colors.DIM}]")
330
+ console.print()
331
+
332
+
333
+ def _show_registry_wizard():
334
+ """Show interactive registry wizard."""
335
+ from prompt_toolkit import Application
336
+ from prompt_toolkit.key_binding import KeyBindings
337
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
338
+ from prompt_toolkit.styles import Style
339
+
340
+ console.print()
341
+ console.print(f"[{Colors.MUTED}]{header('Registry', SEPARATOR_WIDTH)}[/{Colors.MUTED}]")
342
+ console.print()
343
+ console.print(f" [{Colors.DIM}]browse and install community components[/{Colors.DIM}]")
344
+ console.print()
345
+
346
+ # Fetch registry
347
+ console.print(f" [{Colors.DIM}]fetching...[/{Colors.DIM}]", end="\r")
348
+ reg = _fetch_registry()
349
+ console.print(" ", end="\r") # Clear fetching message
350
+
351
+ if not reg:
352
+ return
353
+
354
+ # Build menu items
355
+ categories = [
356
+ ("skills", "Skills", "specialized capabilities"),
357
+ ("rules", "Rules", "coding standards"),
358
+ ("agents", "Agents", "custom configurations"),
359
+ ("verifiers", "Verifiers", "verification configs"),
360
+ ]
361
+
362
+ selected_category = [0]
363
+ result = [None]
364
+
365
+ kb = KeyBindings()
366
+
367
+ @kb.add("up")
368
+ @kb.add("k")
369
+ def move_up(event):
370
+ selected_category[0] = (selected_category[0] - 1) % len(categories)
371
+
372
+ @kb.add("down")
373
+ @kb.add("j")
374
+ def move_down(event):
375
+ selected_category[0] = (selected_category[0] + 1) % len(categories)
376
+
377
+ @kb.add("enter")
378
+ def select(event):
379
+ result[0] = categories[selected_category[0]][0]
380
+ event.app.exit()
381
+
382
+ @kb.add("1")
383
+ def select_1(event):
384
+ result[0] = "skills"
385
+ event.app.exit()
386
+
387
+ @kb.add("2")
388
+ def select_2(event):
389
+ result[0] = "rules"
390
+ event.app.exit()
391
+
392
+ @kb.add("3")
393
+ def select_3(event):
394
+ result[0] = "agents"
395
+ event.app.exit()
396
+
397
+ @kb.add("4")
398
+ def select_4(event):
399
+ result[0] = "verifiers"
400
+ event.app.exit()
401
+
402
+ @kb.add("c-c")
403
+ @kb.add("escape")
404
+ @kb.add("q")
405
+ def cancel(event):
406
+ result[0] = None
407
+ event.app.exit()
408
+
409
+ def get_formatted_menu():
410
+ lines = [("class:title", f"─── Categories {'─' * 30}\n\n")]
411
+
412
+ for i, (key, name, desc) in enumerate(categories):
413
+ count = len(reg.get(key, {}))
414
+ is_selected = i == selected_category[0]
415
+ prefix = "▸ " if is_selected else " "
416
+
417
+ if is_selected:
418
+ lines.append(("class:selected", f" {prefix}{name}"))
419
+ lines.append(("class:count-selected", f" {count}"))
420
+ lines.append(("class:desc-selected", f" {desc}\n"))
421
+ else:
422
+ lines.append(("class:option", f" {prefix}{name}"))
423
+ lines.append(("class:count", f" {count}"))
424
+ lines.append(("class:desc", f" {desc}\n"))
425
+
426
+ lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Enter select 1-4 quick q quit"))
427
+ return lines
428
+
429
+ style = Style.from_dict({
430
+ "title": f"{Colors.MUTED}",
431
+ "selected": f"{Colors.SUCCESS} bold",
432
+ "count-selected": f"{Colors.SUCCESS}",
433
+ "desc-selected": f"{Colors.SUCCESS}",
434
+ "option": f"{Colors.PRIMARY}",
435
+ "count": f"{Colors.MUTED}",
436
+ "desc": f"{Colors.DIM}",
437
+ "hint": f"{Colors.DIM}",
438
+ })
439
+
440
+ layout = Layout(
441
+ HSplit([
442
+ Window(
443
+ FormattedTextControl(get_formatted_menu),
444
+ height=len(categories) + 5,
445
+ ),
446
+ ])
447
+ )
448
+
449
+ app = Application(
450
+ layout=layout,
451
+ key_bindings=kb,
452
+ style=style,
453
+ full_screen=False,
454
+ )
455
+
456
+ try:
457
+ app.run()
458
+ except (KeyboardInterrupt, EOFError):
459
+ return
460
+
461
+ console.print()
462
+
463
+ if result[0] is None:
464
+ return
465
+
466
+ # Show components in selected category
467
+ _show_component_picker(reg, result[0])
468
+
469
+
470
+ def _show_component_picker(reg: dict, category: str):
471
+ """Show interactive component picker for a category."""
472
+ from prompt_toolkit import Application
473
+ from prompt_toolkit.key_binding import KeyBindings
474
+ from prompt_toolkit.layout import Layout, HSplit, Window, FormattedTextControl
475
+ from prompt_toolkit.styles import Style
476
+
477
+ components = reg.get(category, {})
478
+ if not components:
479
+ console.print(f" [{Colors.WARNING}]No {category} available.[/{Colors.WARNING}]")
480
+ return
481
+
482
+ # Build items list
483
+ items = [(name, info) for name, info in components.items()]
484
+
485
+ selected_index = [0]
486
+ selected_items = set() # For multi-select
487
+ result = [None] # "install", "back", or None
488
+
489
+ kb = KeyBindings()
490
+
491
+ @kb.add("up")
492
+ @kb.add("k")
493
+ def move_up(event):
494
+ selected_index[0] = (selected_index[0] - 1) % len(items)
495
+
496
+ @kb.add("down")
497
+ @kb.add("j")
498
+ def move_down(event):
499
+ selected_index[0] = (selected_index[0] + 1) % len(items)
500
+
501
+ @kb.add("space")
502
+ def toggle_select(event):
503
+ name = items[selected_index[0]][0]
504
+ if name in selected_items:
505
+ selected_items.remove(name)
506
+ else:
507
+ selected_items.add(name)
508
+
509
+ @kb.add("enter")
510
+ def install_selected(event):
511
+ if selected_items:
512
+ result[0] = "install"
513
+ else:
514
+ # Install current item
515
+ selected_items.add(items[selected_index[0]][0])
516
+ result[0] = "install"
517
+ event.app.exit()
518
+
519
+ @kb.add("a")
520
+ def select_all(event):
521
+ for name, _ in items:
522
+ selected_items.add(name)
523
+
524
+ @kb.add("b")
525
+ @kb.add("escape")
526
+ def go_back(event):
527
+ result[0] = "back"
528
+ event.app.exit()
529
+
530
+ @kb.add("c-c")
531
+ @kb.add("q")
532
+ def cancel(event):
533
+ result[0] = None
534
+ event.app.exit()
535
+
536
+ def get_formatted_menu():
537
+ lines = [("class:title", f"─── {category.title()} {'─' * (40 - len(category))}\n\n")]
538
+
539
+ for i, (name, info) in enumerate(items):
540
+ is_selected = i == selected_index[0]
541
+ is_checked = name in selected_items
542
+ prefix = "▸ " if is_selected else " "
543
+ checkbox = "●" if is_checked else "○"
544
+
545
+ desc = info.get("description", "")
546
+ if len(desc) > 45:
547
+ desc = desc[:42] + "..."
548
+
549
+ if is_selected:
550
+ lines.append(("class:selected", f" {prefix}{checkbox} {name}"))
551
+ lines.append(("class:desc-selected", f" {desc}\n"))
552
+ else:
553
+ style_class = "class:checked" if is_checked else "class:option"
554
+ lines.append((style_class, f" {prefix}{checkbox} {name}"))
555
+ lines.append(("class:desc", f" {desc}\n"))
556
+
557
+ selected_count = len(selected_items)
558
+ if selected_count > 0:
559
+ lines.append(("class:status", f"\n {selected_count} selected"))
560
+
561
+ lines.append(("class:hint", f"\n{'─' * 45}\n ↑↓ navigate Space toggle Enter install a all b back"))
562
+ return lines
563
+
564
+ style = Style.from_dict({
565
+ "title": f"{Colors.MUTED}",
566
+ "selected": f"{Colors.SUCCESS} bold",
567
+ "checked": f"{Colors.WARNING}",
568
+ "desc-selected": f"{Colors.SUCCESS}",
569
+ "option": f"{Colors.PRIMARY}",
570
+ "desc": f"{Colors.DIM}",
571
+ "status": f"{Colors.WARNING} bold",
572
+ "hint": f"{Colors.DIM}",
573
+ })
574
+
575
+ height = len(items) + 6
576
+
577
+ layout = Layout(
578
+ HSplit([
579
+ Window(
580
+ FormattedTextControl(get_formatted_menu),
581
+ height=height,
582
+ ),
583
+ ])
584
+ )
585
+
586
+ app = Application(
587
+ layout=layout,
588
+ key_bindings=kb,
589
+ style=style,
590
+ full_screen=False,
591
+ )
592
+
593
+ try:
594
+ app.run()
595
+ except (KeyboardInterrupt, EOFError):
596
+ return
597
+
598
+ console.print()
599
+
600
+ if result[0] == "back":
601
+ _show_registry_wizard()
602
+ return
603
+
604
+ if result[0] == "install" and selected_items:
605
+ singular = category[:-1]
606
+ component_ids = [f"{singular}:{name}" for name in selected_items]
607
+
608
+ console.print()
609
+ for cid in component_ids:
610
+ ctype, name = cid.split(":", 1)
611
+ info = components[name]
612
+
613
+ console.print(f" [{Colors.DIM}]installing {cid}...[/{Colors.DIM}]", end="\r")
614
+ content = _fetch_component(info["path"])
615
+ console.print(" ", end="\r") # Clear
616
+
617
+ if not content:
618
+ continue
619
+
620
+ installers = {
621
+ "skill": _install_skill,
622
+ "rule": _install_rule,
623
+ "agent": _install_agent,
624
+ "verifier": _install_verifier,
625
+ }
626
+
627
+ installer = installers.get(ctype)
628
+ if installer:
629
+ try:
630
+ installer(name, content)
631
+ console.print(f" [{Colors.SUCCESS}]{STATUS_ACTIVE}[/{Colors.SUCCESS}] [{Colors.MUTED}]installed:[/{Colors.MUTED}] {cid}")
632
+ except Exception as e:
633
+ console.print(f" [{Colors.ERROR}]error:[/{Colors.ERROR}] {cid} - {e}")
634
+
635
+ console.print()