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,252 @@
|
|
|
1
|
+
"""Credential, device-flow, and model-catalog helpers for Meta Muse OAuth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from .config import (
|
|
16
|
+
FALLBACK_MODELS,
|
|
17
|
+
META_OAUTH_CONFIG,
|
|
18
|
+
get_muse_auth_path,
|
|
19
|
+
get_token_storage_path,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
return data if isinstance(data, dict) else None
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
return None
|
|
31
|
+
except Exception as exc: # noqa: BLE001
|
|
32
|
+
logger.warning("Could not read Meta credentials from %s: %s", path, exc)
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _validate_url(url: str, allowed_host: str) -> str:
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
host = (parsed.hostname or "").lower()
|
|
39
|
+
if parsed.scheme != "https" or host != allowed_host:
|
|
40
|
+
raise ValueError(f"Unexpected Meta endpoint: {url}")
|
|
41
|
+
return url.rstrip("/")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_api_base_url(url: str) -> str:
|
|
45
|
+
"""Allow only Meta's HTTPS Model API front door."""
|
|
46
|
+
return _validate_url(url, "api.meta.ai")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_verification_url(url: str) -> str:
|
|
50
|
+
"""Allow HTTPS verification pages hosted by Meta."""
|
|
51
|
+
parsed = urlparse(url)
|
|
52
|
+
host = (parsed.hostname or "").lower()
|
|
53
|
+
if parsed.scheme != "https" or not (
|
|
54
|
+
host == "meta.com" or host.endswith(".meta.com")
|
|
55
|
+
):
|
|
56
|
+
raise ValueError(f"Unexpected Meta verification URL: {url}")
|
|
57
|
+
return url
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_credentials(data: dict[str, Any], source: str) -> dict[str, Any] | None:
|
|
61
|
+
api_key = data.get("api_key")
|
|
62
|
+
if not isinstance(api_key, str) or not api_key:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
base_url = validate_api_base_url(
|
|
66
|
+
str(data.get("api_base_url") or META_OAUTH_CONFIG["api_base_url"])
|
|
67
|
+
)
|
|
68
|
+
except ValueError as exc:
|
|
69
|
+
logger.warning("Ignoring invalid Meta credential endpoint: %s", exc)
|
|
70
|
+
return None
|
|
71
|
+
normalized = dict(data)
|
|
72
|
+
normalized.update({"api_key": api_key, "api_base_url": base_url, "_source": source})
|
|
73
|
+
return normalized
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_credentials() -> dict[str, Any] | None:
|
|
77
|
+
"""Load Meta credentials using env, Code Puppy, then Muse CLI precedence."""
|
|
78
|
+
env_key = os.getenv("META_API_KEY")
|
|
79
|
+
if env_key:
|
|
80
|
+
return {
|
|
81
|
+
"api_key": env_key,
|
|
82
|
+
"api_base_url": META_OAUTH_CONFIG["api_base_url"],
|
|
83
|
+
"_source": "environment",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
own = _read_json(get_token_storage_path())
|
|
87
|
+
if own:
|
|
88
|
+
normalized = _normalize_credentials(own, "code-puppy")
|
|
89
|
+
if normalized:
|
|
90
|
+
return normalized
|
|
91
|
+
|
|
92
|
+
muse = _read_json(get_muse_auth_path())
|
|
93
|
+
providers = muse.get("providers") if muse else None
|
|
94
|
+
provider = providers.get("meta") if isinstance(providers, dict) else None
|
|
95
|
+
if isinstance(provider, dict):
|
|
96
|
+
return _normalize_credentials(provider, "muse")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def save_credentials(credentials: dict[str, Any]) -> bool:
|
|
101
|
+
"""Atomically save private Meta credentials with mode 0600."""
|
|
102
|
+
if not credentials.get("api_key"):
|
|
103
|
+
raise ValueError("Meta credentials require an api_key")
|
|
104
|
+
destination = get_token_storage_path()
|
|
105
|
+
payload = {key: value for key, value in credentials.items() if key != "_source"}
|
|
106
|
+
temporary_path: str | None = None
|
|
107
|
+
try:
|
|
108
|
+
fd, temporary_path = tempfile.mkstemp(
|
|
109
|
+
prefix=".meta_oauth.", dir=destination.parent
|
|
110
|
+
)
|
|
111
|
+
os.fchmod(fd, 0o600)
|
|
112
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
113
|
+
json.dump(payload, handle, indent=2)
|
|
114
|
+
handle.write("\n")
|
|
115
|
+
os.replace(temporary_path, destination)
|
|
116
|
+
destination.chmod(0o600)
|
|
117
|
+
return True
|
|
118
|
+
except Exception as exc: # noqa: BLE001
|
|
119
|
+
logger.error("Failed to save Meta OAuth credentials: %s", exc)
|
|
120
|
+
if temporary_path:
|
|
121
|
+
Path(temporary_path).unlink(missing_ok=True)
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _problem(response: requests.Response, fallback: str) -> str:
|
|
126
|
+
try:
|
|
127
|
+
payload = response.json()
|
|
128
|
+
except ValueError:
|
|
129
|
+
return fallback
|
|
130
|
+
if not isinstance(payload, dict):
|
|
131
|
+
return fallback
|
|
132
|
+
return str(
|
|
133
|
+
payload.get("detail")
|
|
134
|
+
or payload.get("title")
|
|
135
|
+
or payload.get("error")
|
|
136
|
+
or fallback
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def request_device_authorization() -> dict[str, Any]:
|
|
141
|
+
"""Start Meta's RFC 8628 device authorization flow."""
|
|
142
|
+
base = _validate_url(str(META_OAUTH_CONFIG["auth_base_url"]), "auth.meta.com")
|
|
143
|
+
response = requests.post(
|
|
144
|
+
base + str(META_OAUTH_CONFIG["device_authorization_path"]),
|
|
145
|
+
data={"client_id": META_OAUTH_CONFIG["client_id"]},
|
|
146
|
+
headers={"Accept": "application/json", "User-Agent": "code-puppy/meta-oauth"},
|
|
147
|
+
timeout=META_OAUTH_CONFIG["request_timeout"],
|
|
148
|
+
allow_redirects=False,
|
|
149
|
+
)
|
|
150
|
+
if not response.ok:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
_problem(response, f"Device authorization failed ({response.status_code})")
|
|
153
|
+
)
|
|
154
|
+
payload = response.json()
|
|
155
|
+
if not isinstance(payload, dict):
|
|
156
|
+
raise RuntimeError("Meta device authorization returned invalid JSON")
|
|
157
|
+
verification_url = payload.get("verification_uri_complete") or payload.get(
|
|
158
|
+
"verification_uri"
|
|
159
|
+
)
|
|
160
|
+
if verification_url:
|
|
161
|
+
validate_verification_url(str(verification_url))
|
|
162
|
+
return payload
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def poll_device_token(device_code: str) -> tuple[str, dict[str, Any]]:
|
|
166
|
+
"""Perform one device-token poll and return its OAuth state and payload."""
|
|
167
|
+
base = _validate_url(str(META_OAUTH_CONFIG["auth_base_url"]), "auth.meta.com")
|
|
168
|
+
response = requests.post(
|
|
169
|
+
base + str(META_OAUTH_CONFIG["device_token_path"]),
|
|
170
|
+
data={
|
|
171
|
+
"grant_type": META_OAUTH_CONFIG["device_code_grant"],
|
|
172
|
+
"device_code": device_code,
|
|
173
|
+
"client_id": META_OAUTH_CONFIG["client_id"],
|
|
174
|
+
},
|
|
175
|
+
headers={"Accept": "application/json", "User-Agent": "code-puppy/meta-oauth"},
|
|
176
|
+
timeout=META_OAUTH_CONFIG["request_timeout"],
|
|
177
|
+
allow_redirects=False,
|
|
178
|
+
)
|
|
179
|
+
try:
|
|
180
|
+
payload = response.json()
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
raise RuntimeError("Meta token endpoint returned invalid JSON") from exc
|
|
183
|
+
if response.ok and isinstance(payload, dict) and payload.get("access_token"):
|
|
184
|
+
return "authorized", payload
|
|
185
|
+
if isinstance(payload, dict):
|
|
186
|
+
return str(payload.get("error") or f"http_{response.status_code}"), payload
|
|
187
|
+
return f"http_{response.status_code}", {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def mint_api_credentials(access_token: str) -> dict[str, Any]:
|
|
191
|
+
"""Exchange a Meta account token for a Model API credential."""
|
|
192
|
+
response = requests.post(
|
|
193
|
+
_validate_url(str(META_OAUTH_CONFIG["mint_url"]), "api.meta.ai"),
|
|
194
|
+
headers={
|
|
195
|
+
"Accept": "application/json",
|
|
196
|
+
"Authorization": f"Bearer {access_token}",
|
|
197
|
+
"User-Agent": "code-puppy/meta-oauth",
|
|
198
|
+
"x-api-version": META_OAUTH_CONFIG["api_version"],
|
|
199
|
+
},
|
|
200
|
+
timeout=META_OAUTH_CONFIG["request_timeout"],
|
|
201
|
+
allow_redirects=False,
|
|
202
|
+
)
|
|
203
|
+
if not response.ok:
|
|
204
|
+
raise RuntimeError(
|
|
205
|
+
_problem(response, f"Meta key mint failed ({response.status_code})")
|
|
206
|
+
)
|
|
207
|
+
payload = response.json()
|
|
208
|
+
if not isinstance(payload, dict) or not payload.get("api_key"):
|
|
209
|
+
raise RuntimeError("Meta key mint returned no API key")
|
|
210
|
+
payload["api_base_url"] = validate_api_base_url(
|
|
211
|
+
str(payload.get("base_url") or payload.get("api_base_url") or "")
|
|
212
|
+
)
|
|
213
|
+
payload.pop("base_url", None)
|
|
214
|
+
return payload
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def discover_models(credentials: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
218
|
+
"""Fetch Meta's Muse catalogue and convert it to Code Puppy model specs."""
|
|
219
|
+
response = requests.get(
|
|
220
|
+
_validate_url(str(META_OAUTH_CONFIG["catalog_url"]), "api.meta.ai"),
|
|
221
|
+
headers={
|
|
222
|
+
"Accept": "application/json",
|
|
223
|
+
"Authorization": f"Bearer {credentials['api_key']}",
|
|
224
|
+
"User-Agent": "code-puppy/meta-oauth",
|
|
225
|
+
"x-api-version": META_OAUTH_CONFIG["api_version"],
|
|
226
|
+
},
|
|
227
|
+
timeout=META_OAUTH_CONFIG["request_timeout"],
|
|
228
|
+
)
|
|
229
|
+
if not response.ok:
|
|
230
|
+
raise RuntimeError(
|
|
231
|
+
_problem(response, f"Meta model discovery failed ({response.status_code})")
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
payload = response.json()
|
|
235
|
+
rows = payload.get("data", []) if isinstance(payload, dict) else []
|
|
236
|
+
models: dict[str, dict[str, Any]] = {}
|
|
237
|
+
for row in rows:
|
|
238
|
+
if not isinstance(row, dict) or not isinstance(row.get("id"), str):
|
|
239
|
+
continue
|
|
240
|
+
metadata = row.get("metadata")
|
|
241
|
+
details = metadata.get("muse-code") if isinstance(metadata, dict) else None
|
|
242
|
+
if not isinstance(details, dict) or details.get("is_hidden") is True:
|
|
243
|
+
continue
|
|
244
|
+
raw_limits = details.get("limit")
|
|
245
|
+
limits = raw_limits if isinstance(raw_limits, dict) else {}
|
|
246
|
+
models[row["id"]] = {
|
|
247
|
+
"context_length": int(limits.get("context") or 128_000),
|
|
248
|
+
"supported_settings": ["reasoning_effort"]
|
|
249
|
+
if details.get("reasoning")
|
|
250
|
+
else [],
|
|
251
|
+
}
|
|
252
|
+
return models or dict(FALLBACK_MODELS)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# namespace_skill_search
|
|
2
|
+
|
|
3
|
+
Model-agnostic reimplementation of OpenAI's namespace + `tool_search`
|
|
4
|
+
pattern, applied to Code Puppy's skill catalog.
|
|
5
|
+
|
|
6
|
+
## Why this exists
|
|
7
|
+
|
|
8
|
+
Frontier labs converge on one shape for large tool/skill catalogs:
|
|
9
|
+
**one grouping layer + on-demand search**, instead of a flat list of
|
|
10
|
+
every item injected into the system prompt upfront.
|
|
11
|
+
|
|
12
|
+
- Anthropic's shipped Tool Search Tool: flat on-demand search,
|
|
13
|
+
`defer_loading: true`, results injected at end of context to preserve
|
|
14
|
+
prompt caching. ~85% token reduction, measurable accuracy gains on
|
|
15
|
+
internal MCP evals ([Anthropic engineering blog, Nov 2025](https://www.anthropic.com/engineering/advanced-tool-use)).
|
|
16
|
+
Note: our `load_prompt` fragment lands earlier in the assembled prompt
|
|
17
|
+
(layer 2 of `base_agent.py::get_full_system_prompt`) than the built-in
|
|
18
|
+
flat skill list it replaces did (layer 6, dead last). We prioritized
|
|
19
|
+
additive-safety (`load_prompt` can't collide with another plugin's
|
|
20
|
+
fragment; `get_model_system_prompt` can) over exactly matching
|
|
21
|
+
Anthropic's end-of-context caching position. If cache-hit rate on the
|
|
22
|
+
namespace block ever matters in practice, moving it later is a
|
|
23
|
+
follow-up, not a redesign.
|
|
24
|
+
- OpenAI's shipped `tool_search`: same on-demand mechanic, PLUS an
|
|
25
|
+
explicit **namespace** grouping layer above it
|
|
26
|
+
(`{"type": "namespace", "name": "crm", "tools": [...]}`), because
|
|
27
|
+
"our models have primarily been trained to search those surfaces" —
|
|
28
|
+
namespaces outperform bare deferred functions in their own docs
|
|
29
|
+
([OpenAI tool-search guide](https://developers.openai.com/api/docs/guides/tools-tool-search)).
|
|
30
|
+
They recommend keeping each namespace under 10 functions.
|
|
31
|
+
- Neither ships a deep multi-level tree. One grouping layer is the norm;
|
|
32
|
+
true hierarchical (server -> tool) routing exists mainly in academic
|
|
33
|
+
work (e.g. MCP-Zero, arXiv:2506.01056), not shipped production
|
|
34
|
+
features.
|
|
35
|
+
|
|
36
|
+
## What we copied, and why
|
|
37
|
+
|
|
38
|
+
We copied OpenAI's shape (namespace + search) rather than Anthropic's
|
|
39
|
+
(flat search only), because the namespace layer is a genuine win once a
|
|
40
|
+
catalog gets into the hundreds of items. But OpenAI's mechanism depends
|
|
41
|
+
on a model-specific API flag (`defer_loading`) that only exists for
|
|
42
|
+
their own models. This plugin gets the same effect through Code Puppy's
|
|
43
|
+
model-agnostic plugin hooks instead, so it behaves identically on
|
|
44
|
+
Claude, GPT, Gemini, or any custom endpoint wired through
|
|
45
|
+
`ModelFactory` — no provider-specific request fields required.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
1. **Namespace derivation** (`namespaces.py`) — a skill's namespace is
|
|
50
|
+
its first `tags:` entry (untagged skills land in `General`). No new
|
|
51
|
+
frontmatter field, no migration of existing `SKILL.md` files required.
|
|
52
|
+
2. **Compact directory injection** (`load_prompt` hook) — instead of one
|
|
53
|
+
line per skill (the built-in behavior when
|
|
54
|
+
`frontmatter_in_system_prompt` is on), the system prompt gets one
|
|
55
|
+
line per *namespace* with a 3-item preview and a count. `load_prompt`
|
|
56
|
+
fragments from every plugin are newline-joined by `base_agent.py`, so
|
|
57
|
+
this is safely additive — it cannot collide with or overwrite another
|
|
58
|
+
plugin's fragment the way `get_model_system_prompt` callbacks can
|
|
59
|
+
(see "What we deliberately avoided" below).
|
|
60
|
+
3. **Drill-down tool** (`browse_skill_namespace`) — one tool, three modes:
|
|
61
|
+
- no args -> namespace directory (same content as the prompt block)
|
|
62
|
+
- `namespace="X"` -> every skill in that namespace
|
|
63
|
+
- `query="..."` -> keyword search across all namespaces
|
|
64
|
+
4. **Flat list turned off** — the first time this plugin ever starts up
|
|
65
|
+
(via a `startup` callback, not an import-time side effect — see
|
|
66
|
+
"What we deliberately avoided" below), it calls
|
|
67
|
+
`set_frontmatter_in_system_prompt(False)` via the public config API
|
|
68
|
+
(`code_puppy_core_plugins.agent_skills.config`) so the built-in per-skill
|
|
69
|
+
flat list doesn't also render alongside the namespace directory.
|
|
70
|
+
|
|
71
|
+
**This is a real, persisted config write, not an in-memory toggle.**
|
|
72
|
+
Enabling this plugin once permanently sets
|
|
73
|
+
`frontmatter_in_system_prompt=false` in `puppy.cfg`. A dedicated
|
|
74
|
+
marker key (`namespace_skill_search_frontmatter_migrated`) records
|
|
75
|
+
that the flip already happened, so if you re-enable it afterwards
|
|
76
|
+
with `/skills frontmatter on`, we never touch the flag again — your
|
|
77
|
+
choice sticks across restarts. But if you disable or remove this
|
|
78
|
+
plugin entirely, the flag stays `false`; nothing restores the flat
|
|
79
|
+
list automatically. Run `/skills frontmatter on` yourself if you want
|
|
80
|
+
it back.
|
|
81
|
+
|
|
82
|
+
## What we deliberately avoided
|
|
83
|
+
|
|
84
|
+
- **No config mutation at import time.** The `frontmatter_in_system_prompt`
|
|
85
|
+
flip runs inside a `register_callback("startup", ...)` handler (matching
|
|
86
|
+
the same pattern as `code_puppy/plugins/theme`'s
|
|
87
|
+
`_apply_default_theme_on_first_run`), not as top-level code in
|
|
88
|
+
`register_callbacks.py`. Import-time disk writes are a landmine: they
|
|
89
|
+
fire on every import (including test collection), can't be unit-tested
|
|
90
|
+
without `importlib.reload` gymnastics, and race with plugin load order
|
|
91
|
+
in ways a callback doesn't. A dedicated migration marker (see above)
|
|
92
|
+
makes the one-time-ness explicit and testable instead of relying on
|
|
93
|
+
"only flip it when the shared flag is currently True" as the sole
|
|
94
|
+
signal.
|
|
95
|
+
- **No `get_model_system_prompt` callback.** That phase is
|
|
96
|
+
last-write-wins across plugins — `model_utils.prepare_prompt_for_model`
|
|
97
|
+
threads augmenter results sequentially and the *last* callback
|
|
98
|
+
processed wins on any key both callbacks set. A second plugin
|
|
99
|
+
registering on that phase risks silently clobbering the built-in
|
|
100
|
+
`agent_skills` plugin's contribution. `load_prompt` avoids that
|
|
101
|
+
failure mode entirely by design (simple concatenation, no merge
|
|
102
|
+
conflicts possible).
|
|
103
|
+
- **No vector DB / embeddings.** Namespace + substring search is enough
|
|
104
|
+
at this scale; see Anthropic's own `tool_search_with_embeddings.ipynb`
|
|
105
|
+
cookbook, which only recommends embeddings above ~100 tools with high
|
|
106
|
+
semantic overlap between items.
|
|
107
|
+
- **No deep multi-level tree** (namespace -> subnamespace -> skill). No
|
|
108
|
+
frontier lab ships that in production; one grouping layer is the norm.
|
|
109
|
+
- **No new tool duplicating `list_or_search_skills`.** That tool still
|
|
110
|
+
exists and still works for flat queries; `browse_skill_namespace` is
|
|
111
|
+
additive, not a replacement.
|
|
112
|
+
|
|
113
|
+
## Sizing signal
|
|
114
|
+
|
|
115
|
+
OpenAI's own docs recommend keeping each namespace under 10 functions
|
|
116
|
+
for best token efficiency. We don't enforce this (skills aren't ours to
|
|
117
|
+
re-tag), but the directory block flags any namespace over that
|
|
118
|
+
threshold as `oversized namespace` so it's visible instead of silent.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""namespace_skill_search — OpenAI-style namespace + on-demand search for skills.
|
|
2
|
+
|
|
3
|
+
Model-agnostic reimplementation of the pattern OpenAI ships as its
|
|
4
|
+
`tool_search` + namespaces feature. OpenAI's version relies on a
|
|
5
|
+
model-provider-specific `defer_loading` API flag; this plugin gets the same
|
|
6
|
+
effect — one grouping layer + on-demand search instead of a flat, unranked
|
|
7
|
+
skill list baked into every system prompt — using only Code Puppy's
|
|
8
|
+
model-agnostic plugin hooks, so it behaves identically on Claude, GPT,
|
|
9
|
+
Gemini, or any custom endpoint wired through `ModelFactory`.
|
|
10
|
+
|
|
11
|
+
See README.md in this directory for the full design rationale, including
|
|
12
|
+
what frontier labs (Anthropic, OpenAI, Google) actually ship in production
|
|
13
|
+
vs. what's academic-only, and why this plugin copies OpenAI's shape
|
|
14
|
+
specifically.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Namespace derivation and summary-block rendering.
|
|
2
|
+
|
|
3
|
+
A "namespace" is a skill's first tag (falling back to "General" for
|
|
4
|
+
untagged skills). No new frontmatter field, no schema migration — every
|
|
5
|
+
skill that already has a `tags:` entry in its SKILL.md slots in for free.
|
|
6
|
+
|
|
7
|
+
This mirrors OpenAI's namespace grouping (`{"type": "namespace", "name":
|
|
8
|
+
"crm", "tools": [...]}`) as a plain-data structure instead of a
|
|
9
|
+
model-provider-specific request field.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from typing import Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from code_puppy_core_plugins.agent_skills.enabled_skills import (
|
|
18
|
+
list_enabled_skill_metadata,
|
|
19
|
+
)
|
|
20
|
+
from code_puppy_core_plugins.agent_skills.metadata import SkillMetadata
|
|
21
|
+
|
|
22
|
+
_DEFAULT_NAMESPACE = "General"
|
|
23
|
+
|
|
24
|
+
# OpenAI recommends fewer than 10 functions per namespace; flag oversized
|
|
25
|
+
# namespaces without re-tagging skills we don't own.
|
|
26
|
+
_RECOMMENDED_MAX_PER_NAMESPACE = 10
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _namespace_for(skill: SkillMetadata) -> str:
|
|
30
|
+
"""First tag wins; untagged skills land in the General bucket."""
|
|
31
|
+
if skill.tags:
|
|
32
|
+
return skill.tags[0].strip() or _DEFAULT_NAMESPACE
|
|
33
|
+
return _DEFAULT_NAMESPACE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_namespaces() -> Dict[str, List[SkillMetadata]]:
|
|
37
|
+
"""Group every enabled skill by namespace.
|
|
38
|
+
|
|
39
|
+
Grouping is case-insensitive: skills tagged "finance" and "Finance"
|
|
40
|
+
land in the same bucket. Without this, the directory would silently
|
|
41
|
+
fragment into two separate namespace entries that look distinct to
|
|
42
|
+
the model but are semantically the same category, and a
|
|
43
|
+
case-sensitive drill-down would only ever surface one of them (see
|
|
44
|
+
`search_tool.py`'s case-insensitive `namespace=` lookup, which
|
|
45
|
+
depends on this function never producing two keys that only differ
|
|
46
|
+
by case). The *first* casing encountered wins for display purposes,
|
|
47
|
+
which keeps behavior deterministic for a given skill-discovery order.
|
|
48
|
+
|
|
49
|
+
Returns an empty dict if skills are globally disabled or none exist —
|
|
50
|
+
callers should treat that as "nothing to show", not an error.
|
|
51
|
+
"""
|
|
52
|
+
namespaces: Dict[str, List[SkillMetadata]] = {}
|
|
53
|
+
display_names: Dict[str, str] = {}
|
|
54
|
+
for skill in list_enabled_skill_metadata():
|
|
55
|
+
raw_ns = _namespace_for(skill)
|
|
56
|
+
key = raw_ns.lower()
|
|
57
|
+
display = display_names.setdefault(key, raw_ns)
|
|
58
|
+
namespaces.setdefault(display, []).append(skill)
|
|
59
|
+
return namespaces
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _duplicate_skill_names(namespaces: Dict[str, List[SkillMetadata]]) -> List[str]:
|
|
63
|
+
"""Names shared by 2+ skills, regardless of namespace.
|
|
64
|
+
|
|
65
|
+
Nothing in `agent_skills` enforces globally-unique skill names, and
|
|
66
|
+
grouping by namespace makes a collision more visible (two entries
|
|
67
|
+
with the same name can now show up side-by-side across namespace
|
|
68
|
+
listings), not less. We can't resolve the ambiguity here -- that's an
|
|
69
|
+
`agent_skills`-level concern -- but we can at least flag it instead
|
|
70
|
+
of silently presenting the model with two indistinguishable
|
|
71
|
+
`activate_skill(name)` targets.
|
|
72
|
+
"""
|
|
73
|
+
counts = Counter(s.name for skills in namespaces.values() for s in skills)
|
|
74
|
+
return sorted(name for name, count in counts.items() if count > 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_namespace_summary() -> Optional[str]:
|
|
78
|
+
"""Render the compact `load_prompt` block replacing the flat skill list.
|
|
79
|
+
|
|
80
|
+
Format is intentionally terse — this is the ONE thing that always
|
|
81
|
+
lands in the system prompt, so every line here is context budget spent
|
|
82
|
+
on every single turn.
|
|
83
|
+
"""
|
|
84
|
+
namespaces = build_namespaces()
|
|
85
|
+
if not namespaces:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
total = sum(len(v) for v in namespaces.values())
|
|
89
|
+
lines = [
|
|
90
|
+
"## Skill Namespaces",
|
|
91
|
+
f"{total} skills available across {len(namespaces)} namespaces. "
|
|
92
|
+
"Individual skills are NOT listed here — browse or search instead.",
|
|
93
|
+
"",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
for ns, skills in sorted(namespaces.items(), key=lambda kv: -len(kv[1])):
|
|
97
|
+
preview = ", ".join(s.name for s in skills[:3])
|
|
98
|
+
remainder = len(skills) - 3
|
|
99
|
+
if remainder > 0:
|
|
100
|
+
preview += f", +{remainder} more"
|
|
101
|
+
flag = (
|
|
102
|
+
" oversized namespace"
|
|
103
|
+
if len(skills) > _RECOMMENDED_MAX_PER_NAMESPACE
|
|
104
|
+
else ""
|
|
105
|
+
)
|
|
106
|
+
lines.append(f"- **{ns}** ({len(skills)}){flag}: {preview}")
|
|
107
|
+
|
|
108
|
+
lines.append("")
|
|
109
|
+
lines.append(
|
|
110
|
+
"Call `browse_skill_namespace()` with no arguments to see this same "
|
|
111
|
+
"directory again, `browse_skill_namespace(namespace=...)` to list "
|
|
112
|
+
"every skill in one namespace, or `browse_skill_namespace(query=...)` "
|
|
113
|
+
"to keyword-search across all namespaces. Then `activate_skill(name)` "
|
|
114
|
+
"to load full instructions."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
duplicates = _duplicate_skill_names(namespaces)
|
|
118
|
+
if duplicates:
|
|
119
|
+
lines.append(
|
|
120
|
+
"Note: these skill names appear more than once across "
|
|
121
|
+
f"namespaces, so `activate_skill(name)` may be ambiguous: "
|
|
122
|
+
f"{', '.join(duplicates)}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Entry point: registers all hooks for the namespace_skill_search plugin.
|
|
2
|
+
|
|
3
|
+
Three hooks:
|
|
4
|
+
|
|
5
|
+
1. `startup` -> one-time migration that turns off the built-in flat
|
|
6
|
+
skill list (see `_maybe_disable_frontmatter` below).
|
|
7
|
+
2. `load_prompt` -> injects the compact namespace directory (a few lines
|
|
8
|
+
per namespace instead of one line per skill).
|
|
9
|
+
Fragments from every plugin are simply
|
|
10
|
+
newline-joined by base_agent.py, so this coexists
|
|
11
|
+
safely with any other plugin's fragment.
|
|
12
|
+
3. `register_tools` -> adds `browse_skill_namespace` as a real tool, wired
|
|
13
|
+
to every agent automatically via
|
|
14
|
+
`register_agent_tools`.
|
|
15
|
+
|
|
16
|
+
We deliberately do NOT touch `get_model_system_prompt` (that's where the
|
|
17
|
+
built-in flat skill list gets injected, and where a *second* callback on
|
|
18
|
+
that same phase would silently clobber the first one's output — see
|
|
19
|
+
`model_utils.prepare_prompt_for_model`: augmenter results are threaded
|
|
20
|
+
through sequentially and the *last* one processed wins on any key they
|
|
21
|
+
both set). Instead we turn the flat list off via the public config API
|
|
22
|
+
and let our `load_prompt` fragment be the only skills-summary content in
|
|
23
|
+
the prompt.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
from code_puppy.callbacks import register_callback
|
|
29
|
+
from code_puppy.config import get_value, set_config_value
|
|
30
|
+
from code_puppy_core_plugins.agent_skills.config import (
|
|
31
|
+
get_frontmatter_in_system_prompt,
|
|
32
|
+
set_frontmatter_in_system_prompt,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .namespaces import build_namespace_summary
|
|
36
|
+
from .search_tool import register_browse_skill_namespace
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Own marker distinguishes this plugin's one-time migration from a user's later
|
|
41
|
+
# `/skills frontmatter on`; flip the shared flag once, then never override it.
|
|
42
|
+
_MIGRATION_MARKER_KEY = "namespace_skill_search_frontmatter_migrated"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _maybe_disable_frontmatter() -> None:
|
|
46
|
+
"""One-time migration: turn off the built-in flat per-skill list.
|
|
47
|
+
|
|
48
|
+
Runs on the `startup` callback (not at import time) so it is:
|
|
49
|
+
- testable in isolation (call this function directly with mocked
|
|
50
|
+
config, same pattern as `theme._apply_default_theme_on_first_run`);
|
|
51
|
+
- not repeated on every module import (`startup` callbacks fire once
|
|
52
|
+
per process, same as any other plugin);
|
|
53
|
+
- a single, auditable point of config mutation rather than an
|
|
54
|
+
import-time side effect.
|
|
55
|
+
|
|
56
|
+
Only ever flips the shared `frontmatter_in_system_prompt` flag from
|
|
57
|
+
True -> False, and only the first time this plugin ever starts up. A
|
|
58
|
+
user who runs `/skills frontmatter on` afterwards keeps that choice
|
|
59
|
+
forever — the marker means we never re-evaluate `get_value` for this
|
|
60
|
+
decision again.
|
|
61
|
+
"""
|
|
62
|
+
if get_value(_MIGRATION_MARKER_KEY):
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if get_frontmatter_in_system_prompt():
|
|
66
|
+
set_frontmatter_in_system_prompt(False)
|
|
67
|
+
logger.info(
|
|
68
|
+
"namespace_skill_search: disabled flat skill-list frontmatter "
|
|
69
|
+
"(one-time migration); namespace directory + "
|
|
70
|
+
"browse_skill_namespace tool take over. Run `/skills "
|
|
71
|
+
"frontmatter on` to restore the flat list."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
set_config_value(_MIGRATION_MARKER_KEY, "true")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _on_load_prompt():
|
|
78
|
+
return build_namespace_summary()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _register_tools():
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
"name": "browse_skill_namespace",
|
|
85
|
+
"register_func": register_browse_skill_namespace,
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _advertise_to_all_agents(agent_name=None):
|
|
91
|
+
return ["browse_skill_namespace"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
register_callback("startup", _maybe_disable_frontmatter)
|
|
95
|
+
register_callback("load_prompt", _on_load_prompt)
|
|
96
|
+
register_callback("register_tools", _register_tools)
|
|
97
|
+
register_callback("register_agent_tools", _advertise_to_all_agents)
|
|
98
|
+
|
|
99
|
+
logger.info("namespace_skill_search plugin loaded")
|