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.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- 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()
|