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,309 @@
|
|
|
1
|
+
"""``EventBridge`` — translate Code Puppy runtime events into ACP updates.
|
|
2
|
+
|
|
3
|
+
While Code Puppy runs a turn, pydantic-ai's ``event_stream_handler`` fires the
|
|
4
|
+
``stream_event`` callback for each streamed text/thinking delta, and the tool
|
|
5
|
+
layer fires ``pre_tool_call`` / ``post_tool_call``. This bridge registers those
|
|
6
|
+
hooks (the same seam the shipping ``frontend_emitter`` plugin uses) and maps
|
|
7
|
+
each event to a ``session/update`` notification sent through the SDK's
|
|
8
|
+
``AgentSideConnection.session_update``.
|
|
9
|
+
|
|
10
|
+
We use hooks rather than consuming the ``MessageBus`` because the bus is
|
|
11
|
+
single-consumer and buffers until a renderer attaches, and agent text does not
|
|
12
|
+
arrive there as clean deltas. See ``README.md`` → *Event source*.
|
|
13
|
+
|
|
14
|
+
Tool calls get human-friendly titles ("Edit foo.py", "Run: pytest"), a ``kind``
|
|
15
|
+
so the client picks the right icon, file ``locations``, and — on completion —
|
|
16
|
+
the unified diff as inline content so edits are visible in the client's UI.
|
|
17
|
+
|
|
18
|
+
Routing/run-context (which session is active, whether text streamed, the
|
|
19
|
+
open-tool-call stack) lives in ``state`` so the permission and I/O backends can
|
|
20
|
+
share it. This bridge is pure translation.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
from acp.helpers import (
|
|
30
|
+
start_tool_call,
|
|
31
|
+
text_block,
|
|
32
|
+
tool_content,
|
|
33
|
+
update_agent_message_text,
|
|
34
|
+
update_agent_thought_text,
|
|
35
|
+
update_tool_call,
|
|
36
|
+
)
|
|
37
|
+
from acp.schema import ToolCallLocation
|
|
38
|
+
|
|
39
|
+
from code_puppy.callbacks import register_callback, unregister_callback
|
|
40
|
+
from code_puppy_core_plugins.acp import state
|
|
41
|
+
from code_puppy.tools.common import resolve_path
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# Map Code Puppy tool names to ACP tool-call ``kind`` values so the client
|
|
46
|
+
# picks the right icon/affordance. Unknown tools fall back to "other".
|
|
47
|
+
_TOOL_KINDS = {
|
|
48
|
+
"read_file": "read",
|
|
49
|
+
"list_files": "read",
|
|
50
|
+
"grep": "search",
|
|
51
|
+
"edit_file": "edit",
|
|
52
|
+
"create_file": "edit",
|
|
53
|
+
"replace_in_file": "edit",
|
|
54
|
+
"delete_snippet": "edit",
|
|
55
|
+
"delete_file": "delete",
|
|
56
|
+
"agent_run_shell_command": "execute",
|
|
57
|
+
"agent_share_your_reasoning": "think",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Human-readable verbs for building tool-call titles like "Edit foo.py".
|
|
61
|
+
_TOOL_VERBS = {
|
|
62
|
+
"read_file": "Read",
|
|
63
|
+
"list_files": "List",
|
|
64
|
+
"grep": "Search",
|
|
65
|
+
"edit_file": "Edit",
|
|
66
|
+
"create_file": "Create",
|
|
67
|
+
"replace_in_file": "Edit",
|
|
68
|
+
"delete_snippet": "Edit",
|
|
69
|
+
"delete_file": "Delete",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Tool-arg keys that commonly carry the target file path, in priority order.
|
|
73
|
+
_PATH_ARG_KEYS = ("file_path", "path", "target_file", "filename")
|
|
74
|
+
|
|
75
|
+
# Cap on inline diff/output content so a huge edit never floods the client.
|
|
76
|
+
_MAX_DIFF_CHARS = 8000
|
|
77
|
+
|
|
78
|
+
# Interactive TUI tools that can't run headless over ACP (they'd render a terminal
|
|
79
|
+
# picker + read stdin, the JSON-RPC pipe). Until ACP ratifies structured elicitation
|
|
80
|
+
# (an RFD), block these and steer the model to plain-text questions instead.
|
|
81
|
+
_INTERACTIVE_TOOLS = {"ask_user_question"}
|
|
82
|
+
_INTERACTIVE_BLOCK = {
|
|
83
|
+
"blocked": True,
|
|
84
|
+
"error_message": (
|
|
85
|
+
"[BLOCKED] Interactive pickers aren't available over ACP. Ask the user "
|
|
86
|
+
"your question(s) directly in your normal text response and wait for "
|
|
87
|
+
"their reply — do not call this tool."
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EventBridge:
|
|
93
|
+
"""Forward Code Puppy runtime hook events to the client as updates."""
|
|
94
|
+
|
|
95
|
+
def register(self) -> None:
|
|
96
|
+
"""Register the runtime hooks. Call once when the connection opens."""
|
|
97
|
+
register_callback("stream_event", self._on_stream_event)
|
|
98
|
+
register_callback("pre_tool_call", self._on_pre_tool_call)
|
|
99
|
+
register_callback("post_tool_call", self._on_post_tool_call)
|
|
100
|
+
|
|
101
|
+
def unregister(self) -> None:
|
|
102
|
+
"""Remove the runtime hooks. Call on teardown so no callbacks leak.
|
|
103
|
+
|
|
104
|
+
Mirrors ``register`` so a closed connection leaves no dangling bridge
|
|
105
|
+
callbacks wired into the global registry (which would double-emit if a
|
|
106
|
+
second connection opened in the same process).
|
|
107
|
+
"""
|
|
108
|
+
for phase, handler in (
|
|
109
|
+
("stream_event", self._on_stream_event),
|
|
110
|
+
("pre_tool_call", self._on_pre_tool_call),
|
|
111
|
+
("post_tool_call", self._on_post_tool_call),
|
|
112
|
+
):
|
|
113
|
+
try:
|
|
114
|
+
unregister_callback(phase, handler)
|
|
115
|
+
except Exception: # noqa: BLE001
|
|
116
|
+
logger.debug(
|
|
117
|
+
"ACP: bridge unregister failed for %s", phase, exc_info=True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# ---- Hook handlers ----------------------------------------------------
|
|
121
|
+
async def _on_stream_event(
|
|
122
|
+
self, event_type: str, event_data: Any, agent_session_id: Optional[str] = None
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Map streamed text/thinking to message/thought chunks.
|
|
125
|
+
|
|
126
|
+
Two event shapes carry visible content and BOTH must be forwarded:
|
|
127
|
+
|
|
128
|
+
* ``part_start`` -- a new text/thinking part whose *initial* content
|
|
129
|
+
(``part.content``) is the opening of the message. pydantic-ai often
|
|
130
|
+
packs the first token(s) here rather than in the first delta.
|
|
131
|
+
* ``part_delta`` -- each subsequent chunk (``delta.content_delta``).
|
|
132
|
+
|
|
133
|
+
Start-content and delta-content are disjoint (deltas are only the
|
|
134
|
+
*additions* after the start), so emitting both reconstructs the full
|
|
135
|
+
message with no overlap. Forwarding only ``part_delta`` -- as this
|
|
136
|
+
bridge used to -- silently drops the opening chunk whenever the model
|
|
137
|
+
front-loads it into the start event.
|
|
138
|
+
|
|
139
|
+
The per-run ``state`` session id wins over ``agent_session_id``: the
|
|
140
|
+
former is a ``ContextVar`` isolated to this prompt's task, whereas
|
|
141
|
+
``agent_session_id`` is derived from the process-wide message-bus
|
|
142
|
+
session context, which a concurrent prompt on the same connection can
|
|
143
|
+
overwrite. We only fall back to it when no run is active in this context.
|
|
144
|
+
"""
|
|
145
|
+
session_id = state.get_active_session_id() or agent_session_id
|
|
146
|
+
if session_id is None:
|
|
147
|
+
return
|
|
148
|
+
data = event_data if isinstance(event_data, dict) else {}
|
|
149
|
+
if event_type == "part_start":
|
|
150
|
+
part = data.get("part")
|
|
151
|
+
content = getattr(part, "content", None)
|
|
152
|
+
is_thinking = "Thinking" in (data.get("part_type", "") or "")
|
|
153
|
+
elif event_type == "part_delta":
|
|
154
|
+
delta = data.get("delta")
|
|
155
|
+
content = (
|
|
156
|
+
getattr(delta, "content_delta", None) if delta is not None else None
|
|
157
|
+
)
|
|
158
|
+
is_thinking = "Thinking" in (data.get("delta_type", "") or "")
|
|
159
|
+
else:
|
|
160
|
+
return
|
|
161
|
+
if not content:
|
|
162
|
+
return
|
|
163
|
+
if is_thinking:
|
|
164
|
+
update = update_agent_thought_text(content)
|
|
165
|
+
else:
|
|
166
|
+
state.note_streamed_text()
|
|
167
|
+
update = update_agent_message_text(content)
|
|
168
|
+
await self._send(session_id, update)
|
|
169
|
+
|
|
170
|
+
async def _on_pre_tool_call(
|
|
171
|
+
self, tool_name: str, tool_args: Dict[str, Any], context: Any = None
|
|
172
|
+
) -> Any:
|
|
173
|
+
"""Announce a tool call starting (status ``in_progress``).
|
|
174
|
+
|
|
175
|
+
Interactive TUI tools that can't run over ACP are blocked here (via the
|
|
176
|
+
``pre_tool_call`` blocking contract) *before* we open a tool-call entry,
|
|
177
|
+
so the client never sees a dangling in-progress call.
|
|
178
|
+
"""
|
|
179
|
+
session_id = state.get_active_session_id()
|
|
180
|
+
if session_id is None:
|
|
181
|
+
return None
|
|
182
|
+
if tool_name in _INTERACTIVE_TOOLS:
|
|
183
|
+
return dict(_INTERACTIVE_BLOCK)
|
|
184
|
+
tool_call_id = state.push_tool_call(tool_name)
|
|
185
|
+
update = start_tool_call(
|
|
186
|
+
tool_call_id,
|
|
187
|
+
self._title_for(tool_name, tool_args),
|
|
188
|
+
kind=_TOOL_KINDS.get(tool_name, "other"),
|
|
189
|
+
status="in_progress",
|
|
190
|
+
locations=self._locations_for(tool_args) or None,
|
|
191
|
+
raw_input=tool_args if isinstance(tool_args, dict) else {},
|
|
192
|
+
)
|
|
193
|
+
await self._send(session_id, update)
|
|
194
|
+
|
|
195
|
+
async def _on_post_tool_call(
|
|
196
|
+
self,
|
|
197
|
+
tool_name: str,
|
|
198
|
+
tool_args: Dict[str, Any],
|
|
199
|
+
result: Any,
|
|
200
|
+
duration_ms: float,
|
|
201
|
+
context: Any = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Report a tool call finishing (status ``completed`` / ``failed``)."""
|
|
204
|
+
session_id = state.get_active_session_id()
|
|
205
|
+
if session_id is None:
|
|
206
|
+
return
|
|
207
|
+
tool_call_id = state.pop_tool_call(tool_name)
|
|
208
|
+
if tool_call_id is None:
|
|
209
|
+
return
|
|
210
|
+
status = "completed" if self._is_success(result) else "failed"
|
|
211
|
+
update = update_tool_call(
|
|
212
|
+
tool_call_id,
|
|
213
|
+
status=status,
|
|
214
|
+
content=self._content_for(result) or None,
|
|
215
|
+
raw_output=self._summarize(result),
|
|
216
|
+
)
|
|
217
|
+
await self._send(session_id, update)
|
|
218
|
+
|
|
219
|
+
# ---- Helpers ----------------------------------------------------------
|
|
220
|
+
@staticmethod
|
|
221
|
+
def _title_for(tool_name: str, tool_args: Any) -> str:
|
|
222
|
+
"""Build a human-friendly tool-call title (e.g. ``Edit foo.py``)."""
|
|
223
|
+
if tool_name == "agent_run_shell_command" and isinstance(tool_args, dict):
|
|
224
|
+
command = str(tool_args.get("command", "")).strip()
|
|
225
|
+
if command:
|
|
226
|
+
short = command if len(command) <= 60 else command[:57] + "..."
|
|
227
|
+
return f"Run: {short}"
|
|
228
|
+
verb = _TOOL_VERBS.get(tool_name)
|
|
229
|
+
path = EventBridge._first_path(tool_args)
|
|
230
|
+
if verb and path:
|
|
231
|
+
return f"{verb} {os.path.basename(path)}"
|
|
232
|
+
return tool_name
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _first_path(tool_args: Any) -> Optional[str]:
|
|
236
|
+
if not isinstance(tool_args, dict):
|
|
237
|
+
return None
|
|
238
|
+
for key in _PATH_ARG_KEYS:
|
|
239
|
+
value = tool_args.get(key)
|
|
240
|
+
if isinstance(value, str) and value:
|
|
241
|
+
return value
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _locations_for(tool_args: Any) -> List[ToolCallLocation]:
|
|
246
|
+
"""Extract ACP ``locations`` (files this tool touches) from its args.
|
|
247
|
+
|
|
248
|
+
Lets the client highlight the affected file in the agent panel.
|
|
249
|
+
Best-effort: empty list when no recognizable path arg is present.
|
|
250
|
+
|
|
251
|
+
Uses ``resolve_path`` (not ``os.path.abspath``) so a relative tool-arg
|
|
252
|
+
path is absolutized against the *session's* cwd -- the same base the
|
|
253
|
+
tools themselves resolve against -- rather than the ACP process cwd,
|
|
254
|
+
which would point the client at the wrong file.
|
|
255
|
+
"""
|
|
256
|
+
path = EventBridge._first_path(tool_args)
|
|
257
|
+
return [ToolCallLocation(path=resolve_path(path))] if path else []
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _content_for(result: Any) -> list:
|
|
261
|
+
"""Attach the unified diff (if any) as an inline content block.
|
|
262
|
+
|
|
263
|
+
File-edit tools return a ``diff`` (unified diff text). We surface it as
|
|
264
|
+
a fenced ``diff`` code block so the change is visible inline in the
|
|
265
|
+
client's tool-call entry, in every ACP client — independent of whether
|
|
266
|
+
filesystem delegation is rendering a native diff too.
|
|
267
|
+
"""
|
|
268
|
+
if not isinstance(result, dict):
|
|
269
|
+
return []
|
|
270
|
+
diff = result.get("diff")
|
|
271
|
+
if not isinstance(diff, str) or not diff.strip():
|
|
272
|
+
return []
|
|
273
|
+
if len(diff) > _MAX_DIFF_CHARS:
|
|
274
|
+
diff = diff[: _MAX_DIFF_CHARS - 3] + "..."
|
|
275
|
+
return [tool_content(text_block(f"```diff\n{diff}\n```"))]
|
|
276
|
+
|
|
277
|
+
async def _send(self, session_id: str, update: Any) -> None:
|
|
278
|
+
"""Emit one ``session/update`` notification, swallowing failures.
|
|
279
|
+
|
|
280
|
+
A translation/transport hiccup on one event must never abort the agent
|
|
281
|
+
run, so we log and move on.
|
|
282
|
+
"""
|
|
283
|
+
connection = state.get_connection()
|
|
284
|
+
if connection is None:
|
|
285
|
+
return
|
|
286
|
+
try:
|
|
287
|
+
await connection.session_update(session_id, update)
|
|
288
|
+
except Exception: # noqa: BLE001
|
|
289
|
+
logger.exception("Failed to emit session/update")
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _is_success(result: Any) -> bool:
|
|
293
|
+
"""Best-effort success check mirroring frontend_emitter's heuristic."""
|
|
294
|
+
if isinstance(result, dict):
|
|
295
|
+
if result.get("error"):
|
|
296
|
+
return False
|
|
297
|
+
if result.get("success") is False:
|
|
298
|
+
return False
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _summarize(result: Any) -> Any:
|
|
303
|
+
"""Return a JSON-safe, size-bounded view of a tool result."""
|
|
304
|
+
if result is None or isinstance(result, (str, int, float, bool, dict, list)):
|
|
305
|
+
if isinstance(result, str) and len(result) > 4000:
|
|
306
|
+
return result[:3997] + "..."
|
|
307
|
+
return result
|
|
308
|
+
rendered = str(result)
|
|
309
|
+
return rendered[:3997] + "..." if len(rendered) > 4000 else rendered
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""ACP capability negotiation, expressed in the official SDK models.
|
|
2
|
+
|
|
3
|
+
Both sides of an ACP connection describe what they can do. The client sends its
|
|
4
|
+
``ClientCapabilities`` (can it read/write files? run terminals?) and expects
|
|
5
|
+
our ``AgentCapabilities`` back. The SDK provides typed models for both, so
|
|
6
|
+
this module is a thin, declarative mapping — no hand-rolled JSON.
|
|
7
|
+
|
|
8
|
+
Keeping the shapes here means ``agent.py`` stays about lifecycle, and the one
|
|
9
|
+
place we decide "what does Code Puppy advertise" is obvious and reviewable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Tuple
|
|
15
|
+
|
|
16
|
+
from acp.schema import (
|
|
17
|
+
AgentCapabilities,
|
|
18
|
+
ClientCapabilities,
|
|
19
|
+
PromptCapabilities,
|
|
20
|
+
SessionAdditionalDirectoriesCapabilities,
|
|
21
|
+
SessionCapabilities,
|
|
22
|
+
SessionCloseCapabilities,
|
|
23
|
+
SessionForkCapabilities,
|
|
24
|
+
SessionListCapabilities,
|
|
25
|
+
SessionResumeCapabilities,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def agent_capabilities() -> AgentCapabilities:
|
|
30
|
+
"""Describe what Code Puppy can do, for the ``initialize`` response.
|
|
31
|
+
|
|
32
|
+
* ``prompt_capabilities.embedded_context`` — we splice embedded text
|
|
33
|
+
resources into the prompt (see ``content.py``), so this is honestly
|
|
34
|
+
``True``. ``image`` is also ``True``: image blocks are decoded into
|
|
35
|
+
``BinaryContent`` / ``ImageUrl`` attachments (see ``content.py``). Audio
|
|
36
|
+
input is not consumed yet, so ``audio`` stays ``False``.
|
|
37
|
+
* ``load_session`` — we accept ``session/load`` and rehydrate the
|
|
38
|
+
session's persisted history into a fresh agent, so re-opened threads
|
|
39
|
+
continue the real conversation.
|
|
40
|
+
* ``session_capabilities`` — we support ``list``, ``close``, ``fork``
|
|
41
|
+
(in-memory history duplication), ``resume`` (persisted rehydration), and
|
|
42
|
+
``additional_directories`` (multi-root), all advertised via the SDK's
|
|
43
|
+
marker sub-models.
|
|
44
|
+
"""
|
|
45
|
+
return AgentCapabilities(
|
|
46
|
+
prompt_capabilities=PromptCapabilities(
|
|
47
|
+
image=True, audio=False, embedded_context=True
|
|
48
|
+
),
|
|
49
|
+
load_session=True,
|
|
50
|
+
session_capabilities=SessionCapabilities(
|
|
51
|
+
list=SessionListCapabilities(),
|
|
52
|
+
close=SessionCloseCapabilities(),
|
|
53
|
+
fork=SessionForkCapabilities(),
|
|
54
|
+
resume=SessionResumeCapabilities(),
|
|
55
|
+
additional_directories=SessionAdditionalDirectoriesCapabilities(),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def client_io_caps(caps: ClientCapabilities | None) -> Tuple[bool, bool, bool]:
|
|
61
|
+
"""Return ``(fs_read, fs_write, terminal)`` from the client's capabilities.
|
|
62
|
+
|
|
63
|
+
Drives which I/O edges we delegate to the client vs run locally. Defensive: a
|
|
64
|
+
``None`` blob or a newer client that omits fields degrades to "run
|
|
65
|
+
locally", never to a crash.
|
|
66
|
+
"""
|
|
67
|
+
if caps is None:
|
|
68
|
+
return False, False, False
|
|
69
|
+
fs = getattr(caps, "fs", None)
|
|
70
|
+
fs_read = bool(getattr(fs, "read_text_file", False)) if fs else False
|
|
71
|
+
fs_write = bool(getattr(fs, "write_text_file", False)) if fs else False
|
|
72
|
+
terminal = bool(getattr(caps, "terminal", False))
|
|
73
|
+
return fs_read, fs_write, terminal
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Execute client-typed slash commands headless and return their output.
|
|
2
|
+
|
|
3
|
+
The advertised ``available_commands_update`` tells the client which ``/commands``
|
|
4
|
+
Code Puppy exposes; when the user picks one, the client sends it as a prompt.
|
|
5
|
+
Rather than feed ``/help`` to the model, we route it through Code Puppy's
|
|
6
|
+
``command_handler`` and forward whatever it emits on the message bus back as an
|
|
7
|
+
``agent_message_chunk``.
|
|
8
|
+
|
|
9
|
+
Command side-effects go to the ``MessageBus`` (``emit_info`` etc.), which has no
|
|
10
|
+
renderer in ACP mode. We capture that output by attaching a listener + draining
|
|
11
|
+
the queue around the call. We deliberately do **not** touch ``sys.stdin`` — the
|
|
12
|
+
SDK reads the JSON-RPC pipe from it — so interactive prompt_toolkit menus (a TUI
|
|
13
|
+
affordance) simply aren't driven over ACP; non-interactive commands (``/help``,
|
|
14
|
+
status-style commands) work well. Everything is best-effort and never raises
|
|
15
|
+
into the turn.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Any, List, Optional
|
|
23
|
+
|
|
24
|
+
from code_puppy_core_plugins.acp import state
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_command(text: str) -> bool:
|
|
30
|
+
"""True if ``text`` is a leading ``/slash`` command."""
|
|
31
|
+
return text.strip().startswith("/")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def run(session_id: str, command: str) -> Optional[str]:
|
|
35
|
+
"""Execute ``command`` and forward its bus output to the client.
|
|
36
|
+
|
|
37
|
+
Returns ``None`` when the command was fully handled (its output has been
|
|
38
|
+
forwarded to the client). Returns a **string** when the command handler
|
|
39
|
+
signalled "process this as user input" -- i.e. a custom/markdown command
|
|
40
|
+
that expands into a prompt for the model. The caller then runs the agent on
|
|
41
|
+
that string, exactly as ``cli_runner`` does with a string command result.
|
|
42
|
+
Internal ``__SENTINEL__`` strings (e.g. ``__AUTOSAVE_LOAD__``) are TUI-only
|
|
43
|
+
affordances with no ACP meaning, so they are treated as handled, not modelled.
|
|
44
|
+
"""
|
|
45
|
+
from code_puppy.command_line.command_handler import handle_command
|
|
46
|
+
from code_puppy.messaging.message_queue import get_global_queue
|
|
47
|
+
|
|
48
|
+
queue = get_global_queue()
|
|
49
|
+
collected: List[Any] = []
|
|
50
|
+
|
|
51
|
+
def _listener(message: Any) -> None:
|
|
52
|
+
collected.append(message)
|
|
53
|
+
|
|
54
|
+
queue.add_listener(_listener)
|
|
55
|
+
result: Any = None
|
|
56
|
+
try:
|
|
57
|
+
result = await asyncio.to_thread(handle_command, command.strip())
|
|
58
|
+
except Exception: # noqa: BLE001
|
|
59
|
+
logger.debug("ACP: slash command failed", exc_info=True)
|
|
60
|
+
finally:
|
|
61
|
+
# Drain anything still queued (no renderer thread runs in ACP mode).
|
|
62
|
+
while True:
|
|
63
|
+
message = queue.get_nowait()
|
|
64
|
+
if message is None:
|
|
65
|
+
break
|
|
66
|
+
collected.append(message)
|
|
67
|
+
queue.remove_listener(_listener)
|
|
68
|
+
|
|
69
|
+
text = _render(collected)
|
|
70
|
+
if _is_prompt_expansion(result):
|
|
71
|
+
# Handler expanded the command into a prompt: surface any incidental bus
|
|
72
|
+
# output first, then hand the prompt back to the caller to run through the
|
|
73
|
+
# agent (not display it as a canned answer).
|
|
74
|
+
if text:
|
|
75
|
+
await _emit(session_id, text)
|
|
76
|
+
return result.strip()
|
|
77
|
+
await _emit(session_id, text or f"Ran `{command.strip()}`.")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_prompt_expansion(result: Any) -> bool:
|
|
82
|
+
"""True if ``handle_command`` returned a string to be modelled as a prompt.
|
|
83
|
+
|
|
84
|
+
A non-empty string means "process as user input"; ``__SENTINEL__`` strings
|
|
85
|
+
are internal TUI markers (e.g. ``__AUTOSAVE_LOAD__``) that must not reach
|
|
86
|
+
the model.
|
|
87
|
+
"""
|
|
88
|
+
if not isinstance(result, str):
|
|
89
|
+
return False
|
|
90
|
+
stripped = result.strip()
|
|
91
|
+
if not stripped:
|
|
92
|
+
return False
|
|
93
|
+
return not (stripped.startswith("__") and stripped.endswith("__"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _render(messages: List[Any]) -> str:
|
|
97
|
+
"""Flatten captured bus messages into display text."""
|
|
98
|
+
out: List[str] = []
|
|
99
|
+
for message in messages:
|
|
100
|
+
content = getattr(message, "content", None)
|
|
101
|
+
if content is None:
|
|
102
|
+
continue
|
|
103
|
+
out.append(content if isinstance(content, str) else str(content))
|
|
104
|
+
return "\n".join(s for s in out if s).strip()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _emit(session_id: str, text: str) -> None:
|
|
108
|
+
from acp.helpers import update_agent_message_text
|
|
109
|
+
|
|
110
|
+
connection = state.get_connection()
|
|
111
|
+
if connection is None or not text:
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
await connection.session_update(session_id, update_agent_message_text(text))
|
|
115
|
+
except Exception: # noqa: BLE001
|
|
116
|
+
logger.debug("ACP: command output emit failed", exc_info=True)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Parse ACP prompt content blocks into a Code Puppy prompt.
|
|
2
|
+
|
|
3
|
+
The client delivers a prompt as a list of typed content blocks. The SDK parses
|
|
4
|
+
the wire into pydantic models before our ``prompt`` handler runs, so we work
|
|
5
|
+
with objects (``TextContentBlock``, ``ImageContentBlock``,
|
|
6
|
+
``EmbeddedResourceContentBlock``, …) rather than raw dicts.
|
|
7
|
+
|
|
8
|
+
We split a prompt into three parts that map onto ``run_with_mcp``'s signature:
|
|
9
|
+
|
|
10
|
+
* **text** — text blocks, embedded text resources (spliced in under a header,
|
|
11
|
+
honouring the advertised ``embeddedContext`` capability), and resource-link
|
|
12
|
+
references.
|
|
13
|
+
* **attachments** — image blocks carrying base64 data → pydantic-ai
|
|
14
|
+
``BinaryContent`` (honouring the advertised ``image`` capability).
|
|
15
|
+
* **link_attachments** — image blocks carrying only a URI → ``ImageUrl``.
|
|
16
|
+
|
|
17
|
+
Blocks are duck-typed (``getattr``) so a newer SDK adding fields never breaks
|
|
18
|
+
parsing. Audio is not advertised, so audio blocks degrade to a text note.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import logging
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, List
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ParsedPrompt:
|
|
33
|
+
"""A prompt split into text + multimodal attachments for ``run_with_mcp``."""
|
|
34
|
+
|
|
35
|
+
text: str = ""
|
|
36
|
+
attachments: List[Any] = field(default_factory=list) # BinaryContent
|
|
37
|
+
link_attachments: List[Any] = field(default_factory=list) # ImageUrl
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_prompt(blocks: List[Any]) -> ParsedPrompt:
|
|
41
|
+
"""Split a list of ACP content blocks into text + attachments."""
|
|
42
|
+
parts: List[str] = []
|
|
43
|
+
result = ParsedPrompt()
|
|
44
|
+
for block in blocks or []:
|
|
45
|
+
text = _render_block(block, result)
|
|
46
|
+
if text:
|
|
47
|
+
parts.append(text)
|
|
48
|
+
result.text = "\n\n".join(parts)
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def flatten_prompt(blocks: List[Any]) -> str:
|
|
53
|
+
"""Back-compat text-only view (used where attachments aren't needed)."""
|
|
54
|
+
return parse_prompt(blocks).text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _render_block(block: Any, result: ParsedPrompt) -> str:
|
|
58
|
+
block_type = getattr(block, "type", None)
|
|
59
|
+
|
|
60
|
+
if block_type == "text":
|
|
61
|
+
return getattr(block, "text", "") or ""
|
|
62
|
+
|
|
63
|
+
if block_type == "resource":
|
|
64
|
+
return _render_embedded_resource(getattr(block, "resource", None))
|
|
65
|
+
|
|
66
|
+
if block_type == "resource_link":
|
|
67
|
+
uri = getattr(block, "uri", "") or ""
|
|
68
|
+
name = getattr(block, "name", "") or uri
|
|
69
|
+
return f"[Referenced resource: {name} ({uri})]" if uri else ""
|
|
70
|
+
|
|
71
|
+
if block_type == "image":
|
|
72
|
+
_collect_image(block, result)
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
if block_type == "audio":
|
|
76
|
+
return "[Audio attached]"
|
|
77
|
+
|
|
78
|
+
# Unknown/newer block type: fall back to any ``text`` it carries.
|
|
79
|
+
return getattr(block, "text", "") or ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _collect_image(block: Any, result: ParsedPrompt) -> None:
|
|
83
|
+
"""Turn an image block into a BinaryContent (data) or ImageUrl (uri)."""
|
|
84
|
+
data = getattr(block, "data", None)
|
|
85
|
+
mime = getattr(block, "mime_type", None) or "image/png"
|
|
86
|
+
if data:
|
|
87
|
+
try:
|
|
88
|
+
from pydantic_ai import BinaryContent
|
|
89
|
+
|
|
90
|
+
result.attachments.append(
|
|
91
|
+
BinaryContent(data=base64.b64decode(data), media_type=mime)
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
except Exception: # noqa: BLE001
|
|
95
|
+
logger.debug("ACP: could not decode image data", exc_info=True)
|
|
96
|
+
uri = getattr(block, "uri", None)
|
|
97
|
+
if uri:
|
|
98
|
+
try:
|
|
99
|
+
from pydantic_ai import ImageUrl
|
|
100
|
+
|
|
101
|
+
result.link_attachments.append(ImageUrl(url=uri))
|
|
102
|
+
except Exception: # noqa: BLE001
|
|
103
|
+
logger.debug("ACP: could not build ImageUrl", exc_info=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _render_embedded_resource(resource: Any) -> str:
|
|
107
|
+
"""Render an embedded resource; only the text variant carries usable text."""
|
|
108
|
+
if resource is None:
|
|
109
|
+
return ""
|
|
110
|
+
text = getattr(resource, "text", None)
|
|
111
|
+
if not text:
|
|
112
|
+
uri = getattr(resource, "uri", "") or ""
|
|
113
|
+
return f"[Embedded binary resource: {uri}]" if uri else ""
|
|
114
|
+
uri = getattr(resource, "uri", "") or "embedded"
|
|
115
|
+
return f"<context uri={uri!r}>\n{text}\n</context>"
|