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,231 @@
|
|
|
1
|
+
"""Wire Code Puppy's I/O backends to the client (file reads/writes + terminal).
|
|
2
|
+
|
|
3
|
+
This is the delegation layer that makes Code Puppy *truly native* inside the client:
|
|
4
|
+
workspace file reads/writes go through the client's ``fs/*`` methods (so edits show in
|
|
5
|
+
the client's diff UI and reads see unsaved buffers), and shell commands run through
|
|
6
|
+
the client's ``terminal/*`` methods (so they execute in the client's terminal).
|
|
7
|
+
|
|
8
|
+
It plugs into the two general core seams in ``code_puppy.tools.io_backends``:
|
|
9
|
+
|
|
10
|
+
* ``FileSystemBackend`` is **synchronous** — file tools run in Code Puppy's
|
|
11
|
+
tool threadpool, so ``DelegatedFileSystemBackend`` bridges to the ACP event loop
|
|
12
|
+
with ``run_coroutine_threadsafe`` (same pattern as the approval backend).
|
|
13
|
+
* ``CommandExecutor`` is **asynchronous** — the shell tool awaits it on the
|
|
14
|
+
loop, so ``DelegatedCommandExecutor`` calls the client directly.
|
|
15
|
+
|
|
16
|
+
Both are installed only when the connected client advertises the matching
|
|
17
|
+
capability in ``initialize``; otherwise Code Puppy keeps doing that I/O
|
|
18
|
+
locally. Everything talks to the client through the SDK's ``AgentSideConnection``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
from typing import Any, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
from code_puppy_core_plugins.acp import capabilities, state
|
|
30
|
+
from code_puppy.tools.io_backends import (
|
|
31
|
+
DirEntry,
|
|
32
|
+
ExecResult,
|
|
33
|
+
set_command_executor,
|
|
34
|
+
set_filesystem_backend,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def shell_invocation(command: str) -> Tuple[str, List[str]]:
|
|
41
|
+
"""Split a shell string into ``(program, args)`` for ``terminal/create``.
|
|
42
|
+
|
|
43
|
+
ACP's ``terminal/create`` takes a program + argv, so we run the command
|
|
44
|
+
string through the platform shell (``sh -c`` / ``cmd /c``).
|
|
45
|
+
"""
|
|
46
|
+
if sys.platform.startswith("win"):
|
|
47
|
+
return "cmd", ["/c", command]
|
|
48
|
+
return "/bin/sh", ["-c", command]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DelegatedFileSystemBackend:
|
|
52
|
+
"""Workspace file I/O that delegates *content* to the client's ``fs/*``.
|
|
53
|
+
|
|
54
|
+
Called from Code Puppy's tool threadpool, so the content methods bridge to
|
|
55
|
+
the ACP loop via ``run_coroutine_threadsafe`` and block the worker for the
|
|
56
|
+
answer. The loop stays free to service the round-trip, so there's no
|
|
57
|
+
deadlock; a guard bails out if we ever end up on the loop thread itself.
|
|
58
|
+
|
|
59
|
+
Topology/metadata (exists / is_file / is_dir / list_dir / delete /
|
|
60
|
+
make_dirs) is served from the **local disk**: an ACP editor host runs on
|
|
61
|
+
the same machine and workspace as the agent, so the local filesystem is the
|
|
62
|
+
authoritative source for what exists and how the tree is shaped -- only
|
|
63
|
+
*content* carries the host's unsaved-buffer overlay. This "topology is
|
|
64
|
+
local" decision lives here, in the adapter that legitimately knows its host
|
|
65
|
+
shares the disk, and never leaks into the general ``FileSystemBackend``
|
|
66
|
+
seam (a remote/virtual backend would implement these against its own store).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def read_text_file(
|
|
70
|
+
self, path: str, line: Optional[int] = None, limit: Optional[int] = None
|
|
71
|
+
) -> str:
|
|
72
|
+
async def _read(conn: Any, sid: str) -> str:
|
|
73
|
+
response = await conn.read_text_file(
|
|
74
|
+
path=path, session_id=sid, line=line, limit=limit
|
|
75
|
+
)
|
|
76
|
+
return getattr(response, "content", "") or ""
|
|
77
|
+
|
|
78
|
+
return self._bridge(_read)
|
|
79
|
+
|
|
80
|
+
def write_text_file(self, path: str, content: str) -> None:
|
|
81
|
+
async def _write(conn: Any, sid: str) -> None:
|
|
82
|
+
await conn.write_text_file(content=content, path=path, session_id=sid)
|
|
83
|
+
|
|
84
|
+
self._bridge(_write)
|
|
85
|
+
|
|
86
|
+
# --- topology / metadata: local disk (host shares it) ------------------
|
|
87
|
+
def exists(self, path: str) -> bool:
|
|
88
|
+
return os.path.exists(path)
|
|
89
|
+
|
|
90
|
+
def is_file(self, path: str) -> bool:
|
|
91
|
+
return os.path.isfile(path)
|
|
92
|
+
|
|
93
|
+
def is_dir(self, path: str) -> bool:
|
|
94
|
+
return os.path.isdir(path)
|
|
95
|
+
|
|
96
|
+
def list_dir(self, path: str) -> List[DirEntry]:
|
|
97
|
+
if not os.path.exists(path):
|
|
98
|
+
raise FileNotFoundError(path)
|
|
99
|
+
if not os.path.isdir(path):
|
|
100
|
+
raise NotADirectoryError(path)
|
|
101
|
+
entries: List[DirEntry] = []
|
|
102
|
+
for name in os.listdir(path):
|
|
103
|
+
full = os.path.join(path, name)
|
|
104
|
+
try:
|
|
105
|
+
if os.path.isdir(full):
|
|
106
|
+
entries.append(DirEntry(name=name, is_dir=True, size=0))
|
|
107
|
+
elif os.path.isfile(full):
|
|
108
|
+
size = os.path.getsize(full)
|
|
109
|
+
entries.append(DirEntry(name=name, is_dir=False, size=size))
|
|
110
|
+
except OSError:
|
|
111
|
+
continue
|
|
112
|
+
return entries
|
|
113
|
+
|
|
114
|
+
def delete_file(self, path: str) -> None:
|
|
115
|
+
os.remove(path)
|
|
116
|
+
|
|
117
|
+
def make_dirs(self, path: str) -> None:
|
|
118
|
+
if path:
|
|
119
|
+
os.makedirs(path, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
def _bridge(self, make_coro):
|
|
122
|
+
connection = state.get_connection()
|
|
123
|
+
loop = state.get_loop()
|
|
124
|
+
session_id = state.get_active_session_id()
|
|
125
|
+
if connection is None or loop is None:
|
|
126
|
+
raise RuntimeError("ACP filesystem backend used with no active connection")
|
|
127
|
+
if session_id is None:
|
|
128
|
+
raise RuntimeError("ACP filesystem backend used outside a session")
|
|
129
|
+
try:
|
|
130
|
+
running = asyncio.get_running_loop()
|
|
131
|
+
except RuntimeError:
|
|
132
|
+
running = None
|
|
133
|
+
if running is loop:
|
|
134
|
+
raise RuntimeError("ACP fs backend hit on the ACP loop (would deadlock)")
|
|
135
|
+
return asyncio.run_coroutine_threadsafe(
|
|
136
|
+
make_coro(connection, session_id), loop
|
|
137
|
+
).result()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DelegatedCommandExecutor:
|
|
141
|
+
"""Asynchronous shell execution that delegates to the client's ``terminal/*``.
|
|
142
|
+
|
|
143
|
+
Awaited on the ACP loop, so it talks to the client directly. Creates a terminal,
|
|
144
|
+
waits for exit (honoring ``timeout`` — kills on overrun), collects the
|
|
145
|
+
combined output, and always releases the terminal handle.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
async def run(self, command: str, cwd: Optional[str], timeout: int) -> ExecResult:
|
|
149
|
+
connection = state.get_connection()
|
|
150
|
+
session_id = state.get_active_session_id()
|
|
151
|
+
if connection is None:
|
|
152
|
+
raise RuntimeError("ACP command executor used with no active connection")
|
|
153
|
+
if session_id is None:
|
|
154
|
+
raise RuntimeError("ACP command executor used outside a session")
|
|
155
|
+
|
|
156
|
+
program, args = shell_invocation(command)
|
|
157
|
+
created = await connection.create_terminal(
|
|
158
|
+
command=program, session_id=session_id, args=args, cwd=cwd
|
|
159
|
+
)
|
|
160
|
+
terminal_id = getattr(created, "terminal_id", None)
|
|
161
|
+
if not terminal_id:
|
|
162
|
+
raise RuntimeError("the client did not return a terminalId")
|
|
163
|
+
|
|
164
|
+
timed_out = False
|
|
165
|
+
exit_code: Optional[int] = None
|
|
166
|
+
try:
|
|
167
|
+
try:
|
|
168
|
+
exited = await asyncio.wait_for(
|
|
169
|
+
connection.wait_for_terminal_exit(
|
|
170
|
+
session_id=session_id, terminal_id=terminal_id
|
|
171
|
+
),
|
|
172
|
+
timeout=timeout,
|
|
173
|
+
)
|
|
174
|
+
exit_code = getattr(exited, "exit_code", None)
|
|
175
|
+
except asyncio.TimeoutError:
|
|
176
|
+
timed_out = True
|
|
177
|
+
await self._kill(connection, session_id, terminal_id)
|
|
178
|
+
|
|
179
|
+
out = await connection.terminal_output(
|
|
180
|
+
session_id=session_id, terminal_id=terminal_id
|
|
181
|
+
)
|
|
182
|
+
output = getattr(out, "output", "") or ""
|
|
183
|
+
if exit_code is None:
|
|
184
|
+
status = getattr(out, "exit_status", None)
|
|
185
|
+
exit_code = getattr(status, "exit_code", None) if status else None
|
|
186
|
+
finally:
|
|
187
|
+
await self._release(connection, session_id, terminal_id)
|
|
188
|
+
|
|
189
|
+
if exit_code is None:
|
|
190
|
+
exit_code = -1 if timed_out else 0
|
|
191
|
+
return ExecResult(exit_code=int(exit_code), output=output, timed_out=timed_out)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
async def _kill(connection: Any, session_id: str, terminal_id: str) -> None:
|
|
195
|
+
try:
|
|
196
|
+
await connection.kill_terminal(
|
|
197
|
+
session_id=session_id, terminal_id=terminal_id
|
|
198
|
+
)
|
|
199
|
+
except Exception: # noqa: BLE001
|
|
200
|
+
logger.debug("terminal/kill failed", exc_info=True)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
async def _release(connection: Any, session_id: str, terminal_id: str) -> None:
|
|
204
|
+
try:
|
|
205
|
+
await connection.release_terminal(
|
|
206
|
+
session_id=session_id, terminal_id=terminal_id
|
|
207
|
+
)
|
|
208
|
+
except Exception: # noqa: BLE001
|
|
209
|
+
logger.debug("terminal/release failed", exc_info=True)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def install(client_capabilities: Any) -> None:
|
|
213
|
+
"""Install the client-backed I/O backends the client can actually service.
|
|
214
|
+
|
|
215
|
+
Capability-gated: only reroute reads/writes if the client advertises fs support,
|
|
216
|
+
and only reroute shell if the client advertises terminal support. Unsupported
|
|
217
|
+
edges keep running locally.
|
|
218
|
+
"""
|
|
219
|
+
fs_read, fs_write, terminal = capabilities.client_io_caps(client_capabilities)
|
|
220
|
+
if fs_read and fs_write:
|
|
221
|
+
set_filesystem_backend(DelegatedFileSystemBackend())
|
|
222
|
+
logger.debug("ACP: delegating workspace file I/O to the client")
|
|
223
|
+
if terminal:
|
|
224
|
+
set_command_executor(DelegatedCommandExecutor())
|
|
225
|
+
logger.debug("ACP: delegating shell execution to the client's terminal")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def uninstall() -> None:
|
|
229
|
+
"""Clear both backends so Code Puppy resumes local I/O."""
|
|
230
|
+
set_filesystem_backend(None)
|
|
231
|
+
set_command_executor(None)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Attach client-injected MCP servers (from ``session/new``) to a session agent.
|
|
2
|
+
|
|
3
|
+
ACP lets the client pass MCP server specs per session. We translate each into a
|
|
4
|
+
pydantic-ai MCP server and append it to the agent's ``_mcp_servers`` list;
|
|
5
|
+
``run_with_mcp`` starts them for that session's runs. Best-effort: a malformed
|
|
6
|
+
spec is skipped, never fatal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _kv(items: Any) -> Dict[str, str]:
|
|
18
|
+
"""Turn a list of ``{name, value}`` models into a plain dict."""
|
|
19
|
+
out: Dict[str, str] = {}
|
|
20
|
+
for item in items or []:
|
|
21
|
+
name = getattr(item, "name", None)
|
|
22
|
+
if name is not None:
|
|
23
|
+
out[name] = getattr(item, "value", "") or ""
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _translate(spec: Any) -> Optional[Any]:
|
|
28
|
+
"""Map one ACP MCP server spec to a pydantic-ai MCP server instance."""
|
|
29
|
+
from pydantic_ai.mcp import (
|
|
30
|
+
MCPServerSSE,
|
|
31
|
+
MCPServerStdio,
|
|
32
|
+
MCPServerStreamableHTTP,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
name = getattr(spec, "name", None) or None
|
|
36
|
+
if getattr(spec, "command", None):
|
|
37
|
+
return MCPServerStdio(
|
|
38
|
+
command=spec.command,
|
|
39
|
+
args=list(getattr(spec, "args", None) or []),
|
|
40
|
+
env=_kv(getattr(spec, "env", None)) or None,
|
|
41
|
+
tool_prefix=name,
|
|
42
|
+
)
|
|
43
|
+
url = getattr(spec, "url", None)
|
|
44
|
+
if not url:
|
|
45
|
+
return None
|
|
46
|
+
headers = _kv(getattr(spec, "headers", None)) or None
|
|
47
|
+
if getattr(spec, "type", None) == "sse":
|
|
48
|
+
return MCPServerSSE(url=url, headers=headers, tool_prefix=name)
|
|
49
|
+
return MCPServerStreamableHTTP(url=url, headers=headers, tool_prefix=name)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def attach(agent: Any, specs: List[Any]) -> None:
|
|
53
|
+
"""Append the translated MCP servers from ``specs`` to ``agent``."""
|
|
54
|
+
servers: List[Any] = []
|
|
55
|
+
for spec in specs or []:
|
|
56
|
+
try:
|
|
57
|
+
translated = _translate(spec)
|
|
58
|
+
except Exception: # noqa: BLE001
|
|
59
|
+
logger.debug("ACP: could not translate MCP server spec", exc_info=True)
|
|
60
|
+
continue
|
|
61
|
+
if translated is not None:
|
|
62
|
+
servers.append(translated)
|
|
63
|
+
if not servers:
|
|
64
|
+
return
|
|
65
|
+
existing = getattr(agent, "_mcp_servers", None) or []
|
|
66
|
+
agent._mcp_servers = list(existing) + servers
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""ACP permission integration — the client's dialog becomes the approval authority.
|
|
2
|
+
|
|
3
|
+
Code Puppy has two approval edges, and this module wires both to the client via the
|
|
4
|
+
SDK's ``AgentSideConnection.request_permission`` — without touching core tool
|
|
5
|
+
logic or forcing yolo mode:
|
|
6
|
+
|
|
7
|
+
* **File operations** go through the pluggable *approval backend* seam in
|
|
8
|
+
``code_puppy.tools.common``. Sync file tools run in Code Puppy's tool
|
|
9
|
+
threadpool, so the backend bridges to the ACP event loop via
|
|
10
|
+
``run_coroutine_threadsafe`` to ask the client, then blocks the worker thread for
|
|
11
|
+
the answer. The loop stays free to service the round-trip — no deadlock.
|
|
12
|
+
* **Shell commands** go through the ``run_shell_command`` hook, which is async
|
|
13
|
+
and already runs on the ACP loop, so it can ``await`` the client directly.
|
|
14
|
+
|
|
15
|
+
Both edges fail **closed** (deny) if the connection is gone or the client errors.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import logging
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Any, Dict, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
from acp.schema import PermissionOption, ToolCallUpdate
|
|
26
|
+
|
|
27
|
+
from code_puppy.callbacks import register_callback
|
|
28
|
+
from code_puppy_core_plugins.acp import state
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# How long to wait for a human in the client to answer a permission dialog. Dialogs
|
|
33
|
+
# can sit for a while; deny (fail closed) if it's truly abandoned.
|
|
34
|
+
_PERMISSION_TIMEOUT_S = 600
|
|
35
|
+
|
|
36
|
+
# The allow/deny options we present. The client echoes back the chosen ``option_id``.
|
|
37
|
+
_OPTIONS = [
|
|
38
|
+
PermissionOption(option_id="allow_once", name="Allow", kind="allow_once"),
|
|
39
|
+
PermissionOption(option_id="reject_once", name="Reject", kind="reject_once"),
|
|
40
|
+
]
|
|
41
|
+
_ALLOW_IDS = {"allow_once"}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _tool_call_ref(title: str) -> ToolCallUpdate:
|
|
45
|
+
"""Build the ``toolCall`` ref a permission request attaches to.
|
|
46
|
+
|
|
47
|
+
Reuses the id/title of the tool call currently open (set by
|
|
48
|
+
``pre_tool_call``) so the client pins the dialog to the right agent-panel entry;
|
|
49
|
+
falls back to a standalone id when no tool call is active.
|
|
50
|
+
"""
|
|
51
|
+
current = state.current_tool_call()
|
|
52
|
+
if current is not None:
|
|
53
|
+
name, tool_call_id = current
|
|
54
|
+
return ToolCallUpdate(tool_call_id=tool_call_id, title=name)
|
|
55
|
+
return ToolCallUpdate(tool_call_id=f"perm_{uuid.uuid4().hex[:12]}", title=title)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _ask_client(session_id: str, title: str) -> bool:
|
|
59
|
+
"""Show an allow/deny dialog in the client; return ``True`` if allowed."""
|
|
60
|
+
connection = state.get_connection()
|
|
61
|
+
if connection is None:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
response = await connection.request_permission(
|
|
65
|
+
options=list(_OPTIONS),
|
|
66
|
+
session_id=session_id,
|
|
67
|
+
tool_call=_tool_call_ref(title),
|
|
68
|
+
)
|
|
69
|
+
except Exception: # noqa: BLE001
|
|
70
|
+
logger.exception("session/request_permission failed; denying")
|
|
71
|
+
return False
|
|
72
|
+
outcome = getattr(response, "outcome", None)
|
|
73
|
+
if getattr(outcome, "outcome", None) == "selected":
|
|
74
|
+
return getattr(outcome, "option_id", None) in _ALLOW_IDS
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _approval_backend(
|
|
79
|
+
title: str, message: str, preview: Optional[str]
|
|
80
|
+
) -> Tuple[bool, Optional[str]]:
|
|
81
|
+
"""Approval backend for file ops; asks the client from the tool threadpool.
|
|
82
|
+
|
|
83
|
+
Returns ``(approved, feedback)``. Feedback is always ``None`` — the client's
|
|
84
|
+
dialog is yes/no, not a free-text channel.
|
|
85
|
+
"""
|
|
86
|
+
loop = state.get_loop()
|
|
87
|
+
session_id = state.get_active_session_id()
|
|
88
|
+
if loop is None or session_id is None:
|
|
89
|
+
return False, None
|
|
90
|
+
|
|
91
|
+
# Guard against being called on the loop thread itself: blocking on
|
|
92
|
+
# run_coroutine_threadsafe there would deadlock (file tools run off-loop).
|
|
93
|
+
try:
|
|
94
|
+
running = asyncio.get_running_loop()
|
|
95
|
+
except RuntimeError:
|
|
96
|
+
running = None
|
|
97
|
+
if running is loop:
|
|
98
|
+
logger.error("Approval backend hit on the ACP loop; denying to avoid deadlock")
|
|
99
|
+
return False, None
|
|
100
|
+
|
|
101
|
+
future = asyncio.run_coroutine_threadsafe(_ask_client(session_id, title), loop)
|
|
102
|
+
try:
|
|
103
|
+
allowed = future.result(_PERMISSION_TIMEOUT_S)
|
|
104
|
+
except Exception: # noqa: BLE001 - includes TimeoutError
|
|
105
|
+
# Abandon the in-flight request so it doesn't linger on the loop.
|
|
106
|
+
future.cancel()
|
|
107
|
+
logger.exception("ACP approval bridge failed; denying")
|
|
108
|
+
return False, None
|
|
109
|
+
return bool(allowed), None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _on_run_shell_command(
|
|
113
|
+
context: Any, command: str, cwd: Optional[str] = None, timeout: int = 60
|
|
114
|
+
) -> Optional[Dict[str, Any]]:
|
|
115
|
+
"""``run_shell_command`` hook: gate shell execution through the client.
|
|
116
|
+
|
|
117
|
+
Returns ``None`` to allow (the hook's "no objection"), or a
|
|
118
|
+
``{"blocked": True, ...}`` dict to deny. Inert outside ACP mode.
|
|
119
|
+
|
|
120
|
+
Honors yolo mode: when the user has opted out of approvals we do *not*
|
|
121
|
+
surface a client dialog, matching Code Puppy's file-permission edge (which
|
|
122
|
+
skips its prompt in yolo mode). Otherwise ACP + yolo would silently
|
|
123
|
+
auto-approve file writes yet still prompt for every shell command.
|
|
124
|
+
"""
|
|
125
|
+
session_id = state.get_active_session_id()
|
|
126
|
+
if state.get_connection() is None or session_id is None:
|
|
127
|
+
return None
|
|
128
|
+
from code_puppy.config import get_yolo_mode
|
|
129
|
+
|
|
130
|
+
if get_yolo_mode():
|
|
131
|
+
return None
|
|
132
|
+
allowed = await _ask_client(session_id, f"Run shell command: {command}")
|
|
133
|
+
if allowed:
|
|
134
|
+
return None
|
|
135
|
+
return {
|
|
136
|
+
"blocked": True,
|
|
137
|
+
"error_message": "Command rejected in the client",
|
|
138
|
+
"reasoning": "The user denied this shell command in the client's permission dialog.",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def install() -> None:
|
|
143
|
+
"""Install the file approval backend + the shell permission hook."""
|
|
144
|
+
from code_puppy.tools.common import set_approval_backend
|
|
145
|
+
|
|
146
|
+
set_approval_backend(_approval_backend)
|
|
147
|
+
register_callback("run_shell_command", _on_run_shell_command)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def uninstall() -> None:
|
|
151
|
+
"""Remove the approval backend + shell hook so normal stdin prompting resumes."""
|
|
152
|
+
from code_puppy.callbacks import unregister_callback
|
|
153
|
+
from code_puppy.tools.common import set_approval_backend
|
|
154
|
+
|
|
155
|
+
set_approval_backend(None)
|
|
156
|
+
try:
|
|
157
|
+
unregister_callback("run_shell_command", _on_run_shell_command)
|
|
158
|
+
except Exception: # noqa: BLE001
|
|
159
|
+
logger.debug("ACP: run_shell_command unregister failed", exc_info=True)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Persist ACP session history so ``session/load`` and ``session/resume``
|
|
2
|
+
rehydrate real turns across process restarts.
|
|
3
|
+
|
|
4
|
+
Each session's pydantic-ai message history is pickled under a dedicated
|
|
5
|
+
directory keyed by the ACP session id, reusing code-puppy's existing
|
|
6
|
+
``session_storage`` layer. We keep it separate from the user's autosaves
|
|
7
|
+
(``AUTOSAVE_DIR/acp``) so ACP threads never pollute the ``/resume`` picker.
|
|
8
|
+
|
|
9
|
+
Alongside the pickle we write a small ACP metadata sidecar (``*_acp.json``)
|
|
10
|
+
recording the session's own ``cwd`` + ``additional_directories``. This is what
|
|
11
|
+
lets ``list_persisted`` surface revivable sessions after a
|
|
12
|
+
restart *with the ``cwd`` that ACP's ``SessionInfo`` requires* -- the pickle
|
|
13
|
+
alone can't answer "where did this thread live?", and ``session_storage``'s
|
|
14
|
+
own ``_meta.json`` records the *process* cwd, which is wrong for per-session
|
|
15
|
+
ACP threads.
|
|
16
|
+
|
|
17
|
+
Every public entry point takes an optional ``base_dir``: production callers
|
|
18
|
+
omit it (it defaults to ``AUTOSAVE_DIR/acp``), while tests pass an explicit
|
|
19
|
+
directory so persistence is exercised end-to-end with zero shared global
|
|
20
|
+
state -- no monkeypatching of a module-level location.
|
|
21
|
+
|
|
22
|
+
All operations are best-effort: persistence must never break a live turn, so
|
|
23
|
+
failures are logged and swallowed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import datetime
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, List, Optional
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PersistedSession:
|
|
40
|
+
"""On-disk record of an ACP session, enough to re-list + revive it."""
|
|
41
|
+
|
|
42
|
+
session_id: str
|
|
43
|
+
cwd: Optional[str] = None
|
|
44
|
+
additional_directories: Optional[List[str]] = None
|
|
45
|
+
updated_at: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _base_dir() -> Path:
|
|
49
|
+
from code_puppy.config import AUTOSAVE_DIR
|
|
50
|
+
|
|
51
|
+
return Path(AUTOSAVE_DIR) / "acp"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_base(base_dir: Optional[Path]) -> Path:
|
|
55
|
+
"""Return the caller's ``base_dir`` or the default ``AUTOSAVE_DIR/acp``."""
|
|
56
|
+
return Path(base_dir) if base_dir is not None else _base_dir()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_name(session_id: str) -> str:
|
|
60
|
+
"""Sanitise a session id into a filesystem-safe pickle stem."""
|
|
61
|
+
return "".join(c if (c.isalnum() or c in "-_") else "_" for c in session_id)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Sidecar suffix for ACP-specific metadata. Distinct from ``session_storage``'s
|
|
65
|
+
# own ``_meta.json`` so the two never collide.
|
|
66
|
+
_ACP_META_SUFFIX = "_acp.json"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _acp_meta_path(base: Path, session_id: str) -> Path:
|
|
70
|
+
return base / f"{_safe_name(session_id)}{_ACP_META_SUFFIX}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save(
|
|
74
|
+
session_id: str,
|
|
75
|
+
agent: Any,
|
|
76
|
+
cwd: Optional[str] = None,
|
|
77
|
+
additional_directories: Optional[List[str]] = None,
|
|
78
|
+
base_dir: Optional[Path] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Persist ``agent``'s message history under ``session_id`` (best-effort).
|
|
81
|
+
|
|
82
|
+
Also writes an ACP metadata sidecar carrying the session's ``cwd`` +
|
|
83
|
+
``additional_directories`` so ``list_persisted`` can surface it after a
|
|
84
|
+
restart.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
from code_puppy.session_storage import save_session
|
|
88
|
+
|
|
89
|
+
base = _resolve_base(base_dir)
|
|
90
|
+
history = list(agent.get_message_history())
|
|
91
|
+
if not history:
|
|
92
|
+
return
|
|
93
|
+
timestamp = datetime.datetime.now().isoformat()
|
|
94
|
+
save_session(
|
|
95
|
+
history=history,
|
|
96
|
+
session_name=_safe_name(session_id),
|
|
97
|
+
base_dir=base,
|
|
98
|
+
timestamp=timestamp,
|
|
99
|
+
token_estimator=getattr(agent, "estimate_tokens_for_message", lambda _m: 0),
|
|
100
|
+
auto_saved=True,
|
|
101
|
+
)
|
|
102
|
+
_write_acp_meta(
|
|
103
|
+
base,
|
|
104
|
+
PersistedSession(
|
|
105
|
+
session_id=session_id,
|
|
106
|
+
cwd=cwd,
|
|
107
|
+
additional_directories=list(additional_directories or []) or None,
|
|
108
|
+
updated_at=timestamp,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
except Exception: # noqa: BLE001
|
|
112
|
+
logger.debug("ACP: session persist failed", exc_info=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_acp_meta(base: Path, record: PersistedSession) -> None:
|
|
116
|
+
"""Write the ACP metadata sidecar for ``record`` (best-effort, atomic)."""
|
|
117
|
+
try:
|
|
118
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
path = _acp_meta_path(base, record.session_id)
|
|
120
|
+
payload = {
|
|
121
|
+
"session_id": record.session_id,
|
|
122
|
+
"cwd": record.cwd,
|
|
123
|
+
"additional_directories": record.additional_directories,
|
|
124
|
+
"updated_at": record.updated_at,
|
|
125
|
+
}
|
|
126
|
+
tmp = path.with_suffix(".tmp")
|
|
127
|
+
tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
128
|
+
tmp.replace(path)
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
logger.debug("ACP: session meta persist failed", exc_info=True)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_history(
|
|
134
|
+
session_id: str, base_dir: Optional[Path] = None
|
|
135
|
+
) -> Optional[List[Any]]:
|
|
136
|
+
"""Return the persisted history for ``session_id``, or ``None`` if none."""
|
|
137
|
+
try:
|
|
138
|
+
from code_puppy.session_storage import load_session
|
|
139
|
+
|
|
140
|
+
return list(load_session(_safe_name(session_id), _resolve_base(base_dir)))
|
|
141
|
+
except FileNotFoundError:
|
|
142
|
+
return None
|
|
143
|
+
except Exception: # noqa: BLE001
|
|
144
|
+
logger.debug("ACP: session load failed", exc_info=True)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def list_persisted(base_dir: Optional[Path] = None) -> List[PersistedSession]:
|
|
149
|
+
"""Return every ACP session persisted on disk (newest first).
|
|
150
|
+
|
|
151
|
+
Reads the ACP metadata sidecars so revivable sessions survive process
|
|
152
|
+
restarts. A session whose pickle vanished (only the sidecar remains) is
|
|
153
|
+
skipped -- there is nothing left to rehydrate.
|
|
154
|
+
"""
|
|
155
|
+
base = _resolve_base(base_dir)
|
|
156
|
+
if not base.exists():
|
|
157
|
+
return []
|
|
158
|
+
records: List[PersistedSession] = []
|
|
159
|
+
for meta_path in base.glob(f"*{_ACP_META_SUFFIX}"):
|
|
160
|
+
try:
|
|
161
|
+
data = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
162
|
+
session_id = data.get("session_id")
|
|
163
|
+
if not session_id:
|
|
164
|
+
continue
|
|
165
|
+
if not (base / f"{_safe_name(session_id)}.pkl").exists():
|
|
166
|
+
continue
|
|
167
|
+
records.append(
|
|
168
|
+
PersistedSession(
|
|
169
|
+
session_id=session_id,
|
|
170
|
+
cwd=data.get("cwd"),
|
|
171
|
+
additional_directories=data.get("additional_directories"),
|
|
172
|
+
updated_at=data.get("updated_at"),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
except Exception: # noqa: BLE001
|
|
176
|
+
logger.debug("ACP: skipping unreadable session meta", exc_info=True)
|
|
177
|
+
records.sort(key=lambda r: r.updated_at or "", reverse=True)
|
|
178
|
+
return records
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def delete(session_id: str, base_dir: Optional[Path] = None) -> None:
|
|
182
|
+
"""Remove a persisted session's pickle + sidecars (best-effort).
|
|
183
|
+
|
|
184
|
+
Called when a client explicitly closes a session so a deliberate close
|
|
185
|
+
doesn't leave a ghost that reappears in the next ``list_sessions``.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
base = _resolve_base(base_dir)
|
|
189
|
+
stem = _safe_name(session_id)
|
|
190
|
+
for path in (
|
|
191
|
+
base / f"{stem}.pkl",
|
|
192
|
+
base / f"{stem}_meta.json",
|
|
193
|
+
_acp_meta_path(base, session_id),
|
|
194
|
+
):
|
|
195
|
+
path.unlink(missing_ok=True)
|
|
196
|
+
except Exception: # noqa: BLE001
|
|
197
|
+
logger.debug("ACP: session delete failed", exc_info=True)
|