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,584 @@
|
|
|
1
|
+
"""macOS Accessibility and Quartz bridge.
|
|
2
|
+
|
|
3
|
+
PyObjC is imported lazily so Code Puppy remains importable on every platform.
|
|
4
|
+
Element IDs are intentionally snapshot-scoped: any new snapshot invalidates the
|
|
5
|
+
previous IDs, preventing an agent from acting on a stale accessibility tree.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import tempfile
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .accessibility import snapshot_tree
|
|
17
|
+
from .activation import activate_application, activate_state
|
|
18
|
+
from .backend_types import ComputerUseError
|
|
19
|
+
from .capture import capture_window
|
|
20
|
+
from .keycodes import KEY_CODES
|
|
21
|
+
from .policy import policy_store
|
|
22
|
+
from .safety import require_safe_state
|
|
23
|
+
from .state import state_store
|
|
24
|
+
|
|
25
|
+
MAX_NODES = 500
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MacOSBackend:
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._elements: dict[int, Any] = {}
|
|
31
|
+
self._lock = threading.RLock()
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _api():
|
|
35
|
+
try:
|
|
36
|
+
import ApplicationServices
|
|
37
|
+
except ImportError as exc:
|
|
38
|
+
raise ComputerUseError(
|
|
39
|
+
"Computer Use requires the macOS extra. Install it with "
|
|
40
|
+
"`pip install 'code-puppy[computer-use]'`."
|
|
41
|
+
) from exc
|
|
42
|
+
return ApplicationServices
|
|
43
|
+
|
|
44
|
+
def trusted(self, prompt: bool = False) -> bool:
|
|
45
|
+
quartz = self._api()
|
|
46
|
+
options = {quartz.kAXTrustedCheckOptionPrompt: bool(prompt)}
|
|
47
|
+
return bool(quartz.AXIsProcessTrustedWithOptions(options))
|
|
48
|
+
|
|
49
|
+
def _require_trusted(self, prompt: bool = True) -> Any:
|
|
50
|
+
quartz = self._api()
|
|
51
|
+
if not self.trusted(prompt=prompt):
|
|
52
|
+
raise ComputerUseError(
|
|
53
|
+
"Accessibility permission is required. Enable Code Puppy (or its "
|
|
54
|
+
"terminal) in System Settings > Privacy & Security > Accessibility."
|
|
55
|
+
)
|
|
56
|
+
return quartz
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _copy_attribute(quartz: Any, element: Any, attribute: str) -> Any:
|
|
60
|
+
result = quartz.AXUIElementCopyAttributeValue(element, attribute, None)
|
|
61
|
+
if isinstance(result, tuple):
|
|
62
|
+
error, value = result
|
|
63
|
+
if error != quartz.kAXErrorSuccess:
|
|
64
|
+
return None
|
|
65
|
+
return value
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
def _application(self, app_name: str | None) -> tuple[Any, str]:
|
|
69
|
+
quartz = self._require_trusted()
|
|
70
|
+
if app_name:
|
|
71
|
+
try:
|
|
72
|
+
from AppKit import NSWorkspace
|
|
73
|
+
except ImportError as exc:
|
|
74
|
+
raise ComputerUseError(
|
|
75
|
+
"AppKit bindings are unavailable; reinstall the computer-use extra."
|
|
76
|
+
) from exc
|
|
77
|
+
wanted = app_name.casefold()
|
|
78
|
+
for app in NSWorkspace.sharedWorkspace().runningApplications():
|
|
79
|
+
name = str(app.localizedName() or "")
|
|
80
|
+
bundle = str(app.bundleIdentifier() or "")
|
|
81
|
+
if wanted in {name.casefold(), bundle.casefold()}:
|
|
82
|
+
return quartz.AXUIElementCreateApplication(
|
|
83
|
+
app.processIdentifier()
|
|
84
|
+
), name
|
|
85
|
+
raise ComputerUseError(f"Running application not found: {app_name}")
|
|
86
|
+
|
|
87
|
+
system = quartz.AXUIElementCreateSystemWide()
|
|
88
|
+
app = self._copy_attribute(
|
|
89
|
+
quartz, system, quartz.kAXFocusedApplicationAttribute
|
|
90
|
+
)
|
|
91
|
+
if app is None:
|
|
92
|
+
try:
|
|
93
|
+
from AppKit import NSWorkspace
|
|
94
|
+
except ImportError as exc:
|
|
95
|
+
raise ComputerUseError(
|
|
96
|
+
"No focused macOS application was found."
|
|
97
|
+
) from exc
|
|
98
|
+
frontmost = NSWorkspace.sharedWorkspace().frontmostApplication()
|
|
99
|
+
if frontmost is None:
|
|
100
|
+
raise ComputerUseError("No focused macOS application was found.")
|
|
101
|
+
app = quartz.AXUIElementCreateApplication(frontmost.processIdentifier())
|
|
102
|
+
fallback_name = str(frontmost.localizedName() or "Focused application")
|
|
103
|
+
else:
|
|
104
|
+
fallback_name = "Focused application"
|
|
105
|
+
title = self._copy_attribute(quartz, app, quartz.kAXTitleAttribute)
|
|
106
|
+
return app, str(title or fallback_name)
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _bundle_id(app_name: str) -> str:
|
|
110
|
+
try:
|
|
111
|
+
from AppKit import NSWorkspace
|
|
112
|
+
except ImportError as exc:
|
|
113
|
+
raise ComputerUseError("AppKit bindings are unavailable.") from exc
|
|
114
|
+
wanted = app_name.casefold()
|
|
115
|
+
for app in NSWorkspace.sharedWorkspace().runningApplications():
|
|
116
|
+
name = str(app.localizedName() or "")
|
|
117
|
+
bundle = str(app.bundleIdentifier() or "")
|
|
118
|
+
if wanted in {name.casefold(), bundle.casefold()}:
|
|
119
|
+
return bundle
|
|
120
|
+
raise ComputerUseError(f"Running application not found: {app_name}")
|
|
121
|
+
|
|
122
|
+
def require_state(
|
|
123
|
+
self,
|
|
124
|
+
revision: str,
|
|
125
|
+
*,
|
|
126
|
+
consume: bool = False,
|
|
127
|
+
):
|
|
128
|
+
state = require_safe_state(
|
|
129
|
+
revision,
|
|
130
|
+
)
|
|
131
|
+
activate_state(state)
|
|
132
|
+
if consume:
|
|
133
|
+
return require_safe_state(revision, consume=True)
|
|
134
|
+
return state
|
|
135
|
+
|
|
136
|
+
def snapshot(
|
|
137
|
+
self,
|
|
138
|
+
app_name: str | None = None,
|
|
139
|
+
max_nodes: int = MAX_NODES,
|
|
140
|
+
preferred_window_id: int | None = None,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
policy_store.require_enabled()
|
|
143
|
+
quartz = self._require_trusted()
|
|
144
|
+
application, resolved_name = self._application(app_name)
|
|
145
|
+
policy_store.require(self._bundle_id(resolved_name))
|
|
146
|
+
nodes, elements, metadata = snapshot_tree(
|
|
147
|
+
quartz,
|
|
148
|
+
application,
|
|
149
|
+
self._copy_attribute,
|
|
150
|
+
max_nodes,
|
|
151
|
+
preferred_window_id,
|
|
152
|
+
)
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._elements = elements
|
|
155
|
+
return {
|
|
156
|
+
"success": True,
|
|
157
|
+
"application": resolved_name,
|
|
158
|
+
"node_count": len(nodes),
|
|
159
|
+
**metadata,
|
|
160
|
+
"nodes": nodes,
|
|
161
|
+
"warning": "Element IDs expire when the next snapshot is taken.",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def get_app_state(
|
|
165
|
+
self, app_name: str, max_nodes: int = MAX_NODES
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
if not app_name:
|
|
168
|
+
raise ComputerUseError("app_name is required.")
|
|
169
|
+
policy_store.require(self._bundle_id(app_name))
|
|
170
|
+
target = (
|
|
171
|
+
Path(tempfile.gettempdir())
|
|
172
|
+
/ f"code-puppy-app-state-{int(time.time() * 1000)}.png"
|
|
173
|
+
)
|
|
174
|
+
capture = capture_window(app_name, target)
|
|
175
|
+
# Electron apps expose their useful web AX subtree only while frontmost —
|
|
176
|
+
# activate before reading state so the model gets semantic controls, not an
|
|
177
|
+
# almost-empty generic window tree.
|
|
178
|
+
activate_application(
|
|
179
|
+
int(capture["pid"]),
|
|
180
|
+
str(capture["application"]),
|
|
181
|
+
)
|
|
182
|
+
snapshot = self.snapshot(
|
|
183
|
+
app_name,
|
|
184
|
+
max_nodes,
|
|
185
|
+
preferred_window_id=int(capture["window_id"]),
|
|
186
|
+
)
|
|
187
|
+
with self._lock:
|
|
188
|
+
elements = dict(self._elements)
|
|
189
|
+
state = state_store.create(
|
|
190
|
+
application=str(capture["application"]),
|
|
191
|
+
bundle_id=str(capture["bundle_id"]),
|
|
192
|
+
pid=int(capture["pid"]),
|
|
193
|
+
window_id=int(capture["window_id"]),
|
|
194
|
+
window_title=str(capture["window_title"]),
|
|
195
|
+
geometry=capture["geometry"],
|
|
196
|
+
screenshot_path=str(capture["path"]),
|
|
197
|
+
elements=elements,
|
|
198
|
+
)
|
|
199
|
+
return {
|
|
200
|
+
"success": True,
|
|
201
|
+
**state.public_metadata(),
|
|
202
|
+
"node_count": snapshot["node_count"],
|
|
203
|
+
"scanned_node_count": snapshot["scanned_node_count"],
|
|
204
|
+
"truncated": snapshot["truncated"],
|
|
205
|
+
"scan_limit_reached": snapshot["scan_limit_reached"],
|
|
206
|
+
"overflow_summary": snapshot["overflow_summary"],
|
|
207
|
+
"nodes": snapshot["nodes"],
|
|
208
|
+
"warning": (
|
|
209
|
+
"Use this state_revision for one mutation or one guarded batch, "
|
|
210
|
+
"then fetch fresh app state."
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def _element(
|
|
215
|
+
self, state_revision: str, element_id: int, *, consume: bool = True
|
|
216
|
+
) -> tuple[Any, Any]:
|
|
217
|
+
quartz = self._require_trusted()
|
|
218
|
+
state = self.require_state(state_revision)
|
|
219
|
+
element = state.elements.get(element_id)
|
|
220
|
+
if element is None:
|
|
221
|
+
raise ComputerUseError(
|
|
222
|
+
f"Unknown or expired element ID {element_id}; take a new snapshot."
|
|
223
|
+
)
|
|
224
|
+
if consume:
|
|
225
|
+
self.require_state(state_revision, consume=True)
|
|
226
|
+
return quartz, element
|
|
227
|
+
|
|
228
|
+
def click(
|
|
229
|
+
self, state_revision: str, element_id: int, *, consume: bool = True
|
|
230
|
+
) -> dict[str, Any]:
|
|
231
|
+
quartz, element = self._element(state_revision, element_id, consume=consume)
|
|
232
|
+
error = quartz.AXUIElementPerformAction(element, quartz.kAXPressAction)
|
|
233
|
+
if error != quartz.kAXErrorSuccess:
|
|
234
|
+
raise ComputerUseError(f"AXPress failed with accessibility error {error}.")
|
|
235
|
+
return {
|
|
236
|
+
"success": True,
|
|
237
|
+
"element_id": element_id,
|
|
238
|
+
"consumed_state_revision": state_revision,
|
|
239
|
+
"state_invalidated": True,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def set_value(
|
|
243
|
+
self,
|
|
244
|
+
state_revision: str,
|
|
245
|
+
element_id: int,
|
|
246
|
+
value: str,
|
|
247
|
+
*,
|
|
248
|
+
consume: bool = True,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
quartz, element = self._element(state_revision, element_id, consume=consume)
|
|
251
|
+
error = quartz.AXUIElementSetAttributeValue(
|
|
252
|
+
element, quartz.kAXValueAttribute, value
|
|
253
|
+
)
|
|
254
|
+
if error != quartz.kAXErrorSuccess:
|
|
255
|
+
raise ComputerUseError(
|
|
256
|
+
f"Setting AXValue failed with accessibility error {error}."
|
|
257
|
+
)
|
|
258
|
+
return {
|
|
259
|
+
"success": True,
|
|
260
|
+
"element_id": element_id,
|
|
261
|
+
"consumed_state_revision": state_revision,
|
|
262
|
+
"state_invalidated": True,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def perform_action(
|
|
266
|
+
self,
|
|
267
|
+
state_revision: str,
|
|
268
|
+
element_id: int,
|
|
269
|
+
action: str,
|
|
270
|
+
*,
|
|
271
|
+
consume: bool = True,
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
quartz, element = self._element(state_revision, element_id, consume=False)
|
|
274
|
+
result = quartz.AXUIElementCopyActionNames(element, None)
|
|
275
|
+
if isinstance(result, tuple):
|
|
276
|
+
error, names = result
|
|
277
|
+
actions = list(names or []) if error == quartz.kAXErrorSuccess else []
|
|
278
|
+
else:
|
|
279
|
+
actions = list(result or [])
|
|
280
|
+
matched = next(
|
|
281
|
+
(
|
|
282
|
+
str(item)
|
|
283
|
+
for item in actions
|
|
284
|
+
if str(item).casefold() == action.casefold()
|
|
285
|
+
),
|
|
286
|
+
None,
|
|
287
|
+
)
|
|
288
|
+
if matched is None:
|
|
289
|
+
raise ComputerUseError(
|
|
290
|
+
f"{action!r} is not exposed by element {element_id}. "
|
|
291
|
+
f"Available actions: {[str(item) for item in actions]}"
|
|
292
|
+
)
|
|
293
|
+
if consume:
|
|
294
|
+
self.require_state(state_revision, consume=True)
|
|
295
|
+
error = quartz.AXUIElementPerformAction(element, matched)
|
|
296
|
+
if error != quartz.kAXErrorSuccess:
|
|
297
|
+
raise ComputerUseError(f"Accessibility action failed with error {error}.")
|
|
298
|
+
return {
|
|
299
|
+
"success": True,
|
|
300
|
+
"element_id": element_id,
|
|
301
|
+
"action": matched,
|
|
302
|
+
"state_invalidated": True,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def select_text(
|
|
306
|
+
self,
|
|
307
|
+
state_revision: str,
|
|
308
|
+
element_id: int,
|
|
309
|
+
text: str,
|
|
310
|
+
mode: str = "text",
|
|
311
|
+
prefix: str | None = None,
|
|
312
|
+
suffix: str | None = None,
|
|
313
|
+
*,
|
|
314
|
+
consume: bool = True,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
quartz, element = self._element(state_revision, element_id, consume=False)
|
|
317
|
+
value = self._copy_attribute(quartz, element, quartz.kAXValueAttribute)
|
|
318
|
+
if not isinstance(value, str):
|
|
319
|
+
raise ComputerUseError("The selected element has no text value.")
|
|
320
|
+
starts = []
|
|
321
|
+
offset = 0
|
|
322
|
+
while True:
|
|
323
|
+
found = value.find(text, offset)
|
|
324
|
+
if found < 0:
|
|
325
|
+
break
|
|
326
|
+
before_ok = prefix is None or value[:found].endswith(prefix)
|
|
327
|
+
after = found + len(text)
|
|
328
|
+
after_ok = suffix is None or value[after:].startswith(suffix)
|
|
329
|
+
if before_ok and after_ok:
|
|
330
|
+
starts.append(found)
|
|
331
|
+
offset = found + max(1, len(text))
|
|
332
|
+
if len(starts) != 1:
|
|
333
|
+
raise ComputerUseError(
|
|
334
|
+
f"Text selection is ambiguous: found {len(starts)} matches."
|
|
335
|
+
)
|
|
336
|
+
modes = {
|
|
337
|
+
"text": (starts[0], len(text)),
|
|
338
|
+
"cursor_before": (starts[0], 0),
|
|
339
|
+
"cursor_after": (starts[0] + len(text), 0),
|
|
340
|
+
}
|
|
341
|
+
try:
|
|
342
|
+
location, length = modes[mode]
|
|
343
|
+
except KeyError as exc:
|
|
344
|
+
raise ComputerUseError(f"Unsupported selection mode: {mode}") from exc
|
|
345
|
+
range_value = quartz.AXValueCreate(
|
|
346
|
+
quartz.kAXValueCFRangeType, (location, length)
|
|
347
|
+
)
|
|
348
|
+
if range_value is None:
|
|
349
|
+
raise ComputerUseError("Could not construct a text selection range.")
|
|
350
|
+
if consume:
|
|
351
|
+
self.require_state(state_revision, consume=True)
|
|
352
|
+
error = quartz.AXUIElementSetAttributeValue(
|
|
353
|
+
element, quartz.kAXSelectedTextRangeAttribute, range_value
|
|
354
|
+
)
|
|
355
|
+
if error != quartz.kAXErrorSuccess:
|
|
356
|
+
raise ComputerUseError(
|
|
357
|
+
f"Setting the selected text range failed with error {error}."
|
|
358
|
+
)
|
|
359
|
+
return {
|
|
360
|
+
"success": True,
|
|
361
|
+
"element_id": element_id,
|
|
362
|
+
"mode": mode,
|
|
363
|
+
"range": {"location": location, "length": length},
|
|
364
|
+
"state_invalidated": True,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
def click_pixel(
|
|
368
|
+
self,
|
|
369
|
+
state_revision: str,
|
|
370
|
+
x: float,
|
|
371
|
+
y: float,
|
|
372
|
+
button: str = "left",
|
|
373
|
+
click_count: int = 1,
|
|
374
|
+
*,
|
|
375
|
+
consume: bool = True,
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
quartz = self._require_trusted()
|
|
378
|
+
state = self.require_state(state_revision)
|
|
379
|
+
point = state.geometry.screenshot_to_quartz(x, y)
|
|
380
|
+
buttons = {
|
|
381
|
+
"left": (
|
|
382
|
+
quartz.kCGMouseButtonLeft,
|
|
383
|
+
quartz.kCGEventLeftMouseDown,
|
|
384
|
+
quartz.kCGEventLeftMouseUp,
|
|
385
|
+
),
|
|
386
|
+
"right": (
|
|
387
|
+
quartz.kCGMouseButtonRight,
|
|
388
|
+
quartz.kCGEventRightMouseDown,
|
|
389
|
+
quartz.kCGEventRightMouseUp,
|
|
390
|
+
),
|
|
391
|
+
}
|
|
392
|
+
try:
|
|
393
|
+
mouse_button, down_type, up_type = buttons[button.casefold()]
|
|
394
|
+
except KeyError as exc:
|
|
395
|
+
raise ComputerUseError(f"Unsupported mouse button: {button}") from exc
|
|
396
|
+
click_count = max(1, min(int(click_count), 3))
|
|
397
|
+
if consume:
|
|
398
|
+
self.require_state(state_revision, consume=True)
|
|
399
|
+
for index in range(click_count):
|
|
400
|
+
down = quartz.CGEventCreateMouseEvent(None, down_type, point, mouse_button)
|
|
401
|
+
up = quartz.CGEventCreateMouseEvent(None, up_type, point, mouse_button)
|
|
402
|
+
quartz.CGEventSetIntegerValueField(
|
|
403
|
+
down, quartz.kCGMouseEventClickState, index + 1
|
|
404
|
+
)
|
|
405
|
+
quartz.CGEventSetIntegerValueField(
|
|
406
|
+
up, quartz.kCGMouseEventClickState, index + 1
|
|
407
|
+
)
|
|
408
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, down)
|
|
409
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, up)
|
|
410
|
+
return {
|
|
411
|
+
"success": True,
|
|
412
|
+
"screenshot_point": {"x": x, "y": y},
|
|
413
|
+
"quartz_point": {"x": point[0], "y": point[1]},
|
|
414
|
+
"button": button,
|
|
415
|
+
"click_count": click_count,
|
|
416
|
+
"state_invalidated": True,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
def drag_pixel(
|
|
420
|
+
self,
|
|
421
|
+
state_revision: str,
|
|
422
|
+
start_x: float,
|
|
423
|
+
start_y: float,
|
|
424
|
+
end_x: float,
|
|
425
|
+
end_y: float,
|
|
426
|
+
duration: float = 0.5,
|
|
427
|
+
*,
|
|
428
|
+
consume: bool = True,
|
|
429
|
+
) -> dict[str, Any]:
|
|
430
|
+
quartz = self._require_trusted()
|
|
431
|
+
state = self.require_state(state_revision)
|
|
432
|
+
start = state.geometry.screenshot_to_quartz(start_x, start_y)
|
|
433
|
+
end = state.geometry.screenshot_to_quartz(end_x, end_y)
|
|
434
|
+
duration = max(0.05, min(float(duration), 5.0))
|
|
435
|
+
if consume:
|
|
436
|
+
self.require_state(state_revision, consume=True)
|
|
437
|
+
steps = max(2, min(120, round(duration * 60)))
|
|
438
|
+
down = quartz.CGEventCreateMouseEvent(
|
|
439
|
+
None, quartz.kCGEventLeftMouseDown, start, quartz.kCGMouseButtonLeft
|
|
440
|
+
)
|
|
441
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, down)
|
|
442
|
+
for index in range(1, steps + 1):
|
|
443
|
+
fraction = index / steps
|
|
444
|
+
point = (
|
|
445
|
+
start[0] + (end[0] - start[0]) * fraction,
|
|
446
|
+
start[1] + (end[1] - start[1]) * fraction,
|
|
447
|
+
)
|
|
448
|
+
dragged = quartz.CGEventCreateMouseEvent(
|
|
449
|
+
None,
|
|
450
|
+
quartz.kCGEventLeftMouseDragged,
|
|
451
|
+
point,
|
|
452
|
+
quartz.kCGMouseButtonLeft,
|
|
453
|
+
)
|
|
454
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, dragged)
|
|
455
|
+
time.sleep(duration / steps)
|
|
456
|
+
up = quartz.CGEventCreateMouseEvent(
|
|
457
|
+
None, quartz.kCGEventLeftMouseUp, end, quartz.kCGMouseButtonLeft
|
|
458
|
+
)
|
|
459
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, up)
|
|
460
|
+
return {
|
|
461
|
+
"success": True,
|
|
462
|
+
"start_screenshot_point": {"x": start_x, "y": start_y},
|
|
463
|
+
"end_screenshot_point": {"x": end_x, "y": end_y},
|
|
464
|
+
"duration": duration,
|
|
465
|
+
"state_invalidated": True,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
def press_key(
|
|
469
|
+
self,
|
|
470
|
+
state_revision: str,
|
|
471
|
+
key: str,
|
|
472
|
+
modifiers: list[str] | None = None,
|
|
473
|
+
*,
|
|
474
|
+
consume: bool = True,
|
|
475
|
+
) -> dict[str, Any]:
|
|
476
|
+
quartz = self._require_trusted()
|
|
477
|
+
self.require_state(state_revision)
|
|
478
|
+
normalized = key.casefold()
|
|
479
|
+
if normalized not in KEY_CODES:
|
|
480
|
+
raise ComputerUseError(f"Unsupported key: {key}")
|
|
481
|
+
flags = 0
|
|
482
|
+
flag_map = {
|
|
483
|
+
"command": quartz.kCGEventFlagMaskCommand,
|
|
484
|
+
"shift": quartz.kCGEventFlagMaskShift,
|
|
485
|
+
"option": quartz.kCGEventFlagMaskAlternate,
|
|
486
|
+
"control": quartz.kCGEventFlagMaskControl,
|
|
487
|
+
}
|
|
488
|
+
for modifier in modifiers or []:
|
|
489
|
+
try:
|
|
490
|
+
flags |= flag_map[modifier.casefold()]
|
|
491
|
+
except KeyError as exc:
|
|
492
|
+
raise ComputerUseError(f"Unsupported modifier: {modifier}") from exc
|
|
493
|
+
if consume:
|
|
494
|
+
self.require_state(state_revision, consume=True)
|
|
495
|
+
for down in (True, False):
|
|
496
|
+
event = quartz.CGEventCreateKeyboardEvent(None, KEY_CODES[normalized], down)
|
|
497
|
+
quartz.CGEventSetFlags(event, flags)
|
|
498
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, event)
|
|
499
|
+
return {"success": True, "key": key, "modifiers": modifiers or []}
|
|
500
|
+
|
|
501
|
+
def type_text(
|
|
502
|
+
self, state_revision: str, text: str, *, consume: bool = True
|
|
503
|
+
) -> dict[str, Any]:
|
|
504
|
+
quartz = self._require_trusted()
|
|
505
|
+
self.require_state(state_revision)
|
|
506
|
+
if len(text) > 10_000:
|
|
507
|
+
raise ComputerUseError("Text is limited to 10,000 characters per call.")
|
|
508
|
+
if consume:
|
|
509
|
+
self.require_state(state_revision, consume=True)
|
|
510
|
+
event_down = quartz.CGEventCreateKeyboardEvent(None, 0, True)
|
|
511
|
+
event_up = quartz.CGEventCreateKeyboardEvent(None, 0, False)
|
|
512
|
+
quartz.CGEventKeyboardSetUnicodeString(event_down, len(text), text)
|
|
513
|
+
quartz.CGEventKeyboardSetUnicodeString(event_up, len(text), text)
|
|
514
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, event_down)
|
|
515
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, event_up)
|
|
516
|
+
return {"success": True, "characters": len(text)}
|
|
517
|
+
|
|
518
|
+
def scroll_pages(
|
|
519
|
+
self,
|
|
520
|
+
state_revision: str,
|
|
521
|
+
direction: str,
|
|
522
|
+
pages: float = 1.0,
|
|
523
|
+
*,
|
|
524
|
+
consume: bool = True,
|
|
525
|
+
) -> dict[str, Any]:
|
|
526
|
+
quartz = self._require_trusted()
|
|
527
|
+
state = self.require_state(state_revision)
|
|
528
|
+
directions = {
|
|
529
|
+
"up": (1, 0),
|
|
530
|
+
"down": (-1, 0),
|
|
531
|
+
"left": (0, 1),
|
|
532
|
+
"right": (0, -1),
|
|
533
|
+
}
|
|
534
|
+
try:
|
|
535
|
+
vertical_sign, horizontal_sign = directions[direction.casefold()]
|
|
536
|
+
except KeyError as exc:
|
|
537
|
+
raise ComputerUseError(
|
|
538
|
+
f"Unsupported scroll direction: {direction}"
|
|
539
|
+
) from exc
|
|
540
|
+
pages = max(0.05, min(float(pages), 10.0))
|
|
541
|
+
vertical = round(
|
|
542
|
+
vertical_sign * pages * state.geometry.image_height_pixels * 0.85
|
|
543
|
+
)
|
|
544
|
+
horizontal = round(
|
|
545
|
+
horizontal_sign * pages * state.geometry.image_width_pixels * 0.85
|
|
546
|
+
)
|
|
547
|
+
if consume:
|
|
548
|
+
self.require_state(state_revision, consume=True)
|
|
549
|
+
event = quartz.CGEventCreateScrollWheelEvent(
|
|
550
|
+
None,
|
|
551
|
+
quartz.kCGScrollEventUnitPixel,
|
|
552
|
+
2,
|
|
553
|
+
vertical,
|
|
554
|
+
horizontal,
|
|
555
|
+
)
|
|
556
|
+
quartz.CGEventPost(quartz.kCGHIDEventTap, event)
|
|
557
|
+
return {
|
|
558
|
+
"success": True,
|
|
559
|
+
"direction": direction,
|
|
560
|
+
"pages": pages,
|
|
561
|
+
"pixel_delta": {"vertical": vertical, "horizontal": horizontal},
|
|
562
|
+
"state_invalidated": True,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
def screenshot(
|
|
566
|
+
self, path: str | None = None, app_name: str | None = None
|
|
567
|
+
) -> dict[str, Any]:
|
|
568
|
+
if not app_name:
|
|
569
|
+
raise ComputerUseError(
|
|
570
|
+
"app_name is required so screenshot capture can enforce the "
|
|
571
|
+
"application deny policy."
|
|
572
|
+
)
|
|
573
|
+
target = (
|
|
574
|
+
Path(path).expanduser()
|
|
575
|
+
if path
|
|
576
|
+
else Path(tempfile.gettempdir())
|
|
577
|
+
/ f"code-puppy-computer-use-{int(time.time() * 1000)}.png"
|
|
578
|
+
)
|
|
579
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
580
|
+
policy_store.require(self._bundle_id(app_name))
|
|
581
|
+
return capture_window(app_name, target)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
backend = MacOSBackend()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Typed ScreenCaptureKit bridge through a small cached Swift helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from code_puppy.config import CONFIG_DIR
|
|
14
|
+
|
|
15
|
+
from .backend_types import ComputerUseError
|
|
16
|
+
from .geometry import CaptureGeometry, Rect
|
|
17
|
+
|
|
18
|
+
_BUILD_LOCK = threading.Lock()
|
|
19
|
+
_SOURCE = Path(__file__).with_name("NativeCapture.swift")
|
|
20
|
+
_CACHE_DIR = Path(CONFIG_DIR) / "computer_use"
|
|
21
|
+
_EXECUTABLE = _CACHE_DIR / "native-capture"
|
|
22
|
+
_HASH_FILE = _CACHE_DIR / "native-capture.sha256"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _source_hash() -> str:
|
|
26
|
+
return hashlib.sha256(_SOURCE.read_bytes()).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ensure_helper() -> Path:
|
|
30
|
+
expected_hash = _source_hash()
|
|
31
|
+
with _BUILD_LOCK:
|
|
32
|
+
if (
|
|
33
|
+
_EXECUTABLE.is_file()
|
|
34
|
+
and os.access(_EXECUTABLE, os.X_OK)
|
|
35
|
+
and _HASH_FILE.is_file()
|
|
36
|
+
and _HASH_FILE.read_text().strip() == expected_hash
|
|
37
|
+
):
|
|
38
|
+
return _EXECUTABLE
|
|
39
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
40
|
+
command = [
|
|
41
|
+
"/usr/bin/xcrun",
|
|
42
|
+
"swiftc",
|
|
43
|
+
"-O",
|
|
44
|
+
"-parse-as-library",
|
|
45
|
+
"-framework",
|
|
46
|
+
"AppKit",
|
|
47
|
+
"-framework",
|
|
48
|
+
"ScreenCaptureKit",
|
|
49
|
+
str(_SOURCE),
|
|
50
|
+
"-o",
|
|
51
|
+
str(_EXECUTABLE),
|
|
52
|
+
]
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
command,
|
|
55
|
+
check=False,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
timeout=120,
|
|
59
|
+
)
|
|
60
|
+
if result.returncode != 0:
|
|
61
|
+
raise ComputerUseError(
|
|
62
|
+
"Failed to build the native ScreenCaptureKit helper. Install "
|
|
63
|
+
f"Xcode Command Line Tools. {result.stderr.strip()}"
|
|
64
|
+
)
|
|
65
|
+
os.chmod(_EXECUTABLE, 0o700)
|
|
66
|
+
_HASH_FILE.write_text(expected_hash)
|
|
67
|
+
os.chmod(_HASH_FILE, 0o600)
|
|
68
|
+
return _EXECUTABLE
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def capture_window(app_name: str, target: Path) -> dict[str, Any]:
|
|
72
|
+
helper = _ensure_helper()
|
|
73
|
+
target = target.expanduser().resolve()
|
|
74
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
result = subprocess.run(
|
|
76
|
+
[str(helper), app_name, str(target)],
|
|
77
|
+
check=False,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=30,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode != 0:
|
|
83
|
+
message = result.stderr.strip() or result.stdout.strip()
|
|
84
|
+
raise ComputerUseError(message or "Native window capture failed.")
|
|
85
|
+
try:
|
|
86
|
+
payload = json.loads(result.stdout)
|
|
87
|
+
except json.JSONDecodeError as exc:
|
|
88
|
+
raise ComputerUseError("Native capture returned invalid metadata.") from exc
|
|
89
|
+
bounds = payload["window_bounds_points"]
|
|
90
|
+
geometry = CaptureGeometry(
|
|
91
|
+
window_points=Rect(
|
|
92
|
+
float(bounds["x"]),
|
|
93
|
+
float(bounds["y"]),
|
|
94
|
+
float(bounds["width"]),
|
|
95
|
+
float(bounds["height"]),
|
|
96
|
+
),
|
|
97
|
+
image_width_pixels=int(payload["screenshot_size_pixels"]["width"]),
|
|
98
|
+
image_height_pixels=int(payload["screenshot_size_pixels"]["height"]),
|
|
99
|
+
backing_scale=float(payload["backing_scale"]),
|
|
100
|
+
)
|
|
101
|
+
return {
|
|
102
|
+
"success": True,
|
|
103
|
+
"path": str(target),
|
|
104
|
+
"application": str(payload["application"]),
|
|
105
|
+
"bundle_id": str(payload["bundle_id"]),
|
|
106
|
+
"pid": int(payload["pid"]),
|
|
107
|
+
"window_id": int(payload["window_id"]),
|
|
108
|
+
"window_title": str(payload["window_title"]),
|
|
109
|
+
"geometry": geometry,
|
|
110
|
+
}
|