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,193 @@
1
+ """Register callbacks for the ``puppy_spinner`` plugin.
2
+
3
+ Resurrects the classic bouncing-puppy spinner on the persistent bottom
4
+ bar. The old Rich ``Live`` spinner died in the bottom-bar rewrite (its
5
+ compat shim in ``messaging.spinner`` forwards context text only); this
6
+ plugin animates the bar's dedicated status-PREFIX slot instead, so the
7
+ token-context info written via ``BottomBar.set_status`` is never
8
+ stomped -- two writers, two slots, one row.
9
+
10
+ Lifecycle:
11
+
12
+ * ``agent_run_start`` -- refcount +1; first active run starts a ~5 fps
13
+ asyncio ticker task (same zero-thread pattern as ``subagent_panel``).
14
+ Sub-agent runs fire these hooks too; the refcount naturally keeps the
15
+ puppy running until the LAST run finishes.
16
+ * ``agent_run_end`` -- refcount -1 (fired from ``_runtime``'s ``finally``,
17
+ so cancels/exceptions never leak a spin); at zero the ticker stops and
18
+ the prefix slot is cleared.
19
+
20
+ Headless (``-p`` / non-TTY) runs never start the ticker: the bar is
21
+ inactive and output must stay byte-identical.
22
+
23
+ Customization (``/spinner``): the tick loop reads the active spinner
24
+ from ``spinners`` each iteration, so picking a new style in the TUI
25
+ (or simply editing ``spinners.json`` -- its mtime is watched) takes
26
+ effect on the very next frame, even mid-run.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import asyncio
32
+ import logging
33
+ import threading
34
+ from typing import Optional
35
+
36
+ from code_puppy.callbacks import register_callback
37
+
38
+ from . import commands, spinners
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ _PUPPY = spinners.PUPPY # DOG FACE emoji, escape-spelled (repo emoji filter)
43
+
44
+ #: The classic kennel-bounce (the ``puppy`` entry in the spinner catalogue).
45
+ FRAMES = spinners.BUILTIN_SPINNERS[spinners.DEFAULT_SPINNER].frames
46
+
47
+ #: Sourced from the catalogue so the stock puppy's speed has exactly one
48
+ #: home; kept as a module constant so tests can monkeypatch the tempo.
49
+ _TICK_INTERVAL_S = spinners.BUILTIN_SPINNERS[spinners.DEFAULT_SPINNER].interval
50
+
51
+ _lock = threading.Lock()
52
+ _active_runs = 0
53
+ _ticker_task: Optional["asyncio.Task"] = None
54
+
55
+
56
+ def _bar():
57
+ from code_puppy.messaging.bottom_bar import get_bottom_bar
58
+
59
+ return get_bottom_bar()
60
+
61
+
62
+ # Dispatcher passes hook args positionally (up to seven for agent_run_end); this
63
+ # plugin ignores them, so swallow all args to stay signature-proof.
64
+ async def _on_run_start(*_args, **_kw):
65
+ global _active_runs
66
+ with _lock:
67
+ _active_runs += 1
68
+ _start_ticker()
69
+
70
+
71
+ async def _on_run_end(*_args, **_kw):
72
+ global _active_runs
73
+ with _lock:
74
+ _active_runs = max(0, _active_runs - 1)
75
+ last_one_out = _active_runs == 0
76
+ if last_one_out:
77
+ _stop_ticker()
78
+ _clear_prefix()
79
+
80
+
81
+ def _start_ticker() -> None:
82
+ """Start the ticker task (idempotent; no loop or no bar = no puppy)."""
83
+ global _ticker_task
84
+ try:
85
+ loop = asyncio.get_running_loop()
86
+ except RuntimeError:
87
+ return # sync/odd context: skip the animation entirely
88
+ try:
89
+ if not _bar().is_active():
90
+ return # headless / non-TTY: nothing to paint on
91
+ except Exception:
92
+ return
93
+ with _lock:
94
+ if _ticker_task is not None and not _ticker_task.done():
95
+ return
96
+ _ticker_task = loop.create_task(_tick_loop())
97
+
98
+
99
+ def _stop_ticker() -> None:
100
+ """Cancel the ticker task if it's running. Idempotent."""
101
+ global _ticker_task
102
+ with _lock:
103
+ task = _ticker_task
104
+ _ticker_task = None
105
+ if task is not None and not task.done():
106
+ task.cancel()
107
+
108
+
109
+ async def _tick_loop() -> None:
110
+ """Advance the puppy one cell per tick until no run is active.
111
+
112
+ Just the animation -- no "<puppy> is thinking..." chatter. The
113
+ status row is prime real estate; the token summary needs it more.
114
+ """
115
+ global _ticker_task
116
+ frame = 0
117
+ try:
118
+ while True:
119
+ with _lock:
120
+ if _active_runs <= 0:
121
+ break # belt-and-braces: never outlive the run
122
+ frames, interval = _current_frames_and_interval()
123
+ _paint_prefix(frames[frame % len(frames)])
124
+ frame += 1
125
+ await asyncio.sleep(interval)
126
+ except asyncio.CancelledError:
127
+ pass
128
+ finally:
129
+ _clear_prefix()
130
+ with _lock:
131
+ if _ticker_task is asyncio.current_task():
132
+ _ticker_task = None
133
+
134
+
135
+ def _current_frames_and_interval():
136
+ """The active spinner's frames + interval, re-read every tick.
137
+
138
+ The stock builtin puppy routes through the module constants so tests
139
+ (and nostalgic monkeypatchers) can tweak ``_TICK_INTERVAL_S``; any
140
+ other choice -- including a user-file override of "puppy" -- uses
141
+ the catalogue values. A broken catalogue degrades to the classic.
142
+ """
143
+ try:
144
+ active = spinners.get_active_spinner()
145
+ is_stock_puppy = (
146
+ active.name == spinners.DEFAULT_SPINNER
147
+ and active.source == "builtin"
148
+ and active.interval
149
+ == spinners.BUILTIN_SPINNERS[
150
+ spinners.DEFAULT_SPINNER
151
+ ].interval # a speed override on the puppy still counts as custom
152
+ )
153
+ if not is_stock_puppy:
154
+ return active.frames, active.interval
155
+ except Exception:
156
+ logger.debug("active spinner lookup failed", exc_info=True)
157
+ return FRAMES, _TICK_INTERVAL_S
158
+
159
+
160
+ #: Paint-time gap after every spinner frame, including user-authored frames.
161
+ _PREFIX_GAP = " "
162
+
163
+
164
+ def _paint_prefix(text: str) -> None:
165
+ """Best-effort paint -- a broken bar must never kill the ticker.
166
+
167
+ Non-empty frames get ``_PREFIX_GAP`` appended; clearing stays a
168
+ true empty string so nothing lingers in the prefix slot.
169
+ """
170
+ try:
171
+ _bar().set_status_prefix(text + _PREFIX_GAP if text else "")
172
+ except Exception:
173
+ logger.debug("puppy spinner paint failed", exc_info=True)
174
+
175
+
176
+ def _clear_prefix() -> None:
177
+ _paint_prefix("")
178
+
179
+
180
+ register_callback("agent_run_start", _on_run_start)
181
+ register_callback("agent_run_end", _on_run_end)
182
+ register_callback("custom_command", commands.handle_spinner)
183
+ register_callback("custom_command_help", commands.help_entries)
184
+
185
+
186
+ __all__ = [
187
+ "FRAMES",
188
+ "_on_run_end",
189
+ "_on_run_start",
190
+ "_start_ticker",
191
+ "_stop_ticker",
192
+ "_tick_loop",
193
+ ]
@@ -0,0 +1,444 @@
1
+ """Spinner catalogue + persistence for the ``puppy_spinner`` plugin.
2
+
3
+ Built-in spinners live here. Users add their own by dropping a
4
+ ``spinners.json`` next to ``puppy.cfg`` (see ``USER_SPINNERS_FILE``)::
5
+
6
+ {
7
+ "my-spinner": {
8
+ "frames": ["( . ) ", "( . ) ", "( . ) "],
9
+ "interval": 0.1,
10
+ "description": "a very minimalist puppy"
11
+ }
12
+ }
13
+
14
+ User spinners win on name collision (so you *can* redefine "puppy",
15
+ you monster). An entry that names a builtin and omits ``frames`` is a
16
+ *tweak*: it inherits the builtin's frames and just overrides the
17
+ interval (and/or description) -- the cheap way to re-speed a builtin
18
+ without copying its art::
19
+
20
+ {
21
+ "zoomies": {"interval": 0.1}
22
+ }
23
+
24
+ The active choice persists under the ``spinner_style`` key in
25
+ ``puppy.cfg`` and is cached in-process; ``invalidate_cache()`` forces a
26
+ re-read after external edits. Speed has one rule: a spinner runs at its
27
+ spinners.json value when present, else the builtin default. Dialing a
28
+ speed in the picker (or ``/spinner <name> <secs>``) simply *saves* that
29
+ value into spinners.json -- user land keeps its own numbers, no
30
+ shadow-override layer to reason about.
31
+
32
+ Everything here fails gracefully: a broken JSON file or a vanished
33
+ spinner name always degrades to the classic kennel-bounce puppy.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import logging
40
+ import os
41
+ from dataclasses import dataclass, replace
42
+ from typing import Dict, Optional, Tuple
43
+
44
+ from rich.cells import cell_len
45
+
46
+ from code_puppy import atomic_io, atomic_json
47
+ from code_puppy.config import CONFIG_DIR, get_value, set_value
48
+
49
+ from .builtin_frames import EXTRA_SPECS
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ CONFIG_KEY = "spinner_style"
54
+ DEFAULT_SPINNER = "puppy"
55
+ USER_SPINNERS_FILE = os.path.join(CONFIG_DIR, "spinners.json")
56
+
57
+ MIN_INTERVAL = 0.02 # 50 fps -- any faster is just terminal abuse
58
+ MAX_INTERVAL = 1.0
59
+ #: One speed to rule them all: every builtin ships at this, and it's the
60
+ #: fallback when spinners.json omits (or fumbles) an interval.
61
+ _DEFAULT_INTERVAL = 0.2
62
+ #: ...except the classic. The kennel bounce was tuned for a quicker trot
63
+ #: (0.05s originally) and looks sluggish at the pack default.
64
+ _PUPPY_INTERVAL = 0.06
65
+ _MAX_FRAME_LEN = 40 # the status-prefix slot is prime real estate
66
+
67
+ # Escape-spelled glyphs (repo emoji filter).
68
+ PUPPY = "\U0001f436" # dog face
69
+ _DOG = "\U0001f415" # dog (side view, runs left)
70
+ _DASH = "\U0001f4a8" # dust cloud
71
+ _PAW = "\U0001f43e" # paw prints
72
+ _BONE = "\U0001f9b4" # bone
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class Spinner:
77
+ """One spinner: a frame cycle, a tick interval, and provenance."""
78
+
79
+ name: str
80
+ frames: Tuple[str, ...]
81
+ interval: float
82
+ description: str = ""
83
+ source: str = "builtin" # "builtin" | "user"
84
+
85
+
86
+ def _pad(frames) -> Tuple[str, ...]:
87
+ """Right-pad every frame to a common *cell* width so text after the
88
+ spinner never jumps.
89
+
90
+ ``len()`` is a dirty liar for emoji: a paw print is one code point
91
+ but TWO terminal cells. Padding by ``rich.cells.cell_len`` keeps the
92
+ rendered width constant even when frames mix emoji and ASCII.
93
+ """
94
+ frames = tuple(frames) # materialize: generators only pour out once
95
+ width = max(cell_len(f) for f in frames)
96
+ return tuple(f + " " * (width - cell_len(f)) for f in frames)
97
+
98
+
99
+ def _kennel_bounce(critter: str) -> Tuple[str, ...]:
100
+ """The classic 5-cell kennel bounce: positions 0..4 and back again."""
101
+ return tuple(
102
+ f"({' ' * i}{critter}{' ' * (4 - i)}) " for i in (0, 1, 2, 3, 4, 3, 2, 1)
103
+ )
104
+
105
+
106
+ # name -> (frames, interval, description)
107
+ _BUILTIN_SPECS = {
108
+ "puppy": (
109
+ _kennel_bounce(PUPPY),
110
+ _PUPPY_INTERVAL,
111
+ "the classic kennel bounce (default)",
112
+ ),
113
+ "bone": (
114
+ _kennel_bounce(_BONE),
115
+ _DEFAULT_INTERVAL,
116
+ "same kennel, chewier occupant",
117
+ ),
118
+ "zoomies": (
119
+ tuple(f"{' ' * i}{_DOG}{_DASH}{' ' * (5 - i)}" for i in (5, 4, 3, 2, 1, 0)),
120
+ _DEFAULT_INTERVAL,
121
+ "full-speed dog, dust trailing",
122
+ ),
123
+ "paws": (
124
+ tuple(_PAW * n for n in (1, 2, 3, 4)), # _pad cell-aligns the trail
125
+ _DEFAULT_INTERVAL,
126
+ "a trail of paw prints",
127
+ ),
128
+ "dots": (
129
+ tuple(
130
+ chr(cp)
131
+ for cp in (
132
+ 0x280B,
133
+ 0x2819,
134
+ 0x2839,
135
+ 0x2838,
136
+ 0x283C,
137
+ 0x2834,
138
+ 0x2826,
139
+ 0x2827,
140
+ 0x2807,
141
+ 0x280F,
142
+ )
143
+ ),
144
+ _DEFAULT_INTERVAL,
145
+ "classic braille dots",
146
+ ),
147
+ }
148
+
149
+ # The cli-spinners-style pack (frames + blurbs live in ``builtin_frames``,
150
+ # a generated data-only module). Same default speed as everything else.
151
+ _BUILTIN_SPECS.update(
152
+ {
153
+ name: (frames, _DEFAULT_INTERVAL, desc)
154
+ for name, (frames, desc) in EXTRA_SPECS.items()
155
+ }
156
+ )
157
+
158
+ BUILTIN_SPINNERS: Dict[str, Spinner] = {
159
+ name: Spinner(name=name, frames=_pad(frames), interval=interval, description=desc)
160
+ for name, (frames, interval, desc) in _BUILTIN_SPECS.items()
161
+ }
162
+
163
+
164
+ # --- User spinners (external JSON file) --------------------------------------
165
+ def _parse_interval(name: str, spec: dict, default: float) -> float:
166
+ """Validate + clamp a spec's 'interval', falling back to *default*."""
167
+ interval = spec.get("interval", default)
168
+ # bool check first: JSON `true` is an int subclass, and float(True)
169
+ # is a surprise 1-second spinner. No booleans, thanks.
170
+ if (
171
+ isinstance(interval, bool)
172
+ or not isinstance(interval, (int, float))
173
+ or interval <= 0
174
+ ):
175
+ logger.warning(
176
+ "spinners.json: %r has a bad 'interval'; using %s", name, default
177
+ )
178
+ interval = default
179
+ return clamp_interval(float(interval))
180
+
181
+
182
+ def _parse_builtin_tweak(name: str, spec: dict) -> Optional[Spinner]:
183
+ """A frameless entry naming a builtin: inherit its frames, override
184
+ the rest. The cheap way to re-speed a builtin without copying art.
185
+ """
186
+ if "interval" not in spec and "description" not in spec:
187
+ logger.warning(
188
+ "spinners.json: %r tweaks builtin %r but overrides nothing "
189
+ "(add 'interval' or 'description'); skipping",
190
+ name,
191
+ name,
192
+ )
193
+ return None
194
+ base = BUILTIN_SPINNERS[name]
195
+ description = spec.get("description", base.description)
196
+ if not isinstance(description, str):
197
+ description = base.description
198
+ return Spinner(
199
+ name=name,
200
+ frames=base.frames,
201
+ interval=_parse_interval(name, spec, base.interval),
202
+ description=description,
203
+ source="builtin+user",
204
+ )
205
+
206
+
207
+ def _parse_user_spinner(name: str, spec: object) -> Optional[Spinner]:
208
+ """Validate one user entry. Returns None (and logs) on any problem."""
209
+ if not isinstance(name, str) or not name.strip():
210
+ logger.warning("spinners.json: skipping entry with a blank name")
211
+ return None
212
+ if not isinstance(spec, dict):
213
+ logger.warning("spinners.json: %r is not an object; skipping", name)
214
+ return None
215
+
216
+ raw_frames = spec.get("frames")
217
+ if raw_frames is None and name in BUILTIN_SPINNERS:
218
+ return _parse_builtin_tweak(name, spec)
219
+ if (
220
+ not isinstance(raw_frames, list)
221
+ or not raw_frames
222
+ or not all(isinstance(f, str) and f for f in raw_frames)
223
+ ):
224
+ logger.warning(
225
+ "spinners.json: %r needs a non-empty 'frames' list of strings; skipping",
226
+ name,
227
+ )
228
+ return None
229
+ frames = _pad(f[:_MAX_FRAME_LEN] for f in raw_frames)
230
+
231
+ description = spec.get("description", "")
232
+ if not isinstance(description, str):
233
+ description = ""
234
+
235
+ return Spinner(
236
+ name=name.strip(),
237
+ frames=frames,
238
+ interval=_parse_interval(name, spec, _DEFAULT_INTERVAL),
239
+ description=description,
240
+ source="user",
241
+ )
242
+
243
+
244
+ class _SpinnersNotADict(Exception):
245
+ """Sentinel: spinners.json parsed to something other than a dict."""
246
+
247
+
248
+ def load_user_spinners() -> Dict[str, Spinner]:
249
+ """Read + validate the user spinner file. Missing/broken file = {}.
250
+
251
+ Bounded and lock-aware via :mod:`code_puppy.atomic_json` -- a large or
252
+ concurrently-being-written file can no longer balloon memory or read a
253
+ torn write.
254
+ """
255
+ try:
256
+ data = atomic_json.load_json(USER_SPINNERS_FILE, default={})
257
+ except (OSError, atomic_json.JsonFileCorrupt) as exc:
258
+ logger.warning("Could not read %s: %s", USER_SPINNERS_FILE, exc)
259
+ return {}
260
+ if not isinstance(data, dict):
261
+ logger.warning("%s must be a JSON object; ignoring", USER_SPINNERS_FILE)
262
+ return {}
263
+
264
+ spinners: Dict[str, Spinner] = {}
265
+ for name, spec in data.items():
266
+ spinner = _parse_user_spinner(name, spec)
267
+ if spinner is not None:
268
+ spinners[spinner.name] = spinner
269
+ return spinners
270
+
271
+
272
+ def get_catalogue() -> Dict[str, Spinner]:
273
+ """Builtin + user spinners, user winning on name collision.
274
+
275
+ Sorted alphabetically (case-insensitive) -- every consumer iterates
276
+ this, so ordering lives here once.
277
+ """
278
+ catalogue = dict(BUILTIN_SPINNERS)
279
+ for name, spinner in load_user_spinners().items():
280
+ if name in catalogue:
281
+ logger.info("user spinner %r overrides the builtin one", name)
282
+ catalogue[name] = spinner
283
+ return dict(sorted(catalogue.items(), key=lambda kv: kv[0].lower()))
284
+
285
+
286
+ # --- Saving a speed back into user land ---------------------------------------
287
+ def clamp_interval(value: float) -> float:
288
+ """Clamp *value* into the sane range and round to whole centiseconds."""
289
+ return round(min(max(value, MIN_INTERVAL), MAX_INTERVAL), 2)
290
+
291
+
292
+ def save_interval_tweak(name: str, interval: float) -> bool:
293
+ """Persist *interval* for *name* into spinners.json (creating the
294
+ file if needed).
295
+
296
+ In user land the file *is* the truth: a dialed speed is just a saved
297
+ value, not a separate override layer. Existing entry keys (frames,
298
+ description) are preserved; only ``interval`` changes.
299
+
300
+ Returns False (and logs) when the file can't be read or written --
301
+ we never overwrite a file we couldn't parse. Uses
302
+ :func:`code_puppy.atomic_json.mutate_json` for a bounded, locked,
303
+ atomically-written read-modify-write.
304
+ """
305
+ interval = clamp_interval(interval)
306
+
307
+ def _mutate(data):
308
+ if not isinstance(data, dict):
309
+ raise _SpinnersNotADict()
310
+ entry = data.get(name)
311
+ if not isinstance(entry, dict):
312
+ entry = {}
313
+ entry["interval"] = interval
314
+ data[name] = entry
315
+ return data
316
+
317
+ try:
318
+ atomic_json.mutate_json(USER_SPINNERS_FILE, _mutate, default={})
319
+ except _SpinnersNotADict:
320
+ logger.warning("Not saving speed: %s is not a JSON object", USER_SPINNERS_FILE)
321
+ return False
322
+ except (OSError, atomic_json.JsonFileCorrupt) as exc:
323
+ logger.warning("Could not save speed to %s: %s", USER_SPINNERS_FILE, exc)
324
+ return False
325
+ return True
326
+
327
+
328
+ # Active spinner cache avoids parsing config/JSON every tick and invalidates on
329
+ # spinners.json mtime changes, so external edits apply without a reload.
330
+ _active_cache: Optional[Spinner] = None
331
+ _active_cache_stamp: Optional[float] = None
332
+
333
+
334
+ def user_file_stamp() -> Optional[float]:
335
+ """spinners.json's mtime, or None when the file doesn't exist.
336
+
337
+ Public because the picker watches it too: both the tick loop and the
338
+ open TUI use the same signal to notice external edits.
339
+ """
340
+ try:
341
+ return os.path.getmtime(USER_SPINNERS_FILE)
342
+ except OSError:
343
+ return None
344
+
345
+
346
+ def _load_active() -> Spinner:
347
+ try:
348
+ name = get_value(CONFIG_KEY)
349
+ except Exception: # config layer must never break the spinner
350
+ name = None
351
+ name = (name or DEFAULT_SPINNER).strip()
352
+ catalogue = get_catalogue()
353
+ if name not in catalogue:
354
+ if name != DEFAULT_SPINNER:
355
+ logger.warning(
356
+ "Configured spinner %r not found; falling back to %r",
357
+ name,
358
+ DEFAULT_SPINNER,
359
+ )
360
+ spinner = BUILTIN_SPINNERS[DEFAULT_SPINNER]
361
+ else:
362
+ spinner = catalogue[name]
363
+ return spinner
364
+
365
+
366
+ def get_active_spinner() -> Spinner:
367
+ """The currently selected spinner.
368
+
369
+ Cached, but re-read whenever spinners.json changes on disk (an
370
+ os.stat per call -- cheap enough for the ~5 Hz tick loop).
371
+ """
372
+ global _active_cache, _active_cache_stamp
373
+ stamp = user_file_stamp()
374
+ if _active_cache is None or stamp != _active_cache_stamp:
375
+ _active_cache = _load_active()
376
+ _active_cache_stamp = stamp
377
+ return _active_cache
378
+
379
+
380
+ def set_active(name: str, interval: Optional[float] = None) -> Spinner:
381
+ """Persist + activate *name*, optionally saving a custom speed.
382
+
383
+ A given *interval* is written straight into spinners.json as that
384
+ spinner's speed -- the file is the single source of truth, so the
385
+ value sticks until the user changes it again (there is no separate
386
+ override to clear). Raises KeyError for unknown spinners.
387
+ """
388
+ global _active_cache, _active_cache_stamp
389
+ catalogue = get_catalogue()
390
+ if name not in catalogue:
391
+ raise KeyError(name)
392
+ set_value(CONFIG_KEY, name)
393
+ spinner = catalogue[name]
394
+ if interval is not None:
395
+ interval = clamp_interval(interval)
396
+ if save_interval_tweak(name, interval):
397
+ spinner = get_catalogue()[name] # re-read: the file is the truth
398
+ else:
399
+ # Couldn't write (broken/readonly file): honor the speed for
400
+ # this session rather than silently dropping the request.
401
+ spinner = replace(spinner, interval=interval)
402
+ _active_cache = spinner
403
+ _active_cache_stamp = user_file_stamp()
404
+ return _active_cache
405
+
406
+
407
+ def invalidate_cache() -> None:
408
+ """Drop the cache so the next read re-checks config + spinners.json.
409
+
410
+ Rarely needed -- file edits are caught automatically via mtime; this
411
+ covers out-of-band ``puppy.cfg`` changes (and test isolation).
412
+ """
413
+ global _active_cache, _active_cache_stamp
414
+ _active_cache = None
415
+ _active_cache_stamp = None
416
+
417
+
418
+ # --- Starter template for /spinner init --------------------------------------
419
+ _TEMPLATE = {
420
+ "sniffer": {
421
+ "frames": ["( . ) ", "( . ) ", "( . ) ", "( . ) "],
422
+ "interval": 0.1,
423
+ "description": "a very minimalist puppy",
424
+ },
425
+ # A frameless entry naming a builtin just re-speeds it (this example
426
+ # matches the builtin default, so it's a safe demo -- edit away).
427
+ "zoomies": {"interval": 0.2},
428
+ }
429
+
430
+
431
+ def write_template() -> bool:
432
+ """Write a starter spinners.json. Returns False if one already exists.
433
+
434
+ Locked so two concurrent first-runs (e.g. two terminals) can't both
435
+ pass the existence check and race each other's write.
436
+ """
437
+ with atomic_io.path_lock(USER_SPINNERS_FILE):
438
+ if os.path.isfile(USER_SPINNERS_FILE):
439
+ return False
440
+ atomic_io.atomic_write_bytes(
441
+ USER_SPINNERS_FILE,
442
+ (json.dumps(_TEMPLATE, indent=2) + "\n").encode("utf-8"),
443
+ )
444
+ return True
File without changes