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,206 @@
1
+ """Slash commands for humans interacting with the kennel.
2
+
3
+ Sub-commands under ``/kennel``:
4
+
5
+ * ``/kennel`` — Show stats and recent activity
6
+ * ``/kennel search <query>`` — FTS5 search the current recall scope
7
+ * ``/kennel wings`` — List all wings and drawer counts
8
+ * ``/kennel stats`` — Storage stats and totals
9
+ * ``/kennel help`` — Usage hint
10
+
11
+ All commands return ``True`` to mark them handled (per callback contract)
12
+ or ``None`` to let other plugins try. ``False`` is never returned because
13
+ we own the ``/kennel`` prefix unconditionally.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
21
+
22
+ from . import kennel
23
+ from .config import DB_PATH
24
+ from .state import is_enabled, set_enabled
25
+ from .wings import default_recall_scope, detect_cwd
26
+
27
+
28
+ def _reload_current_agent() -> None:
29
+ """Rebuild the active agent so its tool list reflects the new kennel state.
30
+
31
+ Toggling memory on/off changes what ``register_agent_tools`` advertises,
32
+ but the live agent has already baked its tools in at construction time.
33
+ Without a reload, ``/kennel disable`` would leave the kennel tools
34
+ dangling on the agent (and ``/kennel enable`` wouldn't add them back)
35
+ until the next natural reload. Fail soft — toggling persisted fine
36
+ even if the reload trips.
37
+ """
38
+ try:
39
+ from code_puppy.agents.agent_manager import get_current_agent
40
+
41
+ get_current_agent().reload_code_generation_agent()
42
+ except Exception as exc: # noqa: BLE001
43
+ emit_error(f"Could not reload agent after kennel toggle: {exc!r}")
44
+
45
+
46
+ _COMMAND = "kennel"
47
+ _HELP_LINES: tuple[tuple[str, str], ...] = (
48
+ ("kennel", "Puppy Kennel — local memory: search, stats, wings"),
49
+ )
50
+
51
+
52
+ def _parse(command: str) -> tuple[str, str]:
53
+ """Split ``/kennel <sub> <rest>`` into ``(sub, rest)``."""
54
+ body = command.lstrip("/").strip()
55
+ if body.startswith(_COMMAND):
56
+ body = body[len(_COMMAND) :].strip()
57
+ parts = body.split(None, 1)
58
+ sub = parts[0].lower() if parts else ""
59
+ rest = parts[1] if len(parts) > 1 else ""
60
+ return sub, rest
61
+
62
+
63
+ def _humanize_bytes(n: int) -> str:
64
+ for unit in ("B", "KB", "MB", "GB"):
65
+ if n < 1024:
66
+ return f"{n:.1f} {unit}"
67
+ n /= 1024 # type: ignore[assignment]
68
+ return f"{n:.1f} TB"
69
+
70
+
71
+ def _cmd_stats() -> bool:
72
+ total = kennel.count_drawers()
73
+ wings = kennel.list_wings()
74
+ db_size = DB_PATH.stat().st_size if DB_PATH.exists() else 0
75
+ state = "enabled" if is_enabled() else "DISABLED"
76
+ emit_info(f"Puppy Kennel at `{DB_PATH}`")
77
+ emit_info(f" state : {state}")
78
+ emit_info(f" drawers : {total}")
79
+ emit_info(f" wings : {len(wings)}")
80
+ emit_info(f" on disk : {_humanize_bytes(db_size)}")
81
+ return True
82
+
83
+
84
+ def _cmd_status() -> bool:
85
+ if is_enabled():
86
+ emit_success("Puppy Kennel memory is ENABLED.")
87
+ else:
88
+ emit_warning(
89
+ "Puppy Kennel memory is DISABLED. Run /kennel enable to turn it on."
90
+ )
91
+ return True
92
+
93
+
94
+ def _cmd_enable() -> bool:
95
+ if is_enabled():
96
+ emit_info("Puppy Kennel memory is already enabled.")
97
+ return True
98
+ try:
99
+ set_enabled(True)
100
+ except Exception as exc: # noqa: BLE001
101
+ emit_warning(f"Could not persist enabled state: {exc!r}")
102
+ return True
103
+ _reload_current_agent()
104
+ emit_success("Puppy Kennel memory ENABLED. New runs will be recorded and recalled.")
105
+ return True
106
+
107
+
108
+ def _cmd_disable() -> bool:
109
+ if not is_enabled():
110
+ emit_info("Puppy Kennel memory is already disabled.")
111
+ return True
112
+ try:
113
+ set_enabled(False)
114
+ except Exception as exc: # noqa: BLE001
115
+ emit_warning(f"Could not persist disabled state: {exc!r}")
116
+ return True
117
+ _reload_current_agent()
118
+ emit_success(
119
+ "Puppy Kennel memory DISABLED. Existing drawers remain on disk; "
120
+ "recording and recall are paused. Run /kennel enable to resume."
121
+ )
122
+ return True
123
+
124
+
125
+ def _cmd_wings() -> bool:
126
+ wings = kennel.list_wings()
127
+ if not wings:
128
+ emit_warning("No wings in the kennel yet.")
129
+ return True
130
+ emit_info(f"{len(wings)} wing(s):")
131
+ for w in wings:
132
+ n = kennel.count_drawers(wing_name=w)
133
+ emit_info(f" {w} ({n} drawer{'s' if n != 1 else ''})")
134
+ return True
135
+
136
+
137
+ def _cmd_search(query: str) -> bool:
138
+ if not query.strip():
139
+ emit_warning("Usage: /kennel search <your query>")
140
+ return True
141
+ wings = default_recall_scope("code-puppy", detect_cwd())
142
+ hits = kennel.search_drawers_multi(query, wing_names=wings, limit=5)
143
+ if not hits:
144
+ emit_warning(f"No hits for '{query}' in scope {wings}")
145
+ return True
146
+ emit_success(f"{len(hits)} hit(s) for '{query}':")
147
+ for d in hits:
148
+ agent = (d.metadata or {}).get("agent", "?")
149
+ preview = d.content[:200].replace("\n", " ")
150
+ emit_info(f" [{d.ts}] {agent}: {preview}{'…' if len(d.content) > 200 else ''}")
151
+ return True
152
+
153
+
154
+ def _cmd_help() -> bool:
155
+ emit_info("Puppy Kennel commands:")
156
+ emit_info(" /kennel - stats + recent activity")
157
+ emit_info(" /kennel search <query> - FTS5 search across default scope")
158
+ emit_info(" /kennel wings - list wings with drawer counts")
159
+ emit_info(" /kennel stats - storage stats + enabled state")
160
+ emit_info(" /kennel status - is memory enabled?")
161
+ emit_info(" /kennel enable - turn memory on")
162
+ emit_info(" /kennel disable - turn memory off (drawers preserved)")
163
+ emit_info(" /kennel help - this message")
164
+ return True
165
+
166
+
167
+ def _cmd_default_overview() -> bool:
168
+ """Bare ``/kennel`` — show stats + a tiny preview of the recent block."""
169
+ _cmd_stats()
170
+ from .retriever import build_recall_block
171
+
172
+ block = build_recall_block()
173
+ if block:
174
+ emit_info("")
175
+ emit_info(block)
176
+ return True
177
+
178
+
179
+ def handle(command: str, name: str) -> Any:
180
+ """Dispatch ``/kennel`` and aliases. Returns ``None`` for non-kennel cmds."""
181
+ if name != _COMMAND:
182
+ return None
183
+ sub, rest = _parse(command)
184
+ if not sub:
185
+ return _cmd_default_overview()
186
+ if sub == "search":
187
+ return _cmd_search(rest)
188
+ if sub == "wings":
189
+ return _cmd_wings()
190
+ if sub == "stats":
191
+ return _cmd_stats()
192
+ if sub == "status":
193
+ return _cmd_status()
194
+ if sub in ("enable", "on"):
195
+ return _cmd_enable()
196
+ if sub in ("disable", "off"):
197
+ return _cmd_disable()
198
+ if sub in ("help", "?"):
199
+ return _cmd_help()
200
+ emit_warning(f"Unknown /kennel subcommand: '{sub}'")
201
+ return _cmd_help()
202
+
203
+
204
+ def help_entries() -> list[tuple[str, str]]:
205
+ """``custom_command_help`` callback — list ``/kennel`` in /help."""
206
+ return list(_HELP_LINES)
@@ -0,0 +1,36 @@
1
+ """Configuration for the puppy_kennel plugin.
2
+
3
+ Single source of truth for paths, defaults, and tunable knobs. The
4
+ on/off toggle lives in ``state.py`` and is persisted in ``puppy.cfg``
5
+ under ``kennel_enabled`` -- see that module.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ # Root directory for the kennel on disk.
14
+ KENNEL_ROOT = Path(
15
+ os.environ.get("PUPPY_KENNEL_ROOT", Path.home() / ".code_puppy" / "kennel")
16
+ )
17
+
18
+ # The SQLite database file lives inside the kennel root.
19
+ DB_PATH = KENNEL_ROOT / "kennel.db"
20
+
21
+ # Prompt-packing budget for the recall block; estimate tokens at ~4 chars each
22
+ # to avoid a tokenizer dependency.
23
+ PROMPT_BUDGET_TOKENS = int(os.environ.get("PUPPY_KENNEL_PROMPT_BUDGET", "1500"))
24
+ CHARS_PER_TOKEN = 4
25
+ PROMPT_BUDGET_CHARS = PROMPT_BUDGET_TOKENS * CHARS_PER_TOKEN
26
+
27
+ # Per-class quotas. The remainder goes to recent context (P2).
28
+ USER_PREFS_QUOTA = float(os.environ.get("PUPPY_KENNEL_USER_PREFS_QUOTA", "0.30"))
29
+ STICKY_QUOTA = float(os.environ.get("PUPPY_KENNEL_STICKY_QUOTA", "0.30"))
30
+
31
+ # Drawers shorter than this are noise — skip them in the recall block.
32
+ MIN_DRAWER_CHARS = int(os.environ.get("PUPPY_KENNEL_MIN_DRAWER_CHARS", "80"))
33
+
34
+ # Cap on stored drawer text length (chars). Keeps SQLite happy and FTS indexes
35
+ # from getting comically large. Truncation is fine — verbatim within reason.
36
+ MAX_DRAWER_CHARS = int(os.environ.get("PUPPY_KENNEL_MAX_DRAWER_CHARS", "32000"))
@@ -0,0 +1,368 @@
1
+ """The Kennel — storage layer for puppy_kennel.
2
+
3
+ A thin wrapper around SQLite that exposes the wing/room/drawer vocabulary
4
+ without leaking SQL all over the rest of the plugin. Connections are
5
+ short-lived and use a per-call ``with sqlite3.connect(...)`` block so we
6
+ never accidentally hold the write lock across awaits.
7
+
8
+ WAL mode means many readers + one writer is safe across processes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sqlite3
15
+ from contextlib import contextmanager
16
+ from dataclasses import dataclass
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any, Iterator
20
+
21
+ from .config import DB_PATH, MAX_DRAWER_CHARS, KENNEL_ROOT
22
+ from .schema import PRAGMAS, SCHEMA_SQL
23
+
24
+
25
+ @dataclass(slots=True, frozen=True)
26
+ class Drawer:
27
+ """A verbatim chunk of remembered content."""
28
+
29
+ id: int
30
+ room_id: int
31
+ role: str | None
32
+ content: str
33
+ ts: str
34
+ session_id: str | None
35
+ metadata: dict[str, Any] | None
36
+
37
+
38
+ def _now_iso() -> str:
39
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
40
+
41
+
42
+ def _ensure_root() -> None:
43
+ KENNEL_ROOT.mkdir(parents=True, exist_ok=True)
44
+
45
+
46
+ @contextmanager
47
+ def _connect(path: Path = DB_PATH) -> Iterator[sqlite3.Connection]:
48
+ """Yield a SQLite connection with the kennel PRAGMAs applied.
49
+
50
+ Short-lived by design: open, do work, close. WAL handles concurrency.
51
+ """
52
+ _ensure_root()
53
+ conn = sqlite3.connect(path, timeout=10.0, isolation_level=None)
54
+ try:
55
+ conn.row_factory = sqlite3.Row
56
+ for pragma in PRAGMAS:
57
+ conn.execute(pragma)
58
+ yield conn
59
+ finally:
60
+ conn.close()
61
+
62
+
63
+ def initialize() -> None:
64
+ """Create tables, indexes, and FTS triggers if they don't exist.
65
+
66
+ Idempotent. Safe to call on every plugin load.
67
+ """
68
+ with _connect() as conn:
69
+ conn.executescript(SCHEMA_SQL)
70
+
71
+
72
+ # Wings and rooms (simple upserts)
73
+
74
+
75
+ def ensure_wing(name: str, metadata: dict[str, Any] | None = None) -> int:
76
+ """Return the id of the wing named ``name``, creating it if missing.
77
+
78
+ Uses ``INSERT OR IGNORE`` to dodge the SELECT-then-INSERT TOCTOU race
79
+ between concurrent processes. The single INSERT is atomic; if another
80
+ writer beat us to it the row already exists and we just re-SELECT.
81
+ """
82
+ payload = json.dumps(metadata) if metadata else None
83
+ with _connect() as conn:
84
+ conn.execute(
85
+ "INSERT OR IGNORE INTO wings(name, created_at, metadata) VALUES (?, ?, ?)",
86
+ (name, _now_iso(), payload),
87
+ )
88
+ row = conn.execute("SELECT id FROM wings WHERE name = ?", (name,)).fetchone()
89
+ return int(row["id"])
90
+
91
+
92
+ def ensure_room(wing_id: int, name: str, metadata: dict[str, Any] | None = None) -> int:
93
+ """Return the id of the room (wing_id, name), creating it if missing.
94
+
95
+ Same race-free pattern as ``ensure_wing``.
96
+ """
97
+ payload = json.dumps(metadata) if metadata else None
98
+ with _connect() as conn:
99
+ conn.execute(
100
+ "INSERT OR IGNORE INTO rooms(wing_id, name, created_at, metadata) "
101
+ "VALUES (?, ?, ?, ?)",
102
+ (wing_id, name, _now_iso(), payload),
103
+ )
104
+ row = conn.execute(
105
+ "SELECT id FROM rooms WHERE wing_id = ? AND name = ?",
106
+ (wing_id, name),
107
+ ).fetchone()
108
+ return int(row["id"])
109
+
110
+
111
+ # Drawers — remembered content
112
+
113
+
114
+ def add_drawer(
115
+ room_id: int,
116
+ content: str,
117
+ role: str | None = None,
118
+ session_id: str | None = None,
119
+ metadata: dict[str, Any] | None = None,
120
+ ) -> int:
121
+ """Append a verbatim drawer to a room. Returns the new drawer id."""
122
+ if not content or not content.strip():
123
+ return 0
124
+ if len(content) > MAX_DRAWER_CHARS:
125
+ content = content[:MAX_DRAWER_CHARS] + "\n...[truncated]"
126
+ payload = json.dumps(metadata) if metadata else None
127
+ with _connect() as conn:
128
+ cur = conn.execute(
129
+ "INSERT INTO drawers(room_id, role, content, ts, session_id, metadata) "
130
+ "VALUES (?, ?, ?, ?, ?, ?)",
131
+ (room_id, role, content, _now_iso(), session_id, payload),
132
+ )
133
+ return int(cur.lastrowid)
134
+
135
+
136
+ def _row_to_drawer(row: sqlite3.Row) -> Drawer:
137
+ meta_raw = row["metadata"] if "metadata" in row.keys() else None
138
+ meta = json.loads(meta_raw) if meta_raw else None
139
+ return Drawer(
140
+ id=int(row["id"]),
141
+ room_id=int(row["room_id"]),
142
+ role=row["role"],
143
+ content=row["content"],
144
+ ts=row["ts"],
145
+ session_id=row["session_id"],
146
+ metadata=meta,
147
+ )
148
+
149
+
150
+ def recent_drawers(
151
+ wing_name: str, limit: int = 5, role: str | None = None
152
+ ) -> list[Drawer]:
153
+ """Return the most recent drawers in a wing, optionally role-filtered.
154
+
155
+ ``role=None`` returns every role (assistant + note + ...). Pass a
156
+ specific value like ``"note"`` to get just sticky writes from
157
+ ``kennel_remember``.
158
+ """
159
+ sql_parts = [
160
+ "SELECT d.* FROM drawers d",
161
+ "JOIN rooms r ON r.id = d.room_id",
162
+ "JOIN wings w ON w.id = r.wing_id",
163
+ "WHERE w.name = ?",
164
+ ]
165
+ params: list[Any] = [wing_name]
166
+ if role is not None:
167
+ sql_parts.append("AND d.role = ?")
168
+ params.append(role)
169
+ sql_parts.append("ORDER BY d.ts DESC LIMIT ?")
170
+ params.append(limit)
171
+ with _connect() as conn:
172
+ rows = conn.execute(" ".join(sql_parts), params).fetchall()
173
+ return [_row_to_drawer(r) for r in rows]
174
+
175
+
176
+ def recent_drawers_multi(
177
+ wing_names: list[str] | None = None,
178
+ limit: int = 5,
179
+ ) -> list[Drawer]:
180
+ """Recent drawers across multiple wings, deduplicated by content.
181
+
182
+ Same dual-write workaround as ``search_drawers_multi``: over-fetch then
183
+ dedupe by content, keeping the most recent copy of each unique drawer.
184
+ """
185
+ sql_parts = [
186
+ "SELECT d.* FROM drawers d",
187
+ "JOIN rooms r ON r.id = d.room_id",
188
+ "JOIN wings w ON w.id = r.wing_id",
189
+ ]
190
+ params: list[Any] = []
191
+ if wing_names:
192
+ placeholders = ",".join("?" * len(wing_names))
193
+ sql_parts.append(f"WHERE w.name IN ({placeholders})")
194
+ params.extend(wing_names)
195
+ sql_parts.append("ORDER BY d.ts DESC LIMIT ?")
196
+ params.append(max(limit * 4, limit))
197
+
198
+ with _connect() as conn:
199
+ rows = conn.execute(" ".join(sql_parts), params).fetchall()
200
+
201
+ seen: set[str] = set()
202
+ out: list[Drawer] = []
203
+ for row in rows:
204
+ drawer = _row_to_drawer(row)
205
+ if drawer.content in seen:
206
+ continue
207
+ seen.add(drawer.content)
208
+ out.append(drawer)
209
+ if len(out) >= limit:
210
+ break
211
+ return out
212
+
213
+
214
+ def write_note(
215
+ wing_name: str,
216
+ room_name: str,
217
+ content: str,
218
+ role: str = "note",
219
+ session_id: str | None = None,
220
+ metadata: dict[str, Any] | None = None,
221
+ ) -> int:
222
+ """High-level helper: ensure (wing, room) exist, then add a drawer.
223
+
224
+ Returns the new drawer id, or 0 if the content was blank.
225
+ """
226
+ wing_id = ensure_wing(wing_name)
227
+ room_id = ensure_room(wing_id, room_name)
228
+ return add_drawer(
229
+ room_id=room_id,
230
+ content=content,
231
+ role=role,
232
+ session_id=session_id,
233
+ metadata=metadata,
234
+ )
235
+
236
+
237
+ def wings_with_counts() -> list[tuple[str, int]]:
238
+ """List every wing with its drawer count. Sorted by name."""
239
+ with _connect() as conn:
240
+ rows = conn.execute(
241
+ """
242
+ SELECT w.name AS name, COUNT(d.id) AS n
243
+ FROM wings w
244
+ LEFT JOIN rooms r ON r.wing_id = w.id
245
+ LEFT JOIN drawers d ON d.room_id = r.id
246
+ GROUP BY w.id, w.name
247
+ ORDER BY w.name
248
+ """
249
+ ).fetchall()
250
+ return [(r["name"], int(r["n"])) for r in rows]
251
+
252
+
253
+ def search_drawers(
254
+ query: str,
255
+ wing_name: str | None = None,
256
+ limit: int = 5,
257
+ ) -> list[Drawer]:
258
+ """FTS5 BM25 search across drawers, optionally scoped to a single wing."""
259
+ if not query or not query.strip():
260
+ return []
261
+ fts_query = _sanitize_fts_query(query)
262
+ if not fts_query:
263
+ return []
264
+
265
+ sql_parts = [
266
+ "SELECT d.* FROM drawers d",
267
+ "JOIN drawers_fts f ON f.rowid = d.id",
268
+ "JOIN rooms r ON r.id = d.room_id",
269
+ "JOIN wings w ON w.id = r.wing_id",
270
+ "WHERE drawers_fts MATCH ?",
271
+ ]
272
+ params: list[Any] = [fts_query]
273
+ if wing_name:
274
+ sql_parts.append("AND w.name = ?")
275
+ params.append(wing_name)
276
+ sql_parts.append("ORDER BY bm25(drawers_fts) ASC LIMIT ?")
277
+ params.append(limit)
278
+
279
+ with _connect() as conn:
280
+ rows = conn.execute(" ".join(sql_parts), params).fetchall()
281
+ return [_row_to_drawer(r) for r in rows]
282
+
283
+
284
+ def search_drawers_multi(
285
+ query: str,
286
+ wing_names: list[str] | None = None,
287
+ limit: int = 5,
288
+ ) -> list[Drawer]:
289
+ """Search multiple wings and dedupe by content.
290
+
291
+ Because the recorder currently dual-writes every response to the repo
292
+ wing AND the agent wing, naive multi-wing search returns the same text
293
+ twice. We over-fetch, then dedupe by content keeping the best-scoring
294
+ drawer per unique text. This is a pragmatic workaround for the schema
295
+ duplication noted in the README; remove once the schema is normalised.
296
+ """
297
+ if not query or not query.strip():
298
+ return []
299
+ fts_query = _sanitize_fts_query(query)
300
+ if not fts_query:
301
+ return []
302
+
303
+ sql_parts = [
304
+ "SELECT d.* FROM drawers d",
305
+ "JOIN drawers_fts f ON f.rowid = d.id",
306
+ "JOIN rooms r ON r.id = d.room_id",
307
+ "JOIN wings w ON w.id = r.wing_id",
308
+ "WHERE drawers_fts MATCH ?",
309
+ ]
310
+ params: list[Any] = [fts_query]
311
+ if wing_names:
312
+ placeholders = ",".join("?" * len(wing_names))
313
+ sql_parts.append(f"AND w.name IN ({placeholders})")
314
+ params.extend(wing_names)
315
+ # Over-fetch so dedup still leaves us with ``limit`` distinct drawers.
316
+ sql_parts.append("ORDER BY bm25(drawers_fts) ASC LIMIT ?")
317
+ params.append(max(limit * 4, limit))
318
+
319
+ with _connect() as conn:
320
+ rows = conn.execute(" ".join(sql_parts), params).fetchall()
321
+
322
+ seen: set[str] = set()
323
+ out: list[Drawer] = []
324
+ for row in rows:
325
+ drawer = _row_to_drawer(row)
326
+ if drawer.content in seen:
327
+ continue
328
+ seen.add(drawer.content)
329
+ out.append(drawer)
330
+ if len(out) >= limit:
331
+ break
332
+ return out
333
+
334
+
335
+ # Quote each token and OR them: FTS5 punctuation otherwise acts as operators.
336
+ _FTS_BAD = set('"()*:^-+')
337
+
338
+
339
+ def _sanitize_fts_query(query: str) -> str:
340
+ tokens = []
341
+ for raw in query.split():
342
+ stripped = "".join(ch for ch in raw if ch not in _FTS_BAD).strip()
343
+ if len(stripped) >= 2:
344
+ tokens.append(f'"{stripped}"')
345
+ return " OR ".join(tokens)
346
+
347
+
348
+ def list_wings() -> list[str]:
349
+ with _connect() as conn:
350
+ rows = conn.execute("SELECT name FROM wings ORDER BY name").fetchall()
351
+ return [r["name"] for r in rows]
352
+
353
+
354
+ def count_drawers(wing_name: str | None = None) -> int:
355
+ with _connect() as conn:
356
+ if wing_name is None:
357
+ row = conn.execute("SELECT COUNT(*) AS n FROM drawers").fetchone()
358
+ else:
359
+ row = conn.execute(
360
+ """
361
+ SELECT COUNT(*) AS n FROM drawers d
362
+ JOIN rooms r ON r.id = d.room_id
363
+ JOIN wings w ON w.id = r.wing_id
364
+ WHERE w.name = ?
365
+ """,
366
+ (wing_name,),
367
+ ).fetchone()
368
+ return int(row["n"]) if row else 0