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.
Files changed (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. 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,5 @@
1
+ """Shared backend types without platform imports."""
2
+
3
+
4
+ class ComputerUseError(RuntimeError):
5
+ """A recoverable computer-use failure."""
@@ -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
+ }