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