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,241 @@
1
+ """Agent Skills plugin - registers callbacks for skill integration.
2
+
3
+ This plugin:
4
+ 1. Injects available skills into system prompts
5
+ 2. Registers skill-related tools
6
+ 3. Provides /skills slash command (and alias /skill)
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Tuple
12
+
13
+ from code_puppy.callbacks import register_callback
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _get_skills_prompt_section() -> Optional[str]:
19
+ """Build the skills section to inject into system prompts.
20
+
21
+ Returns None if skills are disabled or no skills found.
22
+ """
23
+ from .config import get_disabled_skills, get_skill_directories, get_skills_enabled
24
+ from .discovery import discover_skills
25
+ from .metadata import SkillMetadata, parse_skill_metadata
26
+ from .prompt_builder import build_available_skills_xml, build_skills_guidance
27
+
28
+ # 1. Check if enabled
29
+ if not get_skills_enabled():
30
+ logger.debug("Skills integration is disabled, skipping prompt injection")
31
+ return None
32
+
33
+ # 2. Discover skills
34
+ skill_dirs = [Path(d) for d in get_skill_directories()]
35
+ discovered = discover_skills(skill_dirs)
36
+
37
+ if not discovered:
38
+ logger.debug("No skills discovered, skipping prompt injection")
39
+ return None
40
+
41
+ # 3. Parse metadata for each and filter out disabled skills
42
+ disabled_skills = get_disabled_skills()
43
+ skills_metadata: List[SkillMetadata] = []
44
+
45
+ for skill_info in discovered:
46
+ # Skip disabled skills
47
+ if skill_info.name in disabled_skills:
48
+ logger.debug(f"Skipping disabled skill: {skill_info.name}")
49
+ continue
50
+
51
+ # Only include skills with valid SKILL.md
52
+ if not skill_info.has_skill_md:
53
+ logger.debug(f"Skipping skill without SKILL.md: {skill_info.name}")
54
+ continue
55
+
56
+ # Parse metadata
57
+ metadata = parse_skill_metadata(skill_info.path)
58
+ if metadata:
59
+ skills_metadata.append(metadata)
60
+ else:
61
+ logger.warning(f"Failed to parse metadata for skill: {skill_info.name}")
62
+
63
+ # 4. Build XML + guidance
64
+ if not skills_metadata:
65
+ logger.debug("No valid skills with metadata found, skipping prompt injection")
66
+ return None
67
+
68
+ xml_section = build_available_skills_xml(skills_metadata)
69
+ guidance = build_skills_guidance()
70
+
71
+ # 5. Return combined string
72
+ combined = f"{xml_section}\n\n{guidance}"
73
+ logger.debug(f"Injecting skills section with {len(skills_metadata)} skills")
74
+ return combined
75
+
76
+
77
+ def _inject_skills_into_prompt(
78
+ model_name: str, default_system_prompt: str, user_prompt: str
79
+ ) -> Optional[Dict[str, Any]]:
80
+ """Callback to inject skills into system prompt.
81
+
82
+ This is registered with the 'get_model_system_prompt' callback phase.
83
+ """
84
+ skills_section = _get_skills_prompt_section()
85
+
86
+ if not skills_section:
87
+ return None # No skills, don't modify prompt
88
+
89
+ # Append skills section to system prompt
90
+ enhanced_prompt = f"{default_system_prompt}\n\n{skills_section}"
91
+
92
+ return {
93
+ "instructions": enhanced_prompt,
94
+ "user_prompt": user_prompt,
95
+ "handled": False, # Let other handlers also process
96
+ }
97
+
98
+
99
+ def _register_skills_tools() -> List[Dict[str, Any]]:
100
+ """Callback to register skills tools.
101
+
102
+ This is registered with the 'register_tools' callback phase.
103
+ Returns tool definitions for the tool registry.
104
+ """
105
+ from code_puppy.tools.skills_tools import (
106
+ register_activate_skill,
107
+ register_list_or_search_skills,
108
+ )
109
+
110
+ return [
111
+ {"name": "activate_skill", "register_func": register_activate_skill},
112
+ {
113
+ "name": "list_or_search_skills",
114
+ "register_func": register_list_or_search_skills,
115
+ },
116
+ ]
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Slash command: /skills (and alias /skill)
121
+ # ---------------------------------------------------------------------------
122
+
123
+ _COMMAND_NAME = "skills"
124
+ _ALIASES = ("skill",)
125
+
126
+
127
+ def _skills_command_help() -> List[Tuple[str, str]]:
128
+ """Advertise /skills in the /help menu."""
129
+ return [
130
+ ("skills", "Manage agent skills – browse, enable, disable, install"),
131
+ ("skill", "Alias for /skills"),
132
+ ]
133
+
134
+
135
+ def _handle_skills_command(command: str, name: str) -> Optional[Any]:
136
+ """Handle /skills and /skill slash commands.
137
+
138
+ Sub-commands:
139
+ /skills – Launch interactive TUI menu
140
+ /skills list – Quick text list of all skills
141
+ /skills install – Browse & install from remote catalog
142
+ /skills enable – Enable skills integration globally
143
+ /skills disable – Disable skills integration globally
144
+ """
145
+ if name not in (_COMMAND_NAME, *_ALIASES):
146
+ return None
147
+
148
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
149
+ from code_puppy.plugins.agent_skills.config import (
150
+ get_disabled_skills,
151
+ get_skills_enabled,
152
+ set_skills_enabled,
153
+ )
154
+ from code_puppy.plugins.agent_skills.discovery import discover_skills
155
+ from code_puppy.plugins.agent_skills.metadata import parse_skill_metadata
156
+ from code_puppy.plugins.agent_skills.skills_menu import show_skills_menu
157
+
158
+ tokens = command.split()
159
+
160
+ if len(tokens) > 1:
161
+ subcommand = tokens[1].lower()
162
+
163
+ if subcommand == "list":
164
+ disabled_skills = get_disabled_skills()
165
+ skills = discover_skills()
166
+ enabled = get_skills_enabled()
167
+
168
+ if not skills:
169
+ emit_info("No skills found.")
170
+ emit_info("Create skills in:")
171
+ emit_info(" - ~/.code_puppy/skills/")
172
+ emit_info(" - ./skills/")
173
+ return True
174
+
175
+ emit_info(
176
+ f"\U0001f6e0\ufe0f Skills (integration: {'enabled' if enabled else 'disabled'})"
177
+ )
178
+ emit_info(f"Found {len(skills)} skill(s):\n")
179
+
180
+ for skill in skills:
181
+ metadata = parse_skill_metadata(skill.path)
182
+ if metadata:
183
+ status = (
184
+ "\U0001f534 disabled"
185
+ if metadata.name in disabled_skills
186
+ else "\U0001f7e2 enabled"
187
+ )
188
+ version_str = f" v{metadata.version}" if metadata.version else ""
189
+ author_str = f" by {metadata.author}" if metadata.author else ""
190
+ emit_info(f" {status} {metadata.name}{version_str}{author_str}")
191
+ emit_info(f" {metadata.description}")
192
+ if metadata.tags:
193
+ emit_info(f" tags: {', '.join(metadata.tags)}")
194
+ else:
195
+ status = (
196
+ "\U0001f534 disabled"
197
+ if skill.name in disabled_skills
198
+ else "\U0001f7e2 enabled"
199
+ )
200
+ emit_info(f" {status} {skill.name}")
201
+ emit_info(" (no SKILL.md metadata found)")
202
+ emit_info("")
203
+ return True
204
+
205
+ elif subcommand == "install":
206
+ from code_puppy.plugins.agent_skills.skills_install_menu import (
207
+ run_skills_install_menu,
208
+ )
209
+
210
+ run_skills_install_menu()
211
+ return True
212
+
213
+ elif subcommand == "enable":
214
+ set_skills_enabled(True)
215
+ emit_success("\u2705 Skills integration enabled globally")
216
+ return True
217
+
218
+ elif subcommand == "disable":
219
+ set_skills_enabled(False)
220
+ emit_warning("\U0001f534 Skills integration disabled globally")
221
+ return True
222
+
223
+ else:
224
+ emit_error(f"Unknown subcommand: {subcommand}")
225
+ emit_info("Usage: /skills [list|install|enable|disable]")
226
+ return True
227
+
228
+ # No subcommand – launch TUI menu
229
+ show_skills_menu()
230
+ return True
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Register all callbacks
235
+ # ---------------------------------------------------------------------------
236
+ register_callback("get_model_system_prompt", _inject_skills_into_prompt)
237
+ register_callback("register_tools", _register_skills_tools)
238
+ register_callback("custom_command_help", _skills_command_help)
239
+ register_callback("custom_command", _handle_skills_command)
240
+
241
+ logger.info("Agent Skills plugin loaded")
@@ -0,0 +1,322 @@
1
+ """Remote skills catalog client.
2
+
3
+ Fetches the remote skills catalog JSON and exposes a cached, parsed view.
4
+
5
+ Design goals:
6
+ - Never crash the app (defensive parsing + broad error handling).
7
+ - Local caching with TTL for fast startup and offline use.
8
+ - Synchronous networking only (httpx.Client).
9
+
10
+ Schema source:
11
+ https://www.llmspec.dev/skills/skills.json
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import time
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Any, Optional
22
+ from urllib.parse import urljoin
23
+
24
+ import httpx
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ SKILLS_JSON_URL = "https://www.llmspec.dev/skills/skills.json"
29
+
30
+ _CACHE_DIR = Path.home() / ".code_puppy" / "cache"
31
+ _CACHE_PATH = _CACHE_DIR / "skills_catalog.json"
32
+ _CACHE_TTL_SECONDS = 30 * 60
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class RemoteSkillEntry:
37
+ """Flattened remote skill entry."""
38
+
39
+ name: str
40
+ description: str
41
+ group: str
42
+ download_url: str
43
+ zip_size_bytes: int
44
+ file_count: int
45
+ has_scripts: bool
46
+ has_references: bool
47
+ has_license: bool
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class RemoteCatalogData:
52
+ """Parsed remote catalog.
53
+
54
+ Attributes:
55
+ version: Catalog version string.
56
+ base_url: Base URL used to build absolute download_url values.
57
+ total_skills: Total number of skills in the remote catalog.
58
+ groups: Raw group objects from the JSON (kept as dicts for flexibility).
59
+ entries: Flattened list of all skills across all groups.
60
+ """
61
+
62
+ version: str
63
+ base_url: str
64
+ total_skills: int
65
+ groups: list[dict[str, Any]]
66
+ entries: list[RemoteSkillEntry]
67
+
68
+
69
+ def _safe_int(value: Any, default: int = 0) -> int:
70
+ """Convert value to int, returning default on failure."""
71
+
72
+ try:
73
+ if value is None:
74
+ return default
75
+ return int(value)
76
+ except Exception:
77
+ return default
78
+
79
+
80
+ def _safe_bool(value: Any, default: bool = False) -> bool:
81
+ """Convert value to bool, returning default on failure."""
82
+
83
+ if value is None:
84
+ return default
85
+ return bool(value)
86
+
87
+
88
+ def _cache_is_fresh(cache_path: Path, ttl_seconds: int) -> bool:
89
+ """Check whether the on-disk catalog cache is within TTL."""
90
+
91
+ try:
92
+ if not cache_path.exists():
93
+ return False
94
+ age_seconds = time.time() - cache_path.stat().st_mtime
95
+ return age_seconds <= ttl_seconds
96
+ except Exception as e:
97
+ logger.debug(f"Failed to check cache age for {cache_path}: {e}")
98
+ return False
99
+
100
+
101
+ def _read_cache(cache_path: Path) -> Optional[dict[str, Any]]:
102
+ """Read and deserialize the cached catalog JSON from disk."""
103
+
104
+ try:
105
+ if not cache_path.exists():
106
+ return None
107
+ raw = cache_path.read_text(encoding="utf-8")
108
+ data = json.loads(raw)
109
+ if not isinstance(data, dict):
110
+ logger.warning(f"Cache JSON is not an object: {cache_path}")
111
+ return None
112
+ return data
113
+ except Exception as e:
114
+ logger.warning(f"Failed to read cache {cache_path}: {e}")
115
+ return None
116
+
117
+
118
+ def _write_cache(cache_path: Path, data: dict[str, Any]) -> bool:
119
+ """Serialize and write catalog JSON to the disk cache."""
120
+
121
+ try:
122
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
123
+ # Stable formatting so diffs are readable when debugging.
124
+ cache_path.write_text(
125
+ json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8"
126
+ )
127
+ return True
128
+ except Exception as e:
129
+ logger.warning(f"Failed to write cache {cache_path}: {e}")
130
+ return False
131
+
132
+
133
+ def _fetch_remote_json(url: str) -> Optional[dict[str, Any]]:
134
+ """Fetch the skills catalog JSON from the remote URL."""
135
+
136
+ headers = {
137
+ "Accept": "application/json",
138
+ "User-Agent": "code-puppy/remote-catalog",
139
+ }
140
+
141
+ try:
142
+ with httpx.Client(timeout=15, headers=headers) as client:
143
+ response = client.get(url)
144
+ response.raise_for_status()
145
+ data = response.json()
146
+
147
+ if not isinstance(data, dict):
148
+ logger.error(f"Remote catalog JSON was not an object. Got: {type(data)}")
149
+ return None
150
+
151
+ return data
152
+
153
+ except httpx.HTTPStatusError as e:
154
+ logger.warning(
155
+ "Remote catalog request returned bad status: "
156
+ f"{e.response.status_code} {e.response.reason_phrase}"
157
+ )
158
+ return None
159
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError) as e:
160
+ logger.warning(f"Remote catalog network failure: {e}")
161
+ return None
162
+ except json.JSONDecodeError as e:
163
+ logger.warning(f"Remote catalog returned invalid JSON: {e}")
164
+ return None
165
+ except Exception as e:
166
+ logger.exception(f"Unexpected error fetching remote catalog: {e}")
167
+ return None
168
+
169
+
170
+ def _parse_catalog(raw: dict[str, Any]) -> Optional[RemoteCatalogData]:
171
+ """Parse raw JSON dicts into a list of RemoteSkillEntry objects."""
172
+
173
+ try:
174
+ version = str(raw.get("version") or "")
175
+ base_url = str(raw.get("base_url") or "")
176
+ total_skills = _safe_int(raw.get("total_skills"), default=0)
177
+
178
+ raw_groups = raw.get("groups")
179
+ if not isinstance(raw_groups, list):
180
+ logger.warning("Remote catalog 'groups' missing or not a list")
181
+ raw_groups = []
182
+
183
+ groups: list[dict[str, Any]] = []
184
+ entries: list[RemoteSkillEntry] = []
185
+
186
+ # Ensure urljoin behaves (needs trailing slash on base).
187
+ base_for_join = base_url.rstrip("/") + "/" if base_url else ""
188
+
189
+ for group_obj in raw_groups:
190
+ if not isinstance(group_obj, dict):
191
+ continue
192
+ groups.append(group_obj)
193
+
194
+ group_slug = str(group_obj.get("slug") or group_obj.get("name") or "")
195
+ skills = group_obj.get("skills")
196
+ if not isinstance(skills, list):
197
+ continue
198
+
199
+ for skill in skills:
200
+ if not isinstance(skill, dict):
201
+ continue
202
+
203
+ name = str(skill.get("name") or "").strip()
204
+ if not name:
205
+ # If name is missing, it can't be indexed/activated anyway.
206
+ continue
207
+
208
+ description = str(skill.get("description") or "")
209
+ group = str(skill.get("group") or group_slug or "")
210
+
211
+ download_path = str(skill.get("download_url") or "")
212
+ download_url = (
213
+ urljoin(base_for_join, download_path)
214
+ if base_for_join
215
+ else download_path
216
+ )
217
+
218
+ contents = skill.get("contents")
219
+ if not isinstance(contents, dict):
220
+ contents = {}
221
+
222
+ entries.append(
223
+ RemoteSkillEntry(
224
+ name=name,
225
+ description=description,
226
+ group=group,
227
+ download_url=download_url,
228
+ zip_size_bytes=_safe_int(
229
+ skill.get("zip_size_bytes"), default=0
230
+ ),
231
+ file_count=_safe_int(skill.get("file_count"), default=0),
232
+ has_scripts=_safe_bool(
233
+ contents.get("has_scripts"), default=False
234
+ ),
235
+ has_references=_safe_bool(
236
+ contents.get("has_references"), default=False
237
+ ),
238
+ has_license=_safe_bool(
239
+ contents.get("has_license"), default=False
240
+ ),
241
+ )
242
+ )
243
+
244
+ if not version:
245
+ logger.debug("Remote catalog 'version' is missing/empty")
246
+ if not base_url:
247
+ logger.debug("Remote catalog 'base_url' is missing/empty")
248
+
249
+ return RemoteCatalogData(
250
+ version=version,
251
+ base_url=base_url,
252
+ total_skills=total_skills,
253
+ groups=groups,
254
+ entries=entries,
255
+ )
256
+
257
+ except Exception as e:
258
+ logger.exception(f"Failed to parse remote catalog JSON: {e}")
259
+ return None
260
+
261
+
262
+ def fetch_remote_catalog(force_refresh: bool = False) -> Optional[RemoteCatalogData]:
263
+ """Fetch the remote skills catalog with caching and offline fallback.
264
+
265
+ Cache behavior:
266
+ - Cache file: ~/.code_puppy/cache/skills_catalog.json
267
+ - TTL: 30 minutes (based on file mtime)
268
+ - Offline fallback: if network fetch fails, use cache if present (even if expired)
269
+
270
+ Args:
271
+ force_refresh: If True, always attempt a network fetch.
272
+
273
+ Returns:
274
+ Parsed RemoteCatalogData on success, otherwise None.
275
+ """
276
+
277
+ cache_fresh = _cache_is_fresh(_CACHE_PATH, _CACHE_TTL_SECONDS)
278
+
279
+ # Use fresh cache unless forced.
280
+ if not force_refresh and cache_fresh:
281
+ logger.info(f"Using fresh remote catalog cache: {_CACHE_PATH}")
282
+ cached = _read_cache(_CACHE_PATH)
283
+ if cached is None:
284
+ logger.warning("Fresh cache exists but could not be read; refetching")
285
+ else:
286
+ parsed = _parse_catalog(cached)
287
+ if parsed is not None:
288
+ return parsed
289
+ logger.warning("Fresh cache exists but could not be parsed; refetching")
290
+
291
+ if force_refresh:
292
+ logger.info("Force refresh enabled; fetching remote skills catalog")
293
+ elif _CACHE_PATH.exists():
294
+ logger.info(
295
+ "Cache is missing or stale; fetching remote skills catalog "
296
+ f"(cache_path={_CACHE_PATH}, fresh={cache_fresh})"
297
+ )
298
+ else:
299
+ logger.info("No cache present; fetching remote skills catalog")
300
+
301
+ remote_raw = _fetch_remote_json(SKILLS_JSON_URL)
302
+ if remote_raw is not None:
303
+ logger.info("Fetched remote skills catalog successfully")
304
+ _write_cache(_CACHE_PATH, remote_raw)
305
+ parsed = _parse_catalog(remote_raw)
306
+ if parsed is not None:
307
+ return parsed
308
+ logger.warning("Remote catalog fetched but failed to parse")
309
+
310
+ # Offline fallback: use cache even if expired.
311
+ if _CACHE_PATH.exists():
312
+ logger.warning(
313
+ "Remote fetch failed; falling back to cached skills catalog "
314
+ f"(even if expired): {_CACHE_PATH}"
315
+ )
316
+ cached = _read_cache(_CACHE_PATH)
317
+ if cached is None:
318
+ return None
319
+ return _parse_catalog(cached)
320
+
321
+ logger.error("Remote fetch failed and no cache is available")
322
+ return None