codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,781 @@
1
+ """Interactive TUI for managing agent skills.
2
+
3
+ Launch with /skills to browse, enable, disable, and configure skills.
4
+ Built with prompt_toolkit for proper interactive split-panel interface.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import List, Optional
12
+
13
+ from prompt_toolkit.application import Application
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
16
+ from prompt_toolkit.layout.controls import FormattedTextControl
17
+ from prompt_toolkit.widgets import Frame
18
+
19
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
20
+ from code_puppy.plugins.agent_skills.config import (
21
+ add_skill_directory,
22
+ get_disabled_skills,
23
+ get_skill_directories,
24
+ get_skills_enabled,
25
+ remove_skill_directory,
26
+ set_skill_disabled,
27
+ set_skills_enabled,
28
+ )
29
+ from code_puppy.plugins.agent_skills.discovery import (
30
+ SkillInfo,
31
+ discover_skills,
32
+ refresh_skill_cache,
33
+ )
34
+ from code_puppy.plugins.agent_skills.metadata import (
35
+ SkillMetadata,
36
+ get_skill_resources,
37
+ parse_skill_metadata,
38
+ )
39
+ from code_puppy.tools.command_runner import set_awaiting_user_input
40
+
41
+ PAGE_SIZE = 15 # Items per page
42
+
43
+
44
+ class SkillsMenu:
45
+ """Interactive TUI for managing agent skills."""
46
+
47
+ def __init__(self):
48
+ """Initialize the skills menu."""
49
+ self.skills: List[SkillInfo] = []
50
+ self.disabled_skills: List[str] = []
51
+ self.skill_directories: List[Path] = []
52
+ self.skills_enabled = False
53
+
54
+ # State management
55
+ self.selected_idx = 0
56
+ self.current_page = 0
57
+ self.result = None
58
+
59
+ # UI controls (set during run)
60
+ self.menu_control: Optional[FormattedTextControl] = None
61
+ self.preview_control: Optional[FormattedTextControl] = None
62
+
63
+ # Initialize data
64
+ self._refresh_data()
65
+
66
+ def _refresh_data(self) -> None:
67
+ """Refresh skills data from disk."""
68
+ try:
69
+ self.skills = discover_skills()
70
+ self.disabled_skills = get_disabled_skills()
71
+ self.skill_directories = get_skill_directories()
72
+ self.skills_enabled = get_skills_enabled()
73
+ except Exception as e:
74
+ emit_error(f"Failed to refresh skills data: {e}")
75
+
76
+ def _get_current_skill(self) -> Optional[SkillInfo]:
77
+ """Get the currently selected skill."""
78
+ if 0 <= self.selected_idx < len(self.skills):
79
+ return self.skills[self.selected_idx]
80
+ return None
81
+
82
+ def _get_skill_metadata(self, skill: SkillInfo) -> Optional[SkillMetadata]:
83
+ """Get metadata for a skill."""
84
+ try:
85
+ return parse_skill_metadata(skill.path)
86
+ except Exception:
87
+ return None
88
+
89
+ def _is_skill_disabled(self, skill: SkillInfo) -> bool:
90
+ """Check if a skill is disabled."""
91
+ metadata = self._get_skill_metadata(skill)
92
+ if metadata:
93
+ return metadata.name in self.disabled_skills
94
+ return skill.name in self.disabled_skills
95
+
96
+ def _toggle_current_skill(self) -> None:
97
+ """Toggle the enabled/disabled state of the current skill."""
98
+ skill = self._get_current_skill()
99
+ if not skill:
100
+ return
101
+
102
+ metadata = self._get_skill_metadata(skill)
103
+ skill_name = metadata.name if metadata else skill.name
104
+
105
+ is_disabled = skill_name in self.disabled_skills
106
+ set_skill_disabled(skill_name, not is_disabled)
107
+ refresh_skill_cache()
108
+ self._refresh_data()
109
+ self.update_display()
110
+
111
+ def _render_skill_list(self) -> List:
112
+ """Render the skill list panel."""
113
+ lines = []
114
+
115
+ # Header with status
116
+ status_color = "fg:ansigreen" if self.skills_enabled else "fg:ansired"
117
+ status_text = "ENABLED" if self.skills_enabled else "DISABLED"
118
+ lines.append((status_color, f" Skills: {status_text}"))
119
+ lines.append(("", "\n\n"))
120
+
121
+ if not self.skills:
122
+ lines.append(("fg:ansiyellow", " No skills found."))
123
+ lines.append(("", "\n"))
124
+ lines.append(("fg:ansibrightblack", " Create skills in:"))
125
+ lines.append(("", "\n"))
126
+ lines.append(("fg:ansibrightblack", " ~/.code_puppy/skills/"))
127
+ lines.append(("", "\n"))
128
+ lines.append(("fg:ansibrightblack", " ./skills/"))
129
+ lines.append(("", "\n\n"))
130
+ self._render_navigation_hints(lines)
131
+ return lines
132
+
133
+ # Calculate pagination
134
+ total_pages = (len(self.skills) + PAGE_SIZE - 1) // PAGE_SIZE
135
+ start_idx = self.current_page * PAGE_SIZE
136
+ end_idx = min(start_idx + PAGE_SIZE, len(self.skills))
137
+
138
+ # Render skills
139
+ for i in range(start_idx, end_idx):
140
+ skill = self.skills[i]
141
+ is_selected = i == self.selected_idx
142
+ is_disabled = self._is_skill_disabled(skill)
143
+
144
+ # Status icon
145
+ status_icon = "✗" if is_disabled else "✓"
146
+ status_style = "fg:ansired" if is_disabled else "fg:ansigreen"
147
+
148
+ # Get skill name from metadata if available
149
+ metadata = self._get_skill_metadata(skill)
150
+ display_name = metadata.name if metadata else skill.name
151
+
152
+ # Format line
153
+ prefix = " > " if is_selected else " "
154
+
155
+ if is_selected:
156
+ lines.append(("bold", prefix))
157
+ lines.append((status_style + " bold", status_icon))
158
+ lines.append(("bold", f" {display_name}"))
159
+ else:
160
+ lines.append(("", prefix))
161
+ lines.append((status_style, status_icon))
162
+ lines.append(("fg:ansibrightblack", f" {display_name}"))
163
+
164
+ lines.append(("", "\n"))
165
+
166
+ # Pagination info
167
+ lines.append(("", "\n"))
168
+ lines.append(
169
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
170
+ )
171
+ lines.append(("", "\n"))
172
+
173
+ self._render_navigation_hints(lines)
174
+ return lines
175
+
176
+ def _render_navigation_hints(self, lines: List) -> None:
177
+ """Render navigation hints at the bottom."""
178
+ lines.append(("", "\n"))
179
+ lines.append(("fg:ansibrightblack", " ↑/↓ or j/k "))
180
+ lines.append(("", "Navigate "))
181
+ lines.append(("fg:ansibrightblack", "←/→ "))
182
+ lines.append(("", "Page\n"))
183
+ lines.append(("fg:ansigreen", " Enter "))
184
+ lines.append(("", "Toggle "))
185
+ lines.append(("fg:ansicyan", " t "))
186
+ lines.append(("", "Toggle System\n"))
187
+ lines.append(("fg:ansimagenta", " Ctrl+A "))
188
+ lines.append(("", "Add Dir "))
189
+ lines.append(("fg:ansiyellow", " Ctrl+D "))
190
+ lines.append(("", "Show Dirs\n"))
191
+ lines.append(("fg:ansimagenta", " i "))
192
+ lines.append(("", "Install from catalog\n"))
193
+ lines.append(("fg:ansiyellow", " r "))
194
+ lines.append(("", "Refresh "))
195
+ lines.append(("fg:ansired", " q "))
196
+ lines.append(("", "Exit"))
197
+
198
+ def _render_skill_details(self) -> List:
199
+ """Render the skill details panel."""
200
+ lines = []
201
+
202
+ lines.append(("dim cyan", " SKILL DETAILS"))
203
+ lines.append(("", "\n\n"))
204
+
205
+ skill = self._get_current_skill()
206
+ if not skill:
207
+ lines.append(("fg:ansiyellow", " No skill selected."))
208
+ lines.append(("", "\n\n"))
209
+ lines.append(("fg:ansibrightblack", " Select a skill from the list"))
210
+ lines.append(("", "\n"))
211
+ lines.append(("fg:ansibrightblack", " to view its details."))
212
+ return lines
213
+
214
+ metadata = self._get_skill_metadata(skill)
215
+ is_disabled = self._is_skill_disabled(skill)
216
+
217
+ # Status
218
+ status_text = "Disabled" if is_disabled else "Enabled"
219
+ status_style = "fg:ansired bold" if is_disabled else "fg:ansigreen bold"
220
+ lines.append(("bold", " Status: "))
221
+ lines.append((status_style, status_text))
222
+ lines.append(("", "\n\n"))
223
+
224
+ if metadata:
225
+ # Name
226
+ lines.append(("bold", f" {metadata.name}"))
227
+ lines.append(("", "\n\n"))
228
+
229
+ # Description
230
+ if metadata.description:
231
+ lines.append(("bold", " Description:"))
232
+ lines.append(("", "\n"))
233
+ # Wrap description
234
+ desc = metadata.description
235
+ wrapped = self._wrap_text(desc, 50)
236
+ for line in wrapped:
237
+ lines.append(("fg:ansibrightblack", f" {line}"))
238
+ lines.append(("", "\n"))
239
+ lines.append(("", "\n"))
240
+
241
+ # Tags
242
+ if metadata.tags:
243
+ lines.append(("bold", " Tags:"))
244
+ lines.append(("", "\n"))
245
+ tags_str = ", ".join(metadata.tags)
246
+ lines.append(("fg:ansicyan", f" {tags_str}"))
247
+ lines.append(("", "\n\n"))
248
+
249
+ # Resources
250
+ resources = get_skill_resources(metadata.path)
251
+ if resources:
252
+ lines.append(("bold", " Resources:"))
253
+ lines.append(("", "\n"))
254
+ for resource in resources[:5]: # Show first 5
255
+ resource_name = getattr(resource, "name", str(resource))
256
+ lines.append(("fg:ansiyellow", f" • {resource_name}"))
257
+ lines.append(("", "\n"))
258
+ if len(resources) > 5:
259
+ lines.append(
260
+ ("fg:ansibrightblack", f" ... and {len(resources) - 5} more")
261
+ )
262
+ lines.append(("", "\n"))
263
+ lines.append(("", "\n"))
264
+
265
+ else:
266
+ # No metadata available
267
+ lines.append(("bold", f" {skill.name}"))
268
+ lines.append(("", "\n\n"))
269
+ lines.append(("fg:ansiyellow", " No metadata available"))
270
+ lines.append(("", "\n"))
271
+ lines.append(("fg:ansibrightblack", " Add a SKILL.md with frontmatter to"))
272
+ lines.append(("", "\n"))
273
+ lines.append(
274
+ ("fg:ansibrightblack", " define name, description, and tags.")
275
+ )
276
+ lines.append(("", "\n\n"))
277
+
278
+ # Path
279
+ lines.append(("bold", " Path:"))
280
+ lines.append(("", "\n"))
281
+ path_str = str(skill.path)
282
+ if len(path_str) > 45:
283
+ path_str = "..." + path_str[-42:]
284
+ lines.append(("fg:ansibrightblack", f" {path_str}"))
285
+ lines.append(("", "\n"))
286
+
287
+ return lines
288
+
289
+ def _wrap_text(self, text: str, width: int) -> List[str]:
290
+ """Wrap text to specified width."""
291
+ words = text.split()
292
+ lines = []
293
+ current_line = []
294
+ current_length = 0
295
+
296
+ for word in words:
297
+ if current_length + len(word) + 1 <= width:
298
+ current_line.append(word)
299
+ current_length += len(word) + 1
300
+ else:
301
+ if current_line:
302
+ lines.append(" ".join(current_line))
303
+ current_line = [word]
304
+ current_length = len(word)
305
+
306
+ if current_line:
307
+ lines.append(" ".join(current_line))
308
+
309
+ return lines or [""]
310
+
311
+ def update_display(self) -> None:
312
+ """Update the display based on current state."""
313
+ if self.menu_control:
314
+ self.menu_control.text = self._render_skill_list()
315
+ if self.preview_control:
316
+ self.preview_control.text = self._render_skill_details()
317
+
318
+ def run(self) -> bool:
319
+ """Run the interactive skills browser.
320
+
321
+ Returns:
322
+ True if changes were made, False otherwise.
323
+ """
324
+ # Reset per-run state
325
+ self.result = None
326
+
327
+ # Build UI
328
+ self.menu_control = FormattedTextControl(text="")
329
+ self.preview_control = FormattedTextControl(text="")
330
+
331
+ menu_window = Window(
332
+ content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
333
+ )
334
+ preview_window = Window(
335
+ content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
336
+ )
337
+
338
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Skills")
339
+ preview_frame = Frame(
340
+ preview_window, width=Dimension(weight=65), title="Details"
341
+ )
342
+
343
+ root_container = VSplit([menu_frame, preview_frame])
344
+
345
+ # Key bindings
346
+ kb = KeyBindings()
347
+
348
+ @kb.add("up")
349
+ @kb.add("c-p") # Ctrl+P
350
+ @kb.add("k")
351
+ def _(event):
352
+ if self.selected_idx > 0:
353
+ self.selected_idx -= 1
354
+ self.current_page = self.selected_idx // PAGE_SIZE
355
+ self.update_display()
356
+
357
+ @kb.add("down")
358
+ @kb.add("c-n") # Ctrl+N
359
+ @kb.add("j")
360
+ def _(event):
361
+ if self.selected_idx < len(self.skills) - 1:
362
+ self.selected_idx += 1
363
+ self.current_page = self.selected_idx // PAGE_SIZE
364
+ self.update_display()
365
+
366
+ @kb.add("left")
367
+ def _(event):
368
+ """Previous page."""
369
+ if self.current_page > 0:
370
+ self.current_page -= 1
371
+ self.selected_idx = self.current_page * PAGE_SIZE
372
+ self.update_display()
373
+
374
+ @kb.add("right")
375
+ def _(event):
376
+ """Next page."""
377
+ total_pages = (len(self.skills) + PAGE_SIZE - 1) // PAGE_SIZE
378
+ if self.current_page < total_pages - 1:
379
+ self.current_page += 1
380
+ self.selected_idx = self.current_page * PAGE_SIZE
381
+ self.update_display()
382
+
383
+ @kb.add("enter")
384
+ def _(event):
385
+ """Toggle skill enabled/disabled."""
386
+ self._toggle_current_skill()
387
+ self.result = "changed"
388
+
389
+ @kb.add("t")
390
+ def _(event):
391
+ """Toggle skills system on/off."""
392
+ new_state = not self.skills_enabled
393
+ set_skills_enabled(new_state)
394
+ self.skills_enabled = new_state
395
+ self.result = "changed"
396
+ self.update_display()
397
+
398
+ @kb.add("r")
399
+ def _(event):
400
+ """Refresh skills."""
401
+ refresh_skill_cache()
402
+ self._refresh_data()
403
+ self.update_display()
404
+
405
+ @kb.add("c-a")
406
+ def _(event):
407
+ """Add a skill directory."""
408
+ self.result = "add_directory"
409
+ event.app.exit()
410
+
411
+ @kb.add("c-d")
412
+ def _(event):
413
+ """Show/manage directories."""
414
+ self.result = "show_directories"
415
+ event.app.exit()
416
+
417
+ @kb.add("i")
418
+ def _(event):
419
+ """Install skills from catalog."""
420
+ self.result = "install"
421
+ event.app.exit()
422
+
423
+ @kb.add("q")
424
+ @kb.add("escape")
425
+ def _(event):
426
+ self.result = "quit"
427
+ event.app.exit()
428
+
429
+ @kb.add("c-c")
430
+ def _(event):
431
+ self.result = "quit"
432
+ event.app.exit()
433
+
434
+ layout = Layout(root_container)
435
+ app = Application(
436
+ layout=layout,
437
+ key_bindings=kb,
438
+ full_screen=False,
439
+ mouse_support=False,
440
+ )
441
+
442
+ set_awaiting_user_input(True)
443
+
444
+ # Enter alternate screen buffer
445
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
446
+ sys.stdout.write("\033[2J\033[H") # Clear and home
447
+ sys.stdout.flush()
448
+ time.sleep(0.05)
449
+
450
+ try:
451
+ # Initial display
452
+ self.update_display()
453
+
454
+ # Clear the buffer
455
+ sys.stdout.write("\033[2J\033[H")
456
+ sys.stdout.flush()
457
+
458
+ # Run application in a background thread to avoid event loop conflicts
459
+ app.run(in_thread=True)
460
+
461
+ finally:
462
+ # Exit alternate screen buffer
463
+ sys.stdout.write("\033[?1049l")
464
+ sys.stdout.flush()
465
+
466
+ # Flush any buffered input to prevent stale keypresses
467
+ try:
468
+ import termios
469
+
470
+ termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
471
+ except Exception:
472
+ pass # ImportError on Windows, termios.error, or not a tty
473
+
474
+ # Small delay to let terminal settle before any output
475
+ time.sleep(0.1)
476
+ set_awaiting_user_input(False)
477
+
478
+ return self.result
479
+
480
+
481
+ def _prompt_for_directory() -> Optional[str]:
482
+ """Prompt user for a directory path to add."""
483
+ from code_puppy.tools.common import safe_input
484
+
485
+ try:
486
+ print("\n" + "=" * 60)
487
+ print("ADD SKILL DIRECTORY")
488
+ print("=" * 60)
489
+ print("\nEnter the path to a directory containing skills.")
490
+ print("Examples:")
491
+ print(" ~/.claude/skills")
492
+ print(" /opt/shared-skills")
493
+ print(" ./my-project-skills")
494
+ print("\nPress Ctrl+C to cancel.\n")
495
+
496
+ path = safe_input("Directory path: ").strip()
497
+ if path:
498
+ # Expand ~ to home directory
499
+ expanded = os.path.expanduser(path)
500
+ return expanded
501
+ except (KeyboardInterrupt, EOFError):
502
+ print("\nCancelled.")
503
+ return None
504
+
505
+
506
+ def _show_directories_menu() -> Optional[str]:
507
+ """Show current directories and allow removal."""
508
+ from code_puppy.tools.common import safe_input
509
+
510
+ try:
511
+ dirs = get_skill_directories()
512
+
513
+ print("\n" + "=" * 60)
514
+ print("SKILL DIRECTORIES")
515
+ print("=" * 60)
516
+ print("\nCurrently configured directories:\n")
517
+
518
+ if not dirs:
519
+ print(" (no directories configured)")
520
+ else:
521
+ for i, d in enumerate(dirs, 1):
522
+ exists = os.path.isdir(os.path.expanduser(d))
523
+ status = "✓" if exists else "✗ (not found)"
524
+ print(f" {i}. {d} {status}")
525
+
526
+ print("\nOptions:")
527
+ print(" Enter a number to remove that directory")
528
+ print(" Press Enter or Ctrl+C to go back\n")
529
+
530
+ choice = safe_input("Choice: ").strip()
531
+ if choice and choice.isdigit():
532
+ idx = int(choice) - 1
533
+ if 0 <= idx < len(dirs):
534
+ dir_to_remove = dirs[idx]
535
+ confirm = (
536
+ safe_input(f"Remove '{dir_to_remove}'? (y/N): ").strip().lower()
537
+ )
538
+ if confirm in ("y", "yes"):
539
+ remove_skill_directory(dir_to_remove)
540
+ print(f"Removed: {dir_to_remove}")
541
+ return "changed"
542
+ except (KeyboardInterrupt, EOFError):
543
+ print("\nCancelled.")
544
+ return None
545
+
546
+
547
+ def show_skills_menu() -> bool:
548
+ """Launch the interactive skills TUI menu.
549
+
550
+ Returns:
551
+ True if changes were made, False otherwise.
552
+ """
553
+
554
+ changes_made = False
555
+
556
+ while True:
557
+ menu = SkillsMenu()
558
+ result = menu.run()
559
+
560
+ if result == "add_directory":
561
+ # Prompt for directory to add
562
+ new_dir = _prompt_for_directory()
563
+ if new_dir:
564
+ if add_skill_directory(new_dir):
565
+ emit_success(f"Added skill directory: {new_dir}")
566
+ changes_made = True
567
+ else:
568
+ emit_warning(f"Directory already configured: {new_dir}")
569
+ # Re-run the menu
570
+ continue
571
+
572
+ elif result == "show_directories":
573
+ # Show directories management
574
+ dir_result = _show_directories_menu()
575
+ if dir_result == "changed":
576
+ changes_made = True
577
+ # Re-run the menu
578
+ continue
579
+
580
+ elif result == "install":
581
+ from code_puppy.plugins.agent_skills.skills_install_menu import (
582
+ run_skills_install_menu,
583
+ )
584
+
585
+ install_result = run_skills_install_menu()
586
+ if install_result:
587
+ changes_made = True
588
+ continue # Re-run the skills menu after install
589
+
590
+ elif result == "changed":
591
+ changes_made = True
592
+ break
593
+ elif result == "quit":
594
+ break
595
+ else:
596
+ # User quit or no-op
597
+ break
598
+
599
+ return changes_made
600
+
601
+
602
+ def list_skills() -> bool:
603
+ """List all discovered skills in a simple format.
604
+
605
+ Returns:
606
+ True if successful, False otherwise.
607
+ """
608
+ try:
609
+ skills = discover_skills()
610
+ disabled_skills = get_disabled_skills()
611
+
612
+ if not skills:
613
+ emit_info(
614
+ "No skills found. Create skills in ~/.code_puppy/skills/ or ./skills/"
615
+ )
616
+ return True
617
+
618
+ emit_info(f"\nFound {len(skills)} skill(s):\n")
619
+
620
+ for skill in skills:
621
+ metadata = parse_skill_metadata(skill.path)
622
+ if metadata:
623
+ is_disabled = metadata.name in disabled_skills
624
+ status = "enabled" if not is_disabled else "disabled"
625
+ emit_info(f" [{status}] {metadata.name}")
626
+ if metadata.description:
627
+ emit_info(f" {metadata.description}")
628
+ resources = get_skill_resources(metadata.path)
629
+ if resources:
630
+ emit_info(f" Resources: {len(resources)}")
631
+ else:
632
+ is_disabled = skill.name in disabled_skills
633
+ status = "enabled" if not is_disabled else "disabled"
634
+ emit_info(f" [{status}] {skill.name} (no metadata)")
635
+
636
+ return True
637
+ except Exception as e:
638
+ emit_error(f"Failed to list skills: {e}")
639
+ return False
640
+
641
+
642
+ def handle_skills_command(args: list[str]) -> bool:
643
+ """Handle skills subcommands from the CLI.
644
+
645
+ Args:
646
+ args: List of command arguments (e.g., ['enable', 'my-skill'])
647
+
648
+ Returns:
649
+ True if successful, False otherwise.
650
+ """
651
+ if not args:
652
+ # Show interactive TUI
653
+ return show_skills_menu()
654
+
655
+ command = args[0].lower()
656
+
657
+ if command == "list":
658
+ return list_skills()
659
+ elif command == "enable":
660
+ if len(args) < 2:
661
+ emit_error("Usage: /skills enable <skill-name>")
662
+ return False
663
+ return _enable_skill(args[1])
664
+ elif command == "disable":
665
+ if len(args) < 2:
666
+ emit_error("Usage: /skills disable <skill-name>")
667
+ return False
668
+ return _disable_skill(args[1])
669
+ elif command == "toggle":
670
+ return _toggle_skills_integration()
671
+ elif command == "refresh":
672
+ return _refresh_skills()
673
+ elif command == "help":
674
+ _show_help()
675
+ return True
676
+ else:
677
+ emit_error(f"Unknown command: {command}")
678
+ emit_info("Use '/skills help' to see available commands.")
679
+ return False
680
+
681
+
682
+ def _enable_skill(skill_name: str) -> bool:
683
+ """Enable a specific skill."""
684
+ try:
685
+ skills = discover_skills()
686
+ skill_names = [s.name for s in skills]
687
+
688
+ # Also check metadata names
689
+ for skill in skills:
690
+ metadata = parse_skill_metadata(skill.path)
691
+ if metadata:
692
+ skill_names.append(metadata.name)
693
+
694
+ if skill_name not in skill_names:
695
+ emit_error(f"Skill '{skill_name}' not found.")
696
+ return False
697
+
698
+ disabled = get_disabled_skills()
699
+ if skill_name not in disabled:
700
+ emit_info(f"Skill '{skill_name}' is already enabled.")
701
+ return True
702
+
703
+ set_skill_disabled(skill_name, False)
704
+ refresh_skill_cache()
705
+ emit_success(f"Skill '{skill_name}' has been enabled.")
706
+ return True
707
+ except Exception as e:
708
+ emit_error(f"Failed to enable skill '{skill_name}': {e}")
709
+ return False
710
+
711
+
712
+ def _disable_skill(skill_name: str) -> bool:
713
+ """Disable a specific skill."""
714
+ try:
715
+ skills = discover_skills()
716
+ skill_names = [s.name for s in skills]
717
+
718
+ # Also check metadata names
719
+ for skill in skills:
720
+ metadata = parse_skill_metadata(skill.path)
721
+ if metadata:
722
+ skill_names.append(metadata.name)
723
+
724
+ if skill_name not in skill_names:
725
+ emit_error(f"Skill '{skill_name}' not found.")
726
+ return False
727
+
728
+ disabled = get_disabled_skills()
729
+ if skill_name in disabled:
730
+ emit_info(f"Skill '{skill_name}' is already disabled.")
731
+ return True
732
+
733
+ set_skill_disabled(skill_name, True)
734
+ refresh_skill_cache()
735
+ emit_success(f"Skill '{skill_name}' has been disabled.")
736
+ return True
737
+ except Exception as e:
738
+ emit_error(f"Failed to disable skill '{skill_name}': {e}")
739
+ return False
740
+
741
+
742
+ def _toggle_skills_integration() -> bool:
743
+ """Toggle skills integration on/off."""
744
+ try:
745
+ current = get_skills_enabled()
746
+ new_state = not current
747
+ set_skills_enabled(new_state)
748
+
749
+ if new_state:
750
+ emit_success("Skills integration has been enabled.")
751
+ else:
752
+ emit_warning("Skills integration has been disabled.")
753
+
754
+ return True
755
+ except Exception as e:
756
+ emit_error(f"Failed to toggle skills integration: {e}")
757
+ return False
758
+
759
+
760
+ def _refresh_skills() -> bool:
761
+ """Refresh the skill cache."""
762
+ try:
763
+ emit_info("Refreshing skill cache...")
764
+ refresh_skill_cache()
765
+ emit_success("Skill cache refreshed successfully.")
766
+ return True
767
+ except Exception as e:
768
+ emit_error(f"Failed to refresh skill cache: {e}")
769
+ return False
770
+
771
+
772
+ def _show_help() -> None:
773
+ """Show help information."""
774
+ emit_info("Available commands:")
775
+ emit_info(" /skills - Show interactive TUI")
776
+ emit_info(" /skills list - List all skills")
777
+ emit_info(" /skills enable <name> - Enable a skill")
778
+ emit_info(" /skills disable <name> - Disable a skill")
779
+ emit_info(" /skills toggle - Toggle skills integration")
780
+ emit_info(" /skills refresh - Refresh skill cache")
781
+ emit_info(" /skills help - Show this help")