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,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
|