code-puppy-core-plugins 0.0.2__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_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Remote-backed skill catalog adapter.
|
|
2
|
+
|
|
3
|
+
This module provides a stable public interface for the rest of the codebase.
|
|
4
|
+
Historically, code_puppy used a local static catalog. We now source skills from
|
|
5
|
+
`remote_catalog.fetch_remote_catalog()` while keeping the same access patterns.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
from code_puppy_core_plugins.agent_skills.skill_catalog import (
|
|
9
|
+
SkillCatalog,
|
|
10
|
+
SkillCatalogEntry,
|
|
11
|
+
_format_display_name,
|
|
12
|
+
catalog,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
If the remote catalog can't be fetched (and there's no cache), the catalog is
|
|
16
|
+
empty by default (and we log a warning).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import List, Optional
|
|
25
|
+
|
|
26
|
+
from code_puppy_core_plugins.agent_skills.remote_catalog import fetch_remote_catalog
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_ACRONYMS = {
|
|
32
|
+
"ai",
|
|
33
|
+
"api",
|
|
34
|
+
"aws",
|
|
35
|
+
"cli",
|
|
36
|
+
"cpu",
|
|
37
|
+
"csv",
|
|
38
|
+
"db",
|
|
39
|
+
"dns",
|
|
40
|
+
"gpu",
|
|
41
|
+
"html",
|
|
42
|
+
"http",
|
|
43
|
+
"https",
|
|
44
|
+
"id",
|
|
45
|
+
"json",
|
|
46
|
+
"jwt",
|
|
47
|
+
"k8s",
|
|
48
|
+
"llm",
|
|
49
|
+
"ml",
|
|
50
|
+
"mvp",
|
|
51
|
+
"oauth",
|
|
52
|
+
"pdf",
|
|
53
|
+
"psql",
|
|
54
|
+
"qa",
|
|
55
|
+
"rest",
|
|
56
|
+
"rpc",
|
|
57
|
+
"sdk",
|
|
58
|
+
"sql",
|
|
59
|
+
"ssh",
|
|
60
|
+
"ssl",
|
|
61
|
+
"tls",
|
|
62
|
+
"tsv",
|
|
63
|
+
"ui",
|
|
64
|
+
"url",
|
|
65
|
+
"utc",
|
|
66
|
+
"uuid",
|
|
67
|
+
"xml",
|
|
68
|
+
"yaml",
|
|
69
|
+
"yml",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _format_display_name(skill_id: str) -> str:
|
|
74
|
+
"""Format a human-readable display name from a skill id.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
data-exploration -> Data Exploration
|
|
78
|
+
pdf -> PDF
|
|
79
|
+
|
|
80
|
+
This is intentionally simple and predictable.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
cleaned = (skill_id or "").strip()
|
|
84
|
+
if not cleaned:
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
parts = [p for p in cleaned.replace("_", "-").split("-") if p]
|
|
88
|
+
formatted: list[str] = []
|
|
89
|
+
|
|
90
|
+
for part in parts:
|
|
91
|
+
lower = part.lower()
|
|
92
|
+
if lower in _ACRONYMS:
|
|
93
|
+
formatted.append(lower.upper())
|
|
94
|
+
else:
|
|
95
|
+
# Keep existing capitalization for words like "Nextflow" if provided,
|
|
96
|
+
# otherwise just Title Case.
|
|
97
|
+
formatted.append(part[:1].upper() + part[1:].lower())
|
|
98
|
+
|
|
99
|
+
return " ".join(formatted)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class SkillCatalogEntry:
|
|
104
|
+
"""Catalog entry for a skill.
|
|
105
|
+
|
|
106
|
+
Fields are designed to match the historical local catalog interface while
|
|
107
|
+
including remote-only fields (download_url, zip_size_bytes).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
id: str
|
|
111
|
+
name: str
|
|
112
|
+
display_name: str
|
|
113
|
+
description: str
|
|
114
|
+
category: str
|
|
115
|
+
tags: List[str] = field(default_factory=list)
|
|
116
|
+
source_path: Optional[Path] = None
|
|
117
|
+
has_scripts: bool = False
|
|
118
|
+
has_references: bool = False
|
|
119
|
+
file_count: int = 0
|
|
120
|
+
download_url: str = ""
|
|
121
|
+
zip_size_bytes: int = 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SkillCatalog:
|
|
125
|
+
"""Remote skill catalog.
|
|
126
|
+
|
|
127
|
+
This class is a simple in-memory index over remote catalog entries.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
"""Initialize the skill catalog with empty indices."""
|
|
132
|
+
|
|
133
|
+
self._entries: list[SkillCatalogEntry] = []
|
|
134
|
+
self._by_id: dict[str, SkillCatalogEntry] = {}
|
|
135
|
+
self._by_category: dict[str, list[SkillCatalogEntry]] = {}
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
remote = fetch_remote_catalog()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
# fetch_remote_catalog should already be defensive, but let's be extra safe.
|
|
141
|
+
logger.warning(f"Failed to fetch remote catalog: {e}")
|
|
142
|
+
remote = None
|
|
143
|
+
|
|
144
|
+
if remote is None:
|
|
145
|
+
logger.warning(
|
|
146
|
+
"Remote skill catalog unavailable (no network and no cache). "
|
|
147
|
+
"Catalog will be empty."
|
|
148
|
+
)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
entries: list[SkillCatalogEntry] = []
|
|
152
|
+
|
|
153
|
+
for remote_entry in remote.entries:
|
|
154
|
+
skill_id = remote_entry.name
|
|
155
|
+
entry = SkillCatalogEntry(
|
|
156
|
+
id=skill_id,
|
|
157
|
+
name=remote_entry.name,
|
|
158
|
+
display_name=_format_display_name(remote_entry.name),
|
|
159
|
+
description=remote_entry.description,
|
|
160
|
+
category=remote_entry.group,
|
|
161
|
+
tags=[],
|
|
162
|
+
source_path=None,
|
|
163
|
+
has_scripts=remote_entry.has_scripts,
|
|
164
|
+
has_references=remote_entry.has_references,
|
|
165
|
+
file_count=remote_entry.file_count,
|
|
166
|
+
download_url=remote_entry.download_url,
|
|
167
|
+
zip_size_bytes=remote_entry.zip_size_bytes,
|
|
168
|
+
)
|
|
169
|
+
entries.append(entry)
|
|
170
|
+
|
|
171
|
+
self._rebuild_indices(entries)
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
f"Loaded remote skill catalog: {len(self._entries)} skills in "
|
|
175
|
+
f"{len(self._by_category)} categories"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _rebuild_indices(self, entries: list[SkillCatalogEntry]) -> None:
|
|
179
|
+
"""Rebuild internal lookup indices from the loaded entries."""
|
|
180
|
+
|
|
181
|
+
self._entries = list(entries)
|
|
182
|
+
self._by_id = {}
|
|
183
|
+
self._by_category = {}
|
|
184
|
+
|
|
185
|
+
for entry in self._entries:
|
|
186
|
+
# Last one wins if duplicates somehow exist.
|
|
187
|
+
self._by_id[entry.id] = entry
|
|
188
|
+
|
|
189
|
+
cat_key = (entry.category or "").casefold()
|
|
190
|
+
self._by_category.setdefault(cat_key, []).append(entry)
|
|
191
|
+
|
|
192
|
+
# Keep category lists stable and predictable.
|
|
193
|
+
for cat_entries in self._by_category.values():
|
|
194
|
+
cat_entries.sort(key=lambda e: e.display_name.casefold())
|
|
195
|
+
|
|
196
|
+
self._entries.sort(key=lambda e: e.display_name.casefold())
|
|
197
|
+
|
|
198
|
+
def list_categories(self) -> List[str]:
|
|
199
|
+
"""List all categories."""
|
|
200
|
+
|
|
201
|
+
categories = {e.category for e in self._entries if e.category}
|
|
202
|
+
return sorted(categories, key=lambda c: c.casefold())
|
|
203
|
+
|
|
204
|
+
def get_by_category(self, category: str) -> List[SkillCatalogEntry]:
|
|
205
|
+
"""Return all entries in a category (case-insensitive)."""
|
|
206
|
+
|
|
207
|
+
if not category:
|
|
208
|
+
return []
|
|
209
|
+
return list(self._by_category.get(category.casefold(), []))
|
|
210
|
+
|
|
211
|
+
def search(self, query: str) -> List[SkillCatalogEntry]:
|
|
212
|
+
"""Search by substring over id/name/display_name/description/tags/category."""
|
|
213
|
+
|
|
214
|
+
q = (query or "").strip().casefold()
|
|
215
|
+
if not q:
|
|
216
|
+
return self.get_all()
|
|
217
|
+
|
|
218
|
+
results: list[SkillCatalogEntry] = []
|
|
219
|
+
for entry in self._entries:
|
|
220
|
+
haystacks = [
|
|
221
|
+
entry.id,
|
|
222
|
+
entry.name,
|
|
223
|
+
entry.display_name,
|
|
224
|
+
entry.description,
|
|
225
|
+
entry.category,
|
|
226
|
+
" ".join(entry.tags),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
if any(q in (h or "").casefold() for h in haystacks):
|
|
230
|
+
results.append(entry)
|
|
231
|
+
|
|
232
|
+
return results
|
|
233
|
+
|
|
234
|
+
def get_by_id(self, skill_id: str) -> Optional[SkillCatalogEntry]:
|
|
235
|
+
"""Get a skill entry by id (case-sensitive exact match)."""
|
|
236
|
+
|
|
237
|
+
if not skill_id:
|
|
238
|
+
return None
|
|
239
|
+
return self._by_id.get(skill_id)
|
|
240
|
+
|
|
241
|
+
def get_all(self) -> List[SkillCatalogEntry]:
|
|
242
|
+
"""Return all entries."""
|
|
243
|
+
|
|
244
|
+
return list(self._entries)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Singleton instance used by the rest of the codebase.
|
|
248
|
+
# NOTE: This must never crash import-time.
|
|
249
|
+
catalog = SkillCatalog()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
__all__ = [
|
|
253
|
+
"SkillCatalog",
|
|
254
|
+
"SkillCatalogEntry",
|
|
255
|
+
"_format_display_name",
|
|
256
|
+
"catalog",
|
|
257
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Per-skill slash commands.
|
|
2
|
+
|
|
3
|
+
Each discovered (and enabled) skill is exposed as a ``/<skill-name>`` slash
|
|
4
|
+
command. Invoking it loads the full ``SKILL.md`` content and submits it
|
|
5
|
+
as the prompt — effectively the same thing the ``activate_skill`` tool
|
|
6
|
+
does, only initiated by the user.
|
|
7
|
+
|
|
8
|
+
This lives in its own module so that ``register_callbacks.py`` stays
|
|
9
|
+
focused on wiring callbacks (SRP, and the file-size cap).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from code_puppy.messaging import emit_info
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Slash command names already owned by the skills plugin itself.
|
|
22
|
+
# We must never shadow them with a per-skill command.
|
|
23
|
+
_RESERVED_NAMES = {"skills", "skill"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _iter_enabled_skills():
|
|
27
|
+
"""Yield ``SkillMetadata`` for every enabled, valid, non-reserved skill.
|
|
28
|
+
|
|
29
|
+
Delegates the disabled / has_skill_md filtering to
|
|
30
|
+
:func:`enabled_skills.iter_enabled_skill_metadata`, so disabled skills
|
|
31
|
+
never get their frontmatter loaded here either.
|
|
32
|
+
"""
|
|
33
|
+
from .enabled_skills import iter_enabled_skill_metadata
|
|
34
|
+
|
|
35
|
+
for meta in iter_enabled_skill_metadata():
|
|
36
|
+
if meta.name in _RESERVED_NAMES:
|
|
37
|
+
logger.debug(
|
|
38
|
+
"Skipping skill %r: name collides with reserved slash command",
|
|
39
|
+
meta.name,
|
|
40
|
+
)
|
|
41
|
+
continue
|
|
42
|
+
yield meta
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def skill_command_help() -> List[Tuple[str, str]]:
|
|
46
|
+
"""Advertise every enabled skill in the ``/help`` menu."""
|
|
47
|
+
entries: List[Tuple[str, str]] = []
|
|
48
|
+
for meta in _iter_enabled_skills():
|
|
49
|
+
desc = meta.description or "(no description)"
|
|
50
|
+
# Trim ridiculously long descriptions for the help table.
|
|
51
|
+
if len(desc) > 80:
|
|
52
|
+
desc = desc[:77] + "..."
|
|
53
|
+
entries.append((meta.name, f"🛠️ Skill: {desc}"))
|
|
54
|
+
return entries
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def handle_skill_command(command: str, name: str) -> Optional[Any]:
|
|
58
|
+
"""Return the SKILL.md content as a MarkdownCommandResult if ``name``
|
|
59
|
+
matches an enabled skill, otherwise ``None``.
|
|
60
|
+
|
|
61
|
+
Extra args after the command are appended as ``Additional context:``,
|
|
62
|
+
mirroring the customizable_commands plugin's contract.
|
|
63
|
+
"""
|
|
64
|
+
if not name or name in _RESERVED_NAMES:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
# Lazy import to avoid a hard dependency on customizable_commands at
|
|
68
|
+
# plugin load time (and to keep things tidy if it ever moves).
|
|
69
|
+
try:
|
|
70
|
+
from code_puppy_core_plugins.customizable_commands.register_callbacks import (
|
|
71
|
+
MarkdownCommandResult,
|
|
72
|
+
)
|
|
73
|
+
except ImportError:
|
|
74
|
+
logger.debug("MarkdownCommandResult unavailable; cannot run skill via slash")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# Find a matching enabled skill.
|
|
78
|
+
match = next((m for m in _iter_enabled_skills() if m.name == name), None)
|
|
79
|
+
if match is None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
from .metadata import load_full_skill_content
|
|
83
|
+
|
|
84
|
+
content = load_full_skill_content(match.path)
|
|
85
|
+
if not content:
|
|
86
|
+
logger.warning("Failed to load SKILL.md for %s", match.name)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Allow user to pass freeform args after the slash command.
|
|
90
|
+
parts = command.split(maxsplit=1)
|
|
91
|
+
args = parts[1].strip() if len(parts) > 1 else ""
|
|
92
|
+
|
|
93
|
+
header = (
|
|
94
|
+
f"You are activating the '{match.name}' skill. Follow its "
|
|
95
|
+
f"SKILL.md instructions below verbatim.\n\n"
|
|
96
|
+
)
|
|
97
|
+
prompt = header + content
|
|
98
|
+
if args:
|
|
99
|
+
prompt = f"{prompt}\n\nAdditional context: {args}"
|
|
100
|
+
|
|
101
|
+
emit_info(f"🛠️ Activating skill via slash command: {match.name}")
|
|
102
|
+
return MarkdownCommandResult(prompt)
|