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,464 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from code_puppy.callbacks import CustomCommandResult, register_callback
|
|
10
|
+
from code_puppy.config import CONFIG_DIR
|
|
11
|
+
from code_puppy.messaging import emit_error, emit_info, emit_shell_line, emit_warning
|
|
12
|
+
|
|
13
|
+
# Reuse the skills plugin's lightweight (dependency-free) frontmatter parser
|
|
14
|
+
# instead of rolling our own -- one source of truth for YAML frontmatter.
|
|
15
|
+
from code_puppy_core_plugins.agent_skills.metadata import (
|
|
16
|
+
FRONTMATTER_PATTERN,
|
|
17
|
+
parse_yaml_frontmatter,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Global cache for loaded commands
|
|
21
|
+
_custom_commands: Dict[str, str] = {}
|
|
22
|
+
_command_descriptions: Dict[str, str] = {}
|
|
23
|
+
# Commands whose frontmatter declares `exec: <shell command>` run directly instead of
|
|
24
|
+
# being agent-forwarded; value is the raw shell string (shell=True => ~/pipes/env work).
|
|
25
|
+
_command_exec_directives: Dict[str, str] = {}
|
|
26
|
+
_commands_loaded: bool = False # Sentinel to track if commands have been loaded
|
|
27
|
+
|
|
28
|
+
# Soft cap so a runaway script can't wedge the prompt forever.
|
|
29
|
+
_EXEC_TIMEOUT_SECONDS = 30
|
|
30
|
+
|
|
31
|
+
# Scan dirs (priority order; later overrides earlier). The global dir reuses the
|
|
32
|
+
# XDG-aware CONFIG_DIR so it stays in lockstep with code-puppy's file layout.
|
|
33
|
+
_GLOBAL_COMMANDS_DIR = os.path.join(CONFIG_DIR, "commands")
|
|
34
|
+
_COMMAND_DIRECTORIES = [
|
|
35
|
+
_GLOBAL_COMMANDS_DIR, # Global commands (all projects)
|
|
36
|
+
".claude/commands",
|
|
37
|
+
".github/prompts",
|
|
38
|
+
".agents/commands",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# `exec:` runs arbitrary shell, so honor it only from the global user-owned dir; a hostile
|
|
42
|
+
# repo could otherwise drop an exec: command that runs on invocation, bypassing the trust
|
|
43
|
+
# ceremony. Bodies still load from project dirs; only the exec fast-path is gated.
|
|
44
|
+
_TRUSTED_EXEC_DIRECTORIES = frozenset({_GLOBAL_COMMANDS_DIR})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_trusted_exec_dir(dir_path: Path) -> bool:
|
|
48
|
+
"""True if ``exec:`` directives from ``dir_path`` are allowed to run.
|
|
49
|
+
|
|
50
|
+
Compares the *resolved* path against the trusted set so symlinks / ``..``
|
|
51
|
+
tricks can't smuggle a project dir past the gate. Fails closed on any
|
|
52
|
+
resolution error.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
resolved = dir_path.resolve()
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
for trusted in _TRUSTED_EXEC_DIRECTORIES:
|
|
59
|
+
try:
|
|
60
|
+
if resolved == Path(trusted).expanduser().resolve():
|
|
61
|
+
return True
|
|
62
|
+
except OSError:
|
|
63
|
+
continue
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Backwards-compatible name for consumers that imported the plugin's marker.
|
|
68
|
+
MarkdownCommandResult = CustomCommandResult
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Namespace directories beginning with these prefixes are skipped entirely
|
|
72
|
+
# (docs, build artefacts, private helpers) so they don't leak as commands.
|
|
73
|
+
_SKIP_DIR_PREFIXES = ("_", ".")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_in_skipped_namespace(md_file: Path, root: Path) -> bool:
|
|
77
|
+
"""True if any parent directory beneath ``root`` is "hidden".
|
|
78
|
+
|
|
79
|
+
Files under directories like ``_docs/``, ``__pycache__/`` or ``.git/`` are
|
|
80
|
+
excluded so they never register as slash commands. Only the namespace
|
|
81
|
+
(parent) parts are checked -- a leaf file named ``_foo.md`` is still loaded.
|
|
82
|
+
"""
|
|
83
|
+
namespace_parts = md_file.relative_to(root).parts[:-1]
|
|
84
|
+
return any(part.startswith(_SKIP_DIR_PREFIXES) for part in namespace_parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Wibey uses a "//" prefix for commands; code-puppy uses "/". Collapse a line-leading
|
|
88
|
+
# (optionally indented) "//" at load time, leaving URLs like ``fetch //cdn.example.com``
|
|
89
|
+
# alone — only a ``//command`` at line start is rewritten.
|
|
90
|
+
_DOUBLE_SLASH_PREFIX_RE = re.compile(r"(?m)^(\s*)//(?=[\w-])")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _normalize_command_prefixes(text: str) -> str:
|
|
94
|
+
"""Rewrite wibey-style ``//cmd`` command references to ``/cmd``.
|
|
95
|
+
|
|
96
|
+
Only a ``//command`` at the start of a line (optionally indented) is
|
|
97
|
+
normalized; inline / whitespace-preceded ``//host`` sequences (e.g.
|
|
98
|
+
protocol-relative URLs) are left untouched. Applied in-memory to command
|
|
99
|
+
bodies and descriptions; the original markdown files are never modified.
|
|
100
|
+
"""
|
|
101
|
+
return _DOUBLE_SLASH_PREFIX_RE.sub(r"\1/", text)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _derive_description(meta: dict, body: str, base_name: str) -> str:
|
|
105
|
+
"""Pick a human-readable description for the /help + completion picker.
|
|
106
|
+
|
|
107
|
+
Priority:
|
|
108
|
+
1. The ``description:`` field from YAML frontmatter (used verbatim).
|
|
109
|
+
2. The first non-empty, non-heading line of the body (truncated).
|
|
110
|
+
3. A title-cased version of the command name.
|
|
111
|
+
"""
|
|
112
|
+
desc = meta.get("description")
|
|
113
|
+
if isinstance(desc, str) and desc.strip():
|
|
114
|
+
return desc.strip()
|
|
115
|
+
|
|
116
|
+
for line in body.split("\n"):
|
|
117
|
+
stripped = line.strip()
|
|
118
|
+
if stripped and not stripped.startswith("#"):
|
|
119
|
+
return stripped[:50] + ("..." if len(stripped) > 50 else "")
|
|
120
|
+
|
|
121
|
+
return base_name.replace("_", " ").replace("-", " ").title()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _command_name_from_path(md_file: Path, root: Path) -> str:
|
|
125
|
+
"""Derive a (possibly namespaced) command name from a markdown file path.
|
|
126
|
+
|
|
127
|
+
The name mirrors the directory structure beneath ``root``, joined with
|
|
128
|
+
``/``. Examples (root = ``~/.code_puppy/commands``)::
|
|
129
|
+
|
|
130
|
+
hello-world.md -> "hello-world"
|
|
131
|
+
flux/new.md -> "flux/new"
|
|
132
|
+
flux/sub/thing.md -> "flux/sub/thing"
|
|
133
|
+
|
|
134
|
+
The ``.prompt.md`` double-suffix (used by ``.github/prompts``) is stripped
|
|
135
|
+
the same way the legacy flat loader did.
|
|
136
|
+
"""
|
|
137
|
+
rel = md_file.relative_to(root)
|
|
138
|
+
if md_file.name.endswith(".prompt.md"):
|
|
139
|
+
leaf = md_file.name[: -len(".prompt.md")]
|
|
140
|
+
else:
|
|
141
|
+
leaf = md_file.stem
|
|
142
|
+
# Join any parent directories (the namespace) with the leaf command name.
|
|
143
|
+
parts = [*rel.parts[:-1], leaf]
|
|
144
|
+
return "/".join(parts)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _load_markdown_commands() -> None:
|
|
148
|
+
"""Load markdown command files from the configured directories.
|
|
149
|
+
|
|
150
|
+
Recursively scans for *.md files in the configured directories and loads
|
|
151
|
+
them as custom commands. Subdirectories become namespaces, so
|
|
152
|
+
``flux/new.md`` is invoked as ``/flux/new``. Later directories override
|
|
153
|
+
earlier ones with the same command name (project commands override global).
|
|
154
|
+
"""
|
|
155
|
+
global _custom_commands, _command_descriptions, _commands_loaded
|
|
156
|
+
|
|
157
|
+
_custom_commands.clear()
|
|
158
|
+
_command_descriptions.clear()
|
|
159
|
+
_command_exec_directives.clear()
|
|
160
|
+
_commands_loaded = True # Mark as loaded even if directories are empty
|
|
161
|
+
|
|
162
|
+
# Process directories in order - later directories override earlier ones
|
|
163
|
+
for directory in _COMMAND_DIRECTORIES:
|
|
164
|
+
dir_path = Path(directory).expanduser()
|
|
165
|
+
if not dir_path.exists():
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
# `exec:` only runs from user-owned trusted dirs (see module docstring
|
|
169
|
+
# on _TRUSTED_EXEC_DIRECTORIES). Compute once per directory.
|
|
170
|
+
exec_allowed = _is_trusted_exec_dir(dir_path)
|
|
171
|
+
|
|
172
|
+
# Look for markdown files (recursively, so subdirs become namespaces)
|
|
173
|
+
pattern = "*.md" if directory != ".github/prompts" else "*.prompt.md"
|
|
174
|
+
# Sort within directory for consistent ordering
|
|
175
|
+
md_files = sorted(dir_path.rglob(pattern))
|
|
176
|
+
|
|
177
|
+
for md_file in md_files:
|
|
178
|
+
try:
|
|
179
|
+
# Skip files under hidden/underscore namespaces (_docs, .git, ...)
|
|
180
|
+
if _is_in_skipped_namespace(md_file, dir_path):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
# Command name mirrors the path beneath dir_path (flux/new.md
|
|
184
|
+
# -> "flux/new"); top-level files stay un-namespaced.
|
|
185
|
+
base_name = _command_name_from_path(md_file, dir_path)
|
|
186
|
+
|
|
187
|
+
# NB: do NOT strip `raw` before parsing -- FRONTMATTER_PATTERN needs a
|
|
188
|
+
# trailing newline after `---`, so a body-less exec-only file would
|
|
189
|
+
# otherwise drop its exec: directive and leak raw YAML as the body.
|
|
190
|
+
raw = md_file.read_text(encoding="utf-8")
|
|
191
|
+
if not raw.strip():
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# Strip the name/description frontmatter so the agent gets clean
|
|
195
|
+
# instructions (else the model treats the YAML block as context noise).
|
|
196
|
+
meta = parse_yaml_frontmatter(raw)
|
|
197
|
+
body = FRONTMATTER_PATTERN.sub("", raw, count=1).strip()
|
|
198
|
+
exec_directive = meta.get("exec")
|
|
199
|
+
has_exec = isinstance(exec_directive, str) and bool(
|
|
200
|
+
exec_directive.strip()
|
|
201
|
+
)
|
|
202
|
+
# Skip only when there's no body AND no exec directive — an exec-only
|
|
203
|
+
# (frontmatter only) command is legitimate and must still load.
|
|
204
|
+
if not body and not has_exec:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# Normalize wibey //cmd -> /cmd in the body the agent receives.
|
|
208
|
+
body = _normalize_command_prefixes(body)
|
|
209
|
+
|
|
210
|
+
description = _normalize_command_prefixes(
|
|
211
|
+
_derive_description(meta, body, base_name)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Later directories override earlier ones (project > global)
|
|
215
|
+
_custom_commands[base_name] = body
|
|
216
|
+
_command_descriptions[base_name] = description
|
|
217
|
+
|
|
218
|
+
# Optional `exec:` runs a script instead of forwarding the body (e.g.
|
|
219
|
+
# /flux/status renders its own colored output). Only honored from trusted
|
|
220
|
+
# user-owned dirs; a project-relative directive is downgraded to normal.
|
|
221
|
+
if has_exec:
|
|
222
|
+
if exec_allowed:
|
|
223
|
+
_command_exec_directives[base_name] = exec_directive.strip()
|
|
224
|
+
else:
|
|
225
|
+
emit_warning(
|
|
226
|
+
f"Ignoring exec: directive on /{base_name} from "
|
|
227
|
+
f"untrusted dir '{directory}' (only "
|
|
228
|
+
f"{_GLOBAL_COMMANDS_DIR} may run exec)."
|
|
229
|
+
)
|
|
230
|
+
# Downgrade to a normal agent-forwarded command; make
|
|
231
|
+
# sure no earlier trusted exec lingers under this name.
|
|
232
|
+
_command_exec_directives.pop(base_name, None)
|
|
233
|
+
else:
|
|
234
|
+
# Later dirs without exec should clear an earlier one for
|
|
235
|
+
# the same name (override semantics for the exec hook too).
|
|
236
|
+
_command_exec_directives.pop(base_name, None)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
emit_error(f"Failed to load command from {md_file}: {e}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
_EXEC_TOKEN_RE = re.compile(
|
|
243
|
+
r"\{(python|scripts|commands|script|command)(?::([^}]+))?\}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _shell_quote(value: str) -> str:
|
|
248
|
+
"""Quote a single argument for the shell ``subprocess`` will spawn.
|
|
249
|
+
|
|
250
|
+
``_run_exec_directive`` uses ``shell=True``; the shell differs by OS, and so
|
|
251
|
+
does its quoting. ``shlex.quote`` is POSIX-only (it emits single-quotes that
|
|
252
|
+
``cmd.exe`` does not understand), so on Windows we defer to
|
|
253
|
+
``subprocess.list2cmdline`` which produces cmd-compatible quoting.
|
|
254
|
+
"""
|
|
255
|
+
if os.name == "nt":
|
|
256
|
+
return subprocess.list2cmdline([value])
|
|
257
|
+
return shlex.quote(value)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _expand_exec_tokens(directive: str) -> str:
|
|
261
|
+
"""Substitute exec tokens ({python}/{script:...}/...) in a directive.
|
|
262
|
+
|
|
263
|
+
Command files reference these tokens so they never hard-code an interpreter,
|
|
264
|
+
a shell-expansion (~), or an OS-specific path -- all three of which break on
|
|
265
|
+
Windows and ignore code-puppy's XDG-configured CONFIG_DIR:
|
|
266
|
+
|
|
267
|
+
{python} -> sys.executable (the venv interpreter running us)
|
|
268
|
+
{scripts} -> <CONFIG_DIR>/scripts (absolute)
|
|
269
|
+
{commands} -> <CONFIG_DIR>/commands (absolute)
|
|
270
|
+
{script:foo.py} -> <CONFIG_DIR>/scripts/foo.py (absolute)
|
|
271
|
+
{command:a/b.md} -> <CONFIG_DIR>/commands/a/b.md (absolute)
|
|
272
|
+
|
|
273
|
+
Each expansion is quoted for the current platform's shell so paths with
|
|
274
|
+
spaces survive. CONFIG_DIR is read fresh on each call so an env override is
|
|
275
|
+
honored. Unknown-shaped tokens are left untouched.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def _replace(match):
|
|
279
|
+
kind, arg = match.group(1), match.group(2)
|
|
280
|
+
if kind == "python":
|
|
281
|
+
return _shell_quote(sys.executable or "python3")
|
|
282
|
+
if kind == "scripts":
|
|
283
|
+
return _shell_quote(os.path.join(CONFIG_DIR, "scripts"))
|
|
284
|
+
if kind == "commands":
|
|
285
|
+
return _shell_quote(os.path.join(CONFIG_DIR, "commands"))
|
|
286
|
+
if kind == "script":
|
|
287
|
+
rel = (arg or "").strip()
|
|
288
|
+
return _shell_quote(os.path.join(CONFIG_DIR, "scripts", *rel.split("/")))
|
|
289
|
+
if kind == "command":
|
|
290
|
+
rel = (arg or "").strip()
|
|
291
|
+
return _shell_quote(os.path.join(CONFIG_DIR, "commands", *rel.split("/")))
|
|
292
|
+
return match.group(0)
|
|
293
|
+
|
|
294
|
+
return _EXEC_TOKEN_RE.sub(_replace, directive)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _run_exec_directive(directive: str, name: str, args: str = "") -> None:
|
|
298
|
+
"""Run an ``exec:`` directive and stream its output via the message bus.
|
|
299
|
+
|
|
300
|
+
Uses ``shell=True`` so directives can use ``~``, env vars, and pipes. This
|
|
301
|
+
is only reachable for commands loaded from a trusted (user-owned) dir --
|
|
302
|
+
the loader refuses to register exec directives from project-relative dirs
|
|
303
|
+
(see :data:`_TRUSTED_EXEC_DIRECTORIES`), so a hostile repo can't smuggle
|
|
304
|
+
one in.
|
|
305
|
+
Sets ``FORCE_COLOR=1`` so scripts emit ANSI even though stdout is a pipe;
|
|
306
|
+
:func:`emit_shell_line` then renders the ANSI via Rich's ``Text.from_ansi``.
|
|
307
|
+
Also sets ``PYTHONIOENCODING=utf-8`` and decodes the captured output as
|
|
308
|
+
UTF-8 explicitly: on Windows a child Python attached to a pipe otherwise
|
|
309
|
+
defaults its stdout to the legacy codepage (cp1252) and dies with
|
|
310
|
+
``UnicodeEncodeError`` the moment a script prints an emoji -- and the
|
|
311
|
+
parent-side ``text=True`` decode would mojibake for the same reason.
|
|
312
|
+
UTF-8 on both ends of the pipe, no exceptions. Deliberately NOT setting
|
|
313
|
+
``PYTHONUTF8=1``: full UTF-8 mode also flips the default ``open()``
|
|
314
|
+
encoding for every Python child of every exec command -- a much broader
|
|
315
|
+
semantic change than the stdio bug calls for, and ``PYTHONIOENCODING``
|
|
316
|
+
alone is verified sufficient to prevent the crash (even for older
|
|
317
|
+
installed scripts still using Rich's legacy-Windows path).
|
|
318
|
+
|
|
319
|
+
Any extra tokens the user typed after the command (e.g. ``/flux/status todo``)
|
|
320
|
+
are shell-quoted and appended to the directive so dangerous shell chars in
|
|
321
|
+
user input can't escape into the command line.
|
|
322
|
+
|
|
323
|
+
``{python}`` / ``{script:...}`` / ``{command:...}`` tokens in the directive
|
|
324
|
+
are expanded first (see :func:`_expand_exec_tokens`) so command files stay
|
|
325
|
+
interpreter-, path-, and OS-agnostic.
|
|
326
|
+
"""
|
|
327
|
+
directive = _expand_exec_tokens(directive)
|
|
328
|
+
full_cmd = directive
|
|
329
|
+
if args:
|
|
330
|
+
try:
|
|
331
|
+
tokens = shlex.split(args)
|
|
332
|
+
except ValueError:
|
|
333
|
+
# Fall back to a single shell-quoted blob if shlex can't parse it
|
|
334
|
+
# (e.g. unbalanced quotes). Keeps the user's intent without crashing.
|
|
335
|
+
tokens = [args]
|
|
336
|
+
if tokens:
|
|
337
|
+
full_cmd = directive + " " + " ".join(shlex.quote(t) for t in tokens)
|
|
338
|
+
|
|
339
|
+
emit_info(f" Running exec for /{name}: {full_cmd}")
|
|
340
|
+
env = {
|
|
341
|
+
**os.environ,
|
|
342
|
+
"FORCE_COLOR": "1",
|
|
343
|
+
# Force UTF-8 stdio in child Pythons; harmless for non-Python tools.
|
|
344
|
+
# (Narrowest lever on purpose -- see docstring for why not PYTHONUTF8.)
|
|
345
|
+
"PYTHONIOENCODING": "utf-8",
|
|
346
|
+
}
|
|
347
|
+
try:
|
|
348
|
+
result = subprocess.run(
|
|
349
|
+
full_cmd,
|
|
350
|
+
shell=True,
|
|
351
|
+
capture_output=True,
|
|
352
|
+
encoding="utf-8",
|
|
353
|
+
errors="replace",
|
|
354
|
+
timeout=_EXEC_TIMEOUT_SECONDS,
|
|
355
|
+
env=env,
|
|
356
|
+
)
|
|
357
|
+
except subprocess.TimeoutExpired:
|
|
358
|
+
emit_error(f"exec for /{name} timed out after {_EXEC_TIMEOUT_SECONDS}s")
|
|
359
|
+
return
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
emit_error(f"exec for /{name} failed to start: {exc}")
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
for line in result.stdout.splitlines():
|
|
365
|
+
emit_shell_line(line, stream="stdout")
|
|
366
|
+
for line in result.stderr.splitlines():
|
|
367
|
+
emit_shell_line(line, stream="stderr")
|
|
368
|
+
|
|
369
|
+
if result.returncode != 0:
|
|
370
|
+
emit_warning(f"exec for /{name} exited with code {result.returncode}")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def reload_commands() -> None:
|
|
374
|
+
"""Force a fresh rescan of the command directories.
|
|
375
|
+
|
|
376
|
+
The cache is populated once at plugin import time. Anything that writes new
|
|
377
|
+
command files *after* import (e.g. the flux_bootstrap plugin installing the
|
|
378
|
+
Flux suite during the ``startup`` hook) must call this so the freshly
|
|
379
|
+
installed commands become dispatchable immediately -- otherwise ``/flux/...``
|
|
380
|
+
is treated as ordinary input until the next restart. This is the explicit
|
|
381
|
+
loader API the review asked for; prefer it over poking the private cache.
|
|
382
|
+
"""
|
|
383
|
+
_load_markdown_commands()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def is_custom_command(name: str) -> bool:
|
|
387
|
+
"""True if ``name`` is a loaded custom command (namespaced or flat).
|
|
388
|
+
|
|
389
|
+
Exposed so the host's file-path disambiguation heuristic can tell a
|
|
390
|
+
genuine path (``/Users/me/file.py``) apart from a namespaced command
|
|
391
|
+
(``/flux/status``) -- both have multiple slashes. Loads commands lazily
|
|
392
|
+
on first use so it works even before the first dispatch.
|
|
393
|
+
"""
|
|
394
|
+
if not _commands_loaded:
|
|
395
|
+
_load_markdown_commands()
|
|
396
|
+
return name in _custom_commands or name in _command_exec_directives
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
400
|
+
"""Return help entries for loaded markdown commands."""
|
|
401
|
+
# Reload commands to pick up any changes
|
|
402
|
+
_load_markdown_commands()
|
|
403
|
+
|
|
404
|
+
help_entries = []
|
|
405
|
+
for name, description in sorted(_command_descriptions.items()):
|
|
406
|
+
help_entries.append((name, description))
|
|
407
|
+
|
|
408
|
+
return help_entries
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _handle_custom_command(command: str, name: str) -> Optional[Any]:
|
|
412
|
+
"""Handle a markdown-based custom command.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
command: The full command string
|
|
416
|
+
name: The command name without leading slash
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
MarkdownCommandResult with content to be processed as input,
|
|
420
|
+
or None if not found
|
|
421
|
+
"""
|
|
422
|
+
if not name:
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
# Ensure commands are loaded (use sentinel, not dict emptiness)
|
|
426
|
+
if not _commands_loaded:
|
|
427
|
+
_load_markdown_commands()
|
|
428
|
+
|
|
429
|
+
# Extract any additional arguments from the command (shared by both paths)
|
|
430
|
+
parts = command.split(maxsplit=1)
|
|
431
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
432
|
+
|
|
433
|
+
# exec: directive wins -- run the script, bypass the agent entirely.
|
|
434
|
+
exec_directive = _command_exec_directives.get(name)
|
|
435
|
+
if exec_directive:
|
|
436
|
+
_run_exec_directive(exec_directive, name, args=args)
|
|
437
|
+
return True # handled; do not invoke the model
|
|
438
|
+
|
|
439
|
+
# Look up the command
|
|
440
|
+
content = _custom_commands.get(name)
|
|
441
|
+
if content is None:
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
# If there are arguments, append them to the prompt
|
|
445
|
+
if args:
|
|
446
|
+
prompt = f"{content}\n\nAdditional context: {args}"
|
|
447
|
+
else:
|
|
448
|
+
prompt = content
|
|
449
|
+
|
|
450
|
+
# Emit info message and return the special marker
|
|
451
|
+
emit_info(f"📝 Executing markdown command: {name}")
|
|
452
|
+
return MarkdownCommandResult(prompt)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# Register callbacks
|
|
456
|
+
register_callback("custom_command_help", _custom_help)
|
|
457
|
+
register_callback("custom_command", _handle_custom_command)
|
|
458
|
+
|
|
459
|
+
# Make the result class available for the command handler
|
|
460
|
+
# Import this in command_handler.py to check for this type
|
|
461
|
+
__all__ = ["MarkdownCommandResult", "reload_commands"]
|
|
462
|
+
|
|
463
|
+
# Load commands at import time
|
|
464
|
+
_load_markdown_commands()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""DBOS durable-execution plugin for Code Puppy."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Cancel an in-flight DBOS workflow, registered via agent_run_cancel hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def cancel_workflow(group_id: str) -> None:
|
|
7
|
+
"""Best-effort cancel of a DBOS workflow. Never raises into core."""
|
|
8
|
+
try:
|
|
9
|
+
from dbos import DBOS
|
|
10
|
+
except ImportError:
|
|
11
|
+
return
|
|
12
|
+
try:
|
|
13
|
+
await DBOS.cancel_workflow_async(group_id)
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Slash command: /dbos on|off|status to toggle the durable-execution plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .config import is_enabled, set_enabled
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_dbos_command(command: str, name: str):
|
|
9
|
+
if name != "dbos":
|
|
10
|
+
return None
|
|
11
|
+
parts = command.strip().split()
|
|
12
|
+
if len(parts) < 2:
|
|
13
|
+
status = "ON" if is_enabled() else "OFF"
|
|
14
|
+
return (
|
|
15
|
+
f"DBOS durable execution is currently {status}. Usage: /dbos on|off|status"
|
|
16
|
+
)
|
|
17
|
+
sub = parts[1].lower()
|
|
18
|
+
if sub == "status":
|
|
19
|
+
status = "ON" if is_enabled() else "OFF"
|
|
20
|
+
return f"DBOS durable execution: {status}"
|
|
21
|
+
if sub == "on":
|
|
22
|
+
set_enabled(True)
|
|
23
|
+
return "DBOS durable execution enabled. Restart code-puppy to apply."
|
|
24
|
+
if sub == "off":
|
|
25
|
+
set_enabled(False)
|
|
26
|
+
return "DBOS durable execution disabled. Restart code-puppy to apply."
|
|
27
|
+
return f"Unknown /dbos subcommand: {sub}. Usage: /dbos on|off|status"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dbos_command_help():
|
|
31
|
+
return [("dbos", "Toggle DBOS durable execution: /dbos on|off|status")]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Plugin-local config accessors for DBOS durable execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from code_puppy.config import DATA_DIR, get_value, set_config_value
|
|
8
|
+
|
|
9
|
+
_DEFAULT_SQLITE_FILE = os.path.join(DATA_DIR, "dbos_store.sqlite")
|
|
10
|
+
|
|
11
|
+
DBOS_DATABASE_URL = os.environ.get(
|
|
12
|
+
"DBOS_SYSTEM_DATABASE_URL", f"sqlite:///{_DEFAULT_SQLITE_FILE}"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_enabled() -> bool:
|
|
17
|
+
"""Return True if 'enable_dbos' in puppy.cfg is truthy. Default: True."""
|
|
18
|
+
cfg_val = get_value("enable_dbos")
|
|
19
|
+
if cfg_val is None:
|
|
20
|
+
return True
|
|
21
|
+
return str(cfg_val).strip().lower() in ("true", "1", "yes", "on")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def feature_capability(name: str) -> bool | None:
|
|
25
|
+
"""Expose plugin state through the generic feature-capability callback."""
|
|
26
|
+
return is_enabled() if name == "dbos_durable_exec" else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_enabled(enabled: bool) -> None:
|
|
30
|
+
"""Persist the 'enable_dbos' switch to puppy.cfg."""
|
|
31
|
+
set_config_value("enable_dbos", "true" if enabled else "false")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""DBOS launch/destroy lifecycle, lifted from cli_runner.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from code_puppy import __version__ as current_version
|
|
10
|
+
from code_puppy.config import DATA_DIR
|
|
11
|
+
from code_puppy.messaging import emit_error, emit_info
|
|
12
|
+
|
|
13
|
+
from .config import _DEFAULT_SQLITE_FILE, DBOS_DATABASE_URL
|
|
14
|
+
from .startup_lock import enable_sqlite_wal, interprocess_lock
|
|
15
|
+
|
|
16
|
+
# Flipped True only after a successful DBOS.launch(). Other modules (wrapper/runtime)
|
|
17
|
+
# MUST check this — `dbos` importable != launched. Stays False in pytest (import w/o
|
|
18
|
+
# on_startup), so the wrapper passes through unmodified.
|
|
19
|
+
_LAUNCHED = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_launched() -> bool:
|
|
23
|
+
"""True iff this plugin successfully called DBOS.launch()."""
|
|
24
|
+
return _LAUNCHED
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def on_startup() -> None:
|
|
28
|
+
"""Initialize and launch DBOS for durable execution."""
|
|
29
|
+
global _LAUNCHED
|
|
30
|
+
try:
|
|
31
|
+
from dbos import DBOS, DBOSConfig
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Shouldn't happen — hook only registers when dbos is importable; kept
|
|
34
|
+
# defensive in case install state shifts between module-load and startup.
|
|
35
|
+
emit_error(
|
|
36
|
+
"[dbos_durable_exec] dbos package not installed; durable exec disabled."
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
dbos_app_version = os.environ.get(
|
|
41
|
+
"DBOS_APP_VERSION", f"{current_version}-{int(time.time() * 1000)}"
|
|
42
|
+
)
|
|
43
|
+
dbos_config: DBOSConfig = {
|
|
44
|
+
"name": "dbos-code-puppy",
|
|
45
|
+
"system_database_url": DBOS_DATABASE_URL,
|
|
46
|
+
"run_admin_server": False,
|
|
47
|
+
"conductor_key": os.environ.get("DBOS_CONDUCTOR_KEY"),
|
|
48
|
+
"log_level": os.environ.get("DBOS_LOG_LEVEL", "ERROR"),
|
|
49
|
+
"application_version": dbos_app_version,
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
emit_info(f"Initializing DBOS with database at: {DBOS_DATABASE_URL}")
|
|
53
|
+
# Multiple instances share one SQLite system DB; DBOS.launch() migration +
|
|
54
|
+
# recovery take SQLite's single-writer lock, so racers collide unless serialized.
|
|
55
|
+
# We: (1) flip to WAL (persistent) so concurrent access rarely blocks, and
|
|
56
|
+
# (2) hold a cross-process file lock so launch/migration runs one process at a
|
|
57
|
+
# time — late starters wait instead of crashing.
|
|
58
|
+
if DBOS_DATABASE_URL.startswith("sqlite"):
|
|
59
|
+
enable_sqlite_wal(_DEFAULT_SQLITE_FILE)
|
|
60
|
+
launch_lock = os.path.join(DATA_DIR, "dbos_store.launch.lock")
|
|
61
|
+
with interprocess_lock(launch_lock, timeout=60.0):
|
|
62
|
+
DBOS(config=dbos_config)
|
|
63
|
+
DBOS.launch()
|
|
64
|
+
_LAUNCHED = True
|
|
65
|
+
except Exception as e:
|
|
66
|
+
emit_error(
|
|
67
|
+
f"[dbos_durable_exec] Error initializing DBOS: {e}\n{traceback.format_exc()}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def on_shutdown() -> None:
|
|
72
|
+
"""Tear DBOS down. Best-effort, never raises."""
|
|
73
|
+
global _LAUNCHED
|
|
74
|
+
try:
|
|
75
|
+
from dbos import DBOS
|
|
76
|
+
except ImportError:
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
DBOS.destroy()
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
finally:
|
|
83
|
+
_LAUNCHED = False
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Wire the DBOS durable-execution plugin into core via callback hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from code_puppy.callbacks import register_callback
|
|
8
|
+
|
|
9
|
+
from .cancel import cancel_workflow
|
|
10
|
+
from .commands import dbos_command_help, handle_dbos_command
|
|
11
|
+
from .config import feature_capability, is_enabled
|
|
12
|
+
from .lifecycle import on_shutdown, on_startup
|
|
13
|
+
from .runtime import dbos_run_context, skip_fallback_render
|
|
14
|
+
from .wrapper import wrap_with_dbos_agent
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
register_callback("feature_capability", feature_capability)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Slash command is always available so users can /dbos on even when the
|
|
23
|
+
# package isn't installed yet (we tell them to install + restart).
|
|
24
|
+
register_callback("custom_command", handle_dbos_command)
|
|
25
|
+
register_callback("custom_command_help", dbos_command_help)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if is_enabled():
|
|
29
|
+
try:
|
|
30
|
+
import dbos # noqa: F401 -- early import check
|
|
31
|
+
except ImportError:
|
|
32
|
+
# logger.info (not debug) so a silently-skipped enabled feature surfaces;
|
|
33
|
+
# install with `pip install code-puppy[durable]` to fix.
|
|
34
|
+
logger.info(
|
|
35
|
+
"DBOS plugin enabled but `dbos` package not installed; "
|
|
36
|
+
"durable-exec hooks not registered. "
|
|
37
|
+
"Install with: pip install 'code-puppy[durable]'"
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
register_callback("startup", on_startup)
|
|
41
|
+
register_callback("shutdown", on_shutdown)
|
|
42
|
+
register_callback("wrap_pydantic_agent", wrap_with_dbos_agent)
|
|
43
|
+
register_callback("agent_run_context", dbos_run_context)
|
|
44
|
+
register_callback("agent_run_cancel", cancel_workflow)
|
|
45
|
+
register_callback("should_skip_fallback_render", skip_fallback_render)
|