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,186 @@
1
+ import importlib
2
+ import importlib.util
3
+ import logging
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # User plugins directory
10
+ USER_PLUGINS_DIR = Path.home() / ".code_puppy" / "plugins"
11
+
12
+ # Track if plugins have already been loaded to prevent duplicate registration
13
+ _PLUGINS_LOADED = False
14
+
15
+
16
+ def _load_builtin_plugins(plugins_dir: Path) -> list[str]:
17
+ """Load built-in plugins from the package plugins directory.
18
+
19
+ Returns list of successfully loaded plugin names.
20
+ """
21
+ # Import safety permission check for shell_safety plugin
22
+ from code_puppy.config import get_safety_permission_level
23
+
24
+ loaded = []
25
+
26
+ for item in plugins_dir.iterdir():
27
+ if item.is_dir() and not item.name.startswith("_"):
28
+ plugin_name = item.name
29
+ callbacks_file = item / "register_callbacks.py"
30
+
31
+ if callbacks_file.exists():
32
+ # Skip shell_safety plugin unless safety_permission_level is "low" or "none"
33
+ if plugin_name == "shell_safety":
34
+ safety_level = get_safety_permission_level()
35
+ if safety_level not in ("none", "low"):
36
+ logger.debug(
37
+ f"Skipping shell_safety plugin - safety_permission_level is '{safety_level}' (needs 'low' or 'none')"
38
+ )
39
+ continue
40
+
41
+ try:
42
+ module_name = f"code_puppy.plugins.{plugin_name}.register_callbacks"
43
+ importlib.import_module(module_name)
44
+ loaded.append(plugin_name)
45
+ except ImportError as e:
46
+ logger.warning(
47
+ f"Failed to import callbacks from built-in plugin {plugin_name}: {e}"
48
+ )
49
+ except Exception as e:
50
+ logger.error(
51
+ f"Unexpected error loading built-in plugin {plugin_name}: {e}"
52
+ )
53
+
54
+ return loaded
55
+
56
+
57
+ def _load_user_plugins(user_plugins_dir: Path) -> list[str]:
58
+ """Load user plugins from ~/.code_puppy/plugins/.
59
+
60
+ Each plugin should be a directory containing a register_callbacks.py file.
61
+ Plugins are loaded by adding their parent to sys.path and importing them.
62
+
63
+ Returns list of successfully loaded plugin names.
64
+ """
65
+ loaded = []
66
+
67
+ if not user_plugins_dir.exists():
68
+ return loaded
69
+
70
+ if not user_plugins_dir.is_dir():
71
+ logger.warning(f"User plugins path is not a directory: {user_plugins_dir}")
72
+ return loaded
73
+
74
+ # Add user plugins directory to sys.path if not already there
75
+ user_plugins_str = str(user_plugins_dir)
76
+ if user_plugins_str not in sys.path:
77
+ sys.path.insert(0, user_plugins_str)
78
+
79
+ for item in user_plugins_dir.iterdir():
80
+ if (
81
+ item.is_dir()
82
+ and not item.name.startswith("_")
83
+ and not item.name.startswith(".")
84
+ ):
85
+ plugin_name = item.name
86
+ callbacks_file = item / "register_callbacks.py"
87
+
88
+ if callbacks_file.exists():
89
+ try:
90
+ # Load the plugin module directly from the file
91
+ module_name = f"{plugin_name}.register_callbacks"
92
+ spec = importlib.util.spec_from_file_location(
93
+ module_name, callbacks_file
94
+ )
95
+ if spec is None or spec.loader is None:
96
+ logger.warning(
97
+ f"Could not create module spec for user plugin: {plugin_name}"
98
+ )
99
+ continue
100
+
101
+ module = importlib.util.module_from_spec(spec)
102
+ sys.modules[module_name] = module
103
+
104
+ spec.loader.exec_module(module)
105
+ loaded.append(plugin_name)
106
+
107
+ except ImportError as e:
108
+ logger.warning(
109
+ f"Failed to import callbacks from user plugin {plugin_name}: {e}"
110
+ )
111
+ except Exception as e:
112
+ logger.error(
113
+ f"Unexpected error loading user plugin {plugin_name}: {e}",
114
+ exc_info=True,
115
+ )
116
+ else:
117
+ # Check if there's an __init__.py - might be a simple plugin
118
+ init_file = item / "__init__.py"
119
+ if init_file.exists():
120
+ try:
121
+ module_name = plugin_name
122
+ spec = importlib.util.spec_from_file_location(
123
+ module_name, init_file
124
+ )
125
+ if spec is None or spec.loader is None:
126
+ continue
127
+
128
+ module = importlib.util.module_from_spec(spec)
129
+ sys.modules[module_name] = module
130
+ spec.loader.exec_module(module)
131
+ loaded.append(plugin_name)
132
+
133
+ except Exception as e:
134
+ logger.error(
135
+ f"Unexpected error loading user plugin {plugin_name}: {e}",
136
+ exc_info=True,
137
+ )
138
+
139
+ return loaded
140
+
141
+
142
+ def load_plugin_callbacks() -> dict[str, list[str]]:
143
+ """Dynamically load register_callbacks.py from all plugin sources.
144
+
145
+ Loads plugins from:
146
+ 1. Built-in plugins in the code_puppy/plugins/ directory
147
+ 2. User plugins in ~/.code_puppy/plugins/
148
+
149
+ Returns dict with 'builtin' and 'user' keys containing lists of loaded plugin names.
150
+
151
+ NOTE: This function is idempotent - calling it multiple times will only
152
+ load plugins once. Subsequent calls return empty lists.
153
+ """
154
+ global _PLUGINS_LOADED
155
+
156
+ # Prevent duplicate loading - plugins register callbacks at import time,
157
+ # so re-importing would cause duplicate registrations
158
+ if _PLUGINS_LOADED:
159
+ logger.debug("Plugins already loaded, skipping duplicate load")
160
+ return {"builtin": [], "user": []}
161
+
162
+ plugins_dir = Path(__file__).parent
163
+
164
+ result = {
165
+ "builtin": _load_builtin_plugins(plugins_dir),
166
+ "user": _load_user_plugins(USER_PLUGINS_DIR),
167
+ }
168
+
169
+ _PLUGINS_LOADED = True
170
+ logger.debug(f"Loaded plugins: builtin={result['builtin']}, user={result['user']}")
171
+
172
+ return result
173
+
174
+
175
+ def get_user_plugins_dir() -> Path:
176
+ """Return the path to the user plugins directory."""
177
+ return USER_PLUGINS_DIR
178
+
179
+
180
+ def ensure_user_plugins_dir() -> Path:
181
+ """Create the user plugins directory if it doesn't exist.
182
+
183
+ Returns the path to the directory.
184
+ """
185
+ USER_PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
186
+ return USER_PLUGINS_DIR
@@ -0,0 +1,22 @@
1
+ """Agent Skills plugin - dynamic skill loading and discovery.
2
+
3
+ This plugin enables code_puppy to discover, load, and use custom skills
4
+ defined in SKILL.md files. Skills can be placed in user-specific or
5
+ project-specific directories for easy sharing and organization.
6
+ """
7
+
8
+ from .metadata import (
9
+ SkillMetadata,
10
+ get_skill_resources,
11
+ load_full_skill_content,
12
+ parse_skill_metadata,
13
+ parse_yaml_frontmatter,
14
+ )
15
+
16
+ __all__ = [
17
+ "SkillMetadata",
18
+ "parse_yaml_frontmatter",
19
+ "parse_skill_metadata",
20
+ "load_full_skill_content",
21
+ "get_skill_resources",
22
+ ]
@@ -0,0 +1,175 @@
1
+ """Plugin-level config helpers for agent_skills."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import List, Set
7
+
8
+ from code_puppy.config import get_value, set_value
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def get_skill_directories() -> List[str]:
14
+ """Get configured skill directories.
15
+
16
+ Returns:
17
+ List of skill directory paths from configuration.
18
+ Reads from puppy.cfg [puppy] section under 'skill_directories' key.
19
+ Default: ['~/.code_puppy/skills', './.code_puppy/skills', './skills']
20
+
21
+ The directories are stored as a JSON list in the config.
22
+ """
23
+ # Try to read from config first
24
+ config_value = get_value("skill_directories")
25
+
26
+ if config_value:
27
+ try:
28
+ # Parse as JSON
29
+ directories = json.loads(config_value)
30
+ # Ensure it's a list
31
+ if isinstance(directories, list):
32
+ return directories
33
+ except json.JSONDecodeError as e:
34
+ logger.error(f"Failed to parse skill_directories config: {e}")
35
+
36
+ # Fallback to defaults
37
+ home_skills = str(Path.home() / ".code_puppy" / "skills")
38
+ project_config_skills = str(Path.cwd() / ".code_puppy" / "skills")
39
+ local_skills = str(Path.cwd() / "skills")
40
+ return [
41
+ home_skills,
42
+ project_config_skills,
43
+ local_skills,
44
+ ]
45
+
46
+
47
+ def add_skill_directory(path: str) -> bool:
48
+ """Add a directory to the skills search path.
49
+
50
+ Args:
51
+ path: Path to add to the skill directories list.
52
+
53
+ Returns:
54
+ True if the directory was added successfully, False otherwise.
55
+ """
56
+ directories = get_skill_directories()
57
+
58
+ # Check if already exists
59
+ if path in directories:
60
+ logger.info(f"Skill directory already exists: {path}")
61
+ return False
62
+
63
+ # Add the new directory
64
+ directories.append(path)
65
+
66
+ try:
67
+ # Save back to config as JSON
68
+ set_value("skill_directories", json.dumps(directories))
69
+ logger.info(f"Added skill directory: {path}")
70
+ return True
71
+ except Exception as e:
72
+ logger.error(f"Failed to add skill directory: {e}")
73
+ return False
74
+
75
+
76
+ def remove_skill_directory(path: str) -> bool:
77
+ """Remove a directory from the skills search path.
78
+
79
+ Args:
80
+ path: Path to remove from the skill directories list.
81
+
82
+ Returns:
83
+ True if the directory was removed successfully, False otherwise.
84
+ """
85
+ directories = get_skill_directories()
86
+
87
+ # Check if exists
88
+ if path not in directories:
89
+ logger.info(f"Skill directory not found: {path}")
90
+ return False
91
+
92
+ # Remove the directory
93
+ directories.remove(path)
94
+
95
+ try:
96
+ # Save back to config as JSON
97
+ set_value("skill_directories", json.dumps(directories))
98
+ logger.info(f"Removed skill directory: {path}")
99
+ return True
100
+ except Exception as e:
101
+ logger.error(f"Failed to remove skill directory: {e}")
102
+ return False
103
+
104
+
105
+ def get_skills_enabled() -> bool:
106
+ """Check if skills integration is globally enabled.
107
+
108
+ Returns:
109
+ True if skills are globally enabled, False otherwise.
110
+ Reads from 'skills_enabled' config key (default: True).
111
+ """
112
+ cfg_val = get_value("skills_enabled")
113
+ if cfg_val is None:
114
+ return True # Enabled by default
115
+ return str(cfg_val).strip().lower() in {"1", "true", "yes", "on"}
116
+
117
+
118
+ def set_skills_enabled(enabled: bool) -> None:
119
+ """Enable or disable skills integration globally.
120
+
121
+ Args:
122
+ enabled: True to enable, False to disable.
123
+ """
124
+ set_value("skills_enabled", "true" if enabled else "false")
125
+ logger.info(f"Skills integration {'enabled' if enabled else 'disabled'}")
126
+
127
+
128
+ def get_disabled_skills() -> Set[str]:
129
+ """Get set of explicitly disabled skill names.
130
+
131
+ Returns:
132
+ Set of skill names that are disabled.
133
+ Reads from 'disabled_skills' config key as a JSON list.
134
+ """
135
+ config_value = get_value("disabled_skills")
136
+
137
+ if config_value:
138
+ try:
139
+ # Parse as JSON
140
+ disabled_list = json.loads(config_value)
141
+ # Ensure it's a list and convert to set
142
+ if isinstance(disabled_list, list):
143
+ return set(disabled_list)
144
+ except json.JSONDecodeError as e:
145
+ logger.error(f"Failed to parse disabled_skills config: {e}")
146
+
147
+ return set()
148
+
149
+
150
+ def set_skill_disabled(skill_name: str, disabled: bool) -> None:
151
+ """Disable or re-enable a specific skill.
152
+
153
+ Args:
154
+ skill_name: Name of the skill to disable/enable.
155
+ disabled: True to disable, False to enable.
156
+ """
157
+ disabled_skills = get_disabled_skills()
158
+
159
+ if disabled:
160
+ # Add to disabled set
161
+ if skill_name in disabled_skills:
162
+ logger.info(f"Skill already disabled: {skill_name}")
163
+ return
164
+ disabled_skills.add(skill_name)
165
+ logger.info(f"Disabled skill: {skill_name}")
166
+ else:
167
+ # Remove from disabled set
168
+ if skill_name not in disabled_skills:
169
+ logger.info(f"Skill already enabled: {skill_name}")
170
+ return
171
+ disabled_skills.remove(skill_name)
172
+ logger.info(f"Enabled skill: {skill_name}")
173
+
174
+ # Save back to config as JSON
175
+ set_value("disabled_skills", json.dumps(list(disabled_skills)))
@@ -0,0 +1,136 @@
1
+ """Skill discovery - scans directories for valid skills."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from code_puppy.plugins.agent_skills.config import get_skill_directories
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @dataclass
14
+ class SkillInfo:
15
+ """Basic skill information from discovery."""
16
+
17
+ name: str
18
+ path: Path
19
+ has_skill_md: bool
20
+
21
+
22
+ # Global cache for discovered skills
23
+ _skill_cache: Optional[List[SkillInfo]] = None
24
+
25
+
26
+ def get_default_skill_directories() -> List[Path]:
27
+ """Return default directories to scan for skills.
28
+
29
+ Returns:
30
+ - ~/.code_puppy/skills (user skills)
31
+ - ./.code_puppy/skills (project config skills)
32
+ - ./skills (project skills)
33
+ """
34
+ return [
35
+ Path.home() / ".code_puppy" / "skills",
36
+ Path.cwd() / ".code_puppy" / "skills",
37
+ Path.cwd() / "skills",
38
+ ]
39
+
40
+
41
+ def is_valid_skill_directory(path: Path) -> bool:
42
+ """Check if a directory contains a valid SKILL.md file.
43
+
44
+ Args:
45
+ path: Directory path to check.
46
+
47
+ Returns:
48
+ True if the directory is a valid skill directory, False otherwise.
49
+ """
50
+ if not path.is_dir():
51
+ return False
52
+
53
+ skill_md_path = path / "SKILL.md"
54
+ return skill_md_path.is_file()
55
+
56
+
57
+ def discover_skills(directories: Optional[List[Path]] = None) -> List[SkillInfo]:
58
+ """Scan directories for valid skills.
59
+
60
+ Args:
61
+ directories: Directories to scan. If None, uses configured
62
+ directories (which includes user-added ones from /skills menu).
63
+
64
+ Returns:
65
+ List of discovered SkillInfo objects.
66
+ """
67
+ global _skill_cache
68
+
69
+ if directories is None:
70
+ # Use configured directories (respects user-added dirs from /skills menu)
71
+ # then merge with defaults to ensure we always check the standard locations
72
+ configured = [Path(d) for d in get_skill_directories()]
73
+ defaults = get_default_skill_directories()
74
+ # Merge: configured first, then any defaults not already covered
75
+ seen = {p.resolve() for p in configured}
76
+ directories = list(configured)
77
+ for d in defaults:
78
+ if d.resolve() not in seen:
79
+ directories.append(d)
80
+
81
+ discovered_skills: List[SkillInfo] = []
82
+
83
+ for directory in directories:
84
+ if not directory.exists():
85
+ logger.debug(f"Skill directory does not exist: {directory}")
86
+ continue
87
+
88
+ if not directory.is_dir():
89
+ logger.warning(f"Skill path is not a directory: {directory}")
90
+ continue
91
+
92
+ # Scan subdirectories within the skill directory
93
+ for skill_dir in directory.iterdir():
94
+ if not skill_dir.is_dir():
95
+ continue
96
+
97
+ # Skip hidden directories
98
+ if skill_dir.name.startswith("."):
99
+ continue
100
+
101
+ has_skill_md = is_valid_skill_directory(skill_dir)
102
+
103
+ # Include if it has SKILL.md (valid skill) or just for discovery
104
+ skill_info = SkillInfo(
105
+ name=skill_dir.name, path=skill_dir, has_skill_md=has_skill_md
106
+ )
107
+ discovered_skills.append(skill_info)
108
+
109
+ if has_skill_md:
110
+ logger.debug(f"Discovered valid skill: {skill_dir.name} at {skill_dir}")
111
+ else:
112
+ logger.debug(
113
+ f"Found skill directory without SKILL.md: {skill_dir.name}"
114
+ )
115
+
116
+ # Update cache
117
+ _skill_cache = discovered_skills
118
+
119
+ logger.info(
120
+ f"Discovered {len(discovered_skills)} skills from {len(directories)} directories"
121
+ )
122
+ return discovered_skills
123
+
124
+
125
+ def refresh_skill_cache() -> List[SkillInfo]:
126
+ """Force re-discovery of all skills.
127
+
128
+ This clears the cache and performs a fresh scan of all default
129
+ skill directories.
130
+
131
+ Returns:
132
+ List of freshly discovered SkillInfo objects.
133
+ """
134
+ global _skill_cache
135
+ _skill_cache = None
136
+ return discover_skills()