alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,384 @@
1
+ """TUI-native pickers for the guarded-workspace startup prompt.
2
+
3
+ When ``ALYSIS_TUI`` is on, the startup workspace-guard resolution (broad/risky
4
+ workspace → choose an action, then pick a project / type a path) is rendered with
5
+ the same visual grammar as the chat + setup TUIs — the green ``#3fb950`` accent,
6
+ the dark picker panel, and the shared :func:`_render_picker_rows` renderer —
7
+ instead of the classic Rich/inline prompt.
8
+
9
+ These functions are drop-in replacements for the ``select_action_interactive`` /
10
+ ``select_candidate_interactive`` / ``prompt_text`` callbacks that
11
+ :func:`alysis_code.workspace_binding_ui.resolve_startup_workspace_binding`
12
+ already accepts, so the binding *logic* is untouched: we only change how each step
13
+ is painted. Each step is its own short-lived full-screen ``Application`` (the
14
+ binding resolver drives the loop), mirroring how the classic selectors run one
15
+ inline picker per call.
16
+
17
+ Every entry point degrades gracefully: if a real terminal is unavailable (or
18
+ prompt_toolkit raises), the action/candidate selectors return
19
+ ``(None, False)`` so the resolver falls back to its typed-prompt path, exactly
20
+ like the classic selectors do.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import re
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from .app import _ACCENT, _render_picker_rows, _wrap_line
31
+ from .app import _STYLE as _CHAT_STYLE
32
+
33
+ # The shared row builders prefix labels with "N) " for the classic numbered
34
+ # prompt; the TUI picker draws its own "N." gutter, so strip the leading number.
35
+ _NUM_PREFIX = re.compile(r"^\s*\d+\)\s*")
36
+
37
+ _PICKER_HINT = "↑/↓ move · Enter confirm · 1-9 quick-pick · Esc cancel"
38
+ _INPUT_HINT = "Enter confirm · Esc cancel"
39
+
40
+
41
+ def _strip_num(label: str) -> str:
42
+ return _NUM_PREFIX.sub("", str(label)).strip()
43
+
44
+
45
+ def _guard_style() -> Any:
46
+ from prompt_toolkit.styles import Style, merge_styles
47
+
48
+ extra = Style.from_dict(
49
+ {
50
+ "wsg.header": f"bold {_ACCENT}",
51
+ "wsg.headerdim": "#6e7681",
52
+ "wsg.title": "bold #e6edf3",
53
+ "wsg.subtitle": "#8b949e",
54
+ "wsg.footer": "#6e7681",
55
+ "wsg.label": "#c9d1d9",
56
+ "wsg.dim": "#6e7681",
57
+ }
58
+ )
59
+ return merge_styles([_CHAT_STYLE, extra])
60
+
61
+
62
+ def _header_fragments(subtitle: str) -> Any:
63
+ from prompt_toolkit.formatted_text import FormattedText
64
+
65
+ return FormattedText(
66
+ [
67
+ ("class:wsg.header", "◆ alysis"),
68
+ ("class:wsg.headerdim", f" · {subtitle}"),
69
+ ]
70
+ )
71
+
72
+
73
+ def _footer_fragments(hint: str) -> Any:
74
+ from prompt_toolkit.formatted_text import FormattedText
75
+
76
+ return FormattedText([("class:wsg.footer", hint)])
77
+
78
+
79
+ def _run_option_picker(
80
+ *,
81
+ subtitle: str,
82
+ title: str,
83
+ rows: list[tuple[str, str, str]],
84
+ default_index: int = 0,
85
+ input: Any | None = None,
86
+ output: Any | None = None,
87
+ ) -> tuple[str | None, bool]:
88
+ """Render a selectable option list and return ``(value, interactive_available)``.
89
+
90
+ ``rows`` are ``(value, label, description)`` tuples (the label may carry the
91
+ classic ``N)`` prefix, which is stripped). ``value`` of the chosen row is
92
+ returned, or ``None`` when the user cancels (Esc / Ctrl-C). On any terminal /
93
+ prompt_toolkit failure the call returns ``(None, False)`` so the resolver can
94
+ fall back to its typed prompt.
95
+ """
96
+ if not rows:
97
+ return None, True
98
+ try:
99
+ from prompt_toolkit.application import Application
100
+ from prompt_toolkit.application.current import get_app
101
+ from prompt_toolkit.data_structures import Point
102
+ from prompt_toolkit.formatted_text import FormattedText
103
+ from prompt_toolkit.key_binding import KeyBindings
104
+ from prompt_toolkit.layout import Layout
105
+ from prompt_toolkit.layout.containers import HSplit, ScrollOffsets, Window
106
+ from prompt_toolkit.layout.controls import FormattedTextControl
107
+ from prompt_toolkit.layout.margins import ScrollbarMargin
108
+ except Exception:
109
+ return None, False
110
+
111
+ picker_rows = [
112
+ {"label": _strip_num(label), "description": desc, "value": value}
113
+ for value, label, desc in rows
114
+ ]
115
+ state: dict[str, Any] = {
116
+ "index": max(0, min(default_index, len(picker_rows) - 1)),
117
+ "result": None,
118
+ }
119
+ cursor = {"row": 0}
120
+
121
+ def _width(win: Window, fallback: int = 80) -> int:
122
+ info = win.render_info
123
+ if info is not None and info.window_width:
124
+ return int(info.window_width)
125
+ try:
126
+ return max(20, get_app().output.get_size().columns - 2)
127
+ except Exception:
128
+ return fallback
129
+
130
+ def _body() -> FormattedText:
131
+ width = _width(body_window)
132
+ out_rows: list[list[tuple[str, str]]] = []
133
+ for line in _wrap_line(title, width):
134
+ out_rows.append([("class:wsg.title", line)])
135
+ out_rows.append([("", "")]) # spacer
136
+ header_lines = len(out_rows)
137
+ # Hint lives in the pinned footer only (it stays put while a long list
138
+ # scrolls), so suppress the picker's own trailing hint to avoid a dupe.
139
+ picker = _render_picker_rows(picker_rows, state["index"], width, hint="")
140
+ located = next(
141
+ (i for i, prow in enumerate(picker) if any("selcaret" in s for s, _ in prow)),
142
+ 0,
143
+ )
144
+ cursor["row"] = header_lines + located
145
+ out_rows.extend(picker)
146
+ flat: list[tuple[str, str]] = []
147
+ for row in out_rows:
148
+ flat.extend(row)
149
+ flat.append(("", "\n"))
150
+ return FormattedText(flat)
151
+
152
+ body_window = Window(
153
+ FormattedTextControl(
154
+ _body,
155
+ focusable=True,
156
+ get_cursor_position=lambda: Point(0, cursor["row"]),
157
+ ),
158
+ wrap_lines=True,
159
+ scroll_offsets=ScrollOffsets(top=1, bottom=1),
160
+ right_margins=[ScrollbarMargin(display_arrows=False)],
161
+ )
162
+ root = HSplit(
163
+ [
164
+ Window(FormattedTextControl(lambda: _header_fragments(subtitle)), height=1),
165
+ Window(height=1),
166
+ body_window,
167
+ Window(height=1),
168
+ Window(FormattedTextControl(lambda: _footer_fragments(_PICKER_HINT)), height=1),
169
+ ]
170
+ )
171
+
172
+ kb = KeyBindings()
173
+
174
+ def _invalidate() -> None:
175
+ try:
176
+ get_app().invalidate()
177
+ except Exception:
178
+ pass
179
+
180
+ @kb.add("up", eager=True)
181
+ @kb.add("k", eager=True)
182
+ def _up(event: Any) -> None:
183
+ state["index"] = (state["index"] - 1) % len(picker_rows)
184
+ _invalidate()
185
+
186
+ @kb.add("down", eager=True)
187
+ @kb.add("j", eager=True)
188
+ def _down(event: Any) -> None:
189
+ state["index"] = (state["index"] + 1) % len(picker_rows)
190
+ _invalidate()
191
+
192
+ @kb.add("enter", eager=True)
193
+ def _confirm(event: Any) -> None:
194
+ state["result"] = picker_rows[state["index"]]["value"]
195
+ event.app.exit()
196
+
197
+ for _digit in "123456789":
198
+
199
+ @kb.add(_digit, eager=True)
200
+ def _pick(event: Any) -> None:
201
+ try:
202
+ idx = int(event.data) - 1
203
+ except (TypeError, ValueError):
204
+ return
205
+ if 0 <= idx < len(picker_rows):
206
+ state["index"] = idx
207
+ state["result"] = picker_rows[idx]["value"]
208
+ event.app.exit()
209
+
210
+ @kb.add("escape")
211
+ @kb.add("c-c")
212
+ @kb.add("c-d")
213
+ def _cancel(event: Any) -> None:
214
+ state["result"] = None
215
+ event.app.exit()
216
+
217
+ app: Application = Application(
218
+ layout=Layout(root, focused_element=body_window),
219
+ key_bindings=kb,
220
+ style=_guard_style(),
221
+ full_screen=True,
222
+ mouse_support=False,
223
+ input=input,
224
+ output=output,
225
+ )
226
+ try:
227
+ app.run()
228
+ except Exception:
229
+ return None, False
230
+ return state["result"], True
231
+
232
+
233
+ def select_guarded_workspace_action(
234
+ *,
235
+ binding: Any,
236
+ candidates: tuple[Any, ...],
237
+ allow_use_current_action: bool = True,
238
+ console: Any | None = None,
239
+ input: Any | None = None,
240
+ output: Any | None = None,
241
+ ) -> tuple[str | None, bool]:
242
+ """TUI replacement for the guarded-workspace action selector."""
243
+ from ...workspace_binding_ui import guarded_workspace_action_rows
244
+
245
+ rows = guarded_workspace_action_rows(
246
+ binding=binding,
247
+ candidates=candidates,
248
+ allow_use_current_action=allow_use_current_action,
249
+ )
250
+ # Default to "choose project" when offered (index 1 once "use current" leads),
251
+ # else the first row — same default the classic selector uses.
252
+ default_index = 1 if allow_use_current_action and len(rows) > 1 else 0
253
+ path_label = os.fspath(getattr(binding, "requested_path", ""))
254
+ return _run_option_picker(
255
+ subtitle="workspace",
256
+ title=f"Guarded workspace: {path_label}",
257
+ rows=rows,
258
+ default_index=default_index,
259
+ input=input,
260
+ output=output,
261
+ )
262
+
263
+
264
+ def select_workspace_candidate(
265
+ *,
266
+ base_path: Path,
267
+ candidates: tuple[Any, ...],
268
+ console: Any | None = None,
269
+ input: Any | None = None,
270
+ output: Any | None = None,
271
+ ) -> tuple[Path | None, bool]:
272
+ """TUI replacement for the project-folder candidate selector."""
273
+ from ...workspace_binding_ui import workspace_candidate_rows
274
+
275
+ rows = workspace_candidate_rows(base_path=base_path, candidates=candidates)
276
+ if not rows:
277
+ return None, True
278
+ value, interactive_available = _run_option_picker(
279
+ subtitle="workspace",
280
+ title=f"Project folders under {os.fspath(base_path)}",
281
+ rows=rows,
282
+ default_index=0,
283
+ input=input,
284
+ output=output,
285
+ )
286
+ if value is None:
287
+ return None, interactive_available
288
+ return Path(value), interactive_available
289
+
290
+
291
+ def workspace_guard_prompt_text(
292
+ text: str,
293
+ *,
294
+ default: str | None = None,
295
+ input: Any | None = None,
296
+ output: Any | None = None,
297
+ ) -> str:
298
+ """TUI replacement for the guarded-workspace typed prompt.
299
+
300
+ Returns the entered text (or ``default`` when blank). Raises
301
+ ``KeyboardInterrupt`` on Esc / Ctrl-C so the resolver treats it as "go back"
302
+ — matching the classic ``_prompt_text_with_escape`` contract.
303
+ """
304
+ from prompt_toolkit.application import Application
305
+ from prompt_toolkit.filters import Condition
306
+ from prompt_toolkit.formatted_text import FormattedText
307
+ from prompt_toolkit.key_binding import KeyBindings
308
+ from prompt_toolkit.layout import Layout
309
+ from prompt_toolkit.layout.containers import ConditionalContainer, HSplit, Window
310
+ from prompt_toolkit.layout.controls import FormattedTextControl
311
+ from prompt_toolkit.layout.processors import BeforeInput
312
+ from prompt_toolkit.widgets import Frame, TextArea
313
+
314
+ input_area = TextArea(
315
+ height=1,
316
+ multiline=False,
317
+ wrap_lines=False,
318
+ style="class:tui.input",
319
+ input_processors=[BeforeInput("> ", style="class:tui.prompt")],
320
+ )
321
+
322
+ def _label() -> FormattedText:
323
+ frags: list[tuple[str, str]] = [("class:wsg.label", f"{text}")]
324
+ return FormattedText(frags)
325
+
326
+ def _hint() -> FormattedText:
327
+ if default is not None:
328
+ return FormattedText([("class:wsg.dim", f"default: {default} (Enter to accept)")])
329
+ return FormattedText([("", "")])
330
+
331
+ has_default = default is not None
332
+ root = HSplit(
333
+ [
334
+ Window(FormattedTextControl(lambda: _header_fragments("workspace")), height=1),
335
+ Window(height=1),
336
+ Window(FormattedTextControl(_label), height=1),
337
+ ConditionalContainer(
338
+ Window(FormattedTextControl(_hint), height=1),
339
+ filter=Condition(lambda: has_default),
340
+ ),
341
+ Frame(input_area),
342
+ Window(height=1),
343
+ Window(FormattedTextControl(lambda: _footer_fragments(_INPUT_HINT)), height=1),
344
+ ]
345
+ )
346
+
347
+ kb = KeyBindings()
348
+ result: dict[str, Any] = {"text": None, "cancelled": False}
349
+
350
+ @kb.add("enter", eager=True)
351
+ def _submit(event: Any) -> None:
352
+ result["text"] = input_area.text
353
+ event.app.exit()
354
+
355
+ @kb.add("escape")
356
+ @kb.add("c-c")
357
+ @kb.add("c-d")
358
+ def _cancel(event: Any) -> None:
359
+ result["cancelled"] = True
360
+ event.app.exit()
361
+
362
+ app: Application = Application(
363
+ layout=Layout(root, focused_element=input_area),
364
+ key_bindings=kb,
365
+ style=_guard_style(),
366
+ full_screen=True,
367
+ mouse_support=False,
368
+ input=input,
369
+ output=output,
370
+ )
371
+ app.run()
372
+ if result["cancelled"]:
373
+ raise KeyboardInterrupt
374
+ entered = str(result["text"] or "").strip()
375
+ if not entered and default is not None:
376
+ return default
377
+ return entered
378
+
379
+
380
+ __all__ = [
381
+ "select_guarded_workspace_action",
382
+ "select_workspace_candidate",
383
+ "workspace_guard_prompt_text",
384
+ ]
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import shutil
5
+ import subprocess
6
+ import uuid
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+
10
+
11
+ class ClipboardError(RuntimeError):
12
+ pass
13
+
14
+
15
+ def _run_clipboard_writer(command: list[str], payload: bytes) -> str | None:
16
+ try:
17
+ proc = subprocess.run(
18
+ command,
19
+ input=payload,
20
+ capture_output=True,
21
+ check=False,
22
+ timeout=10,
23
+ )
24
+ except (OSError, subprocess.SubprocessError) as exc:
25
+ return str(exc)
26
+ if proc.returncode == 0:
27
+ return None
28
+ detail = _decode_text(proc.stderr) if proc.stderr else ""
29
+ return detail or f"exit code {proc.returncode}"
30
+
31
+
32
+ def copy_text_to_clipboard(text: str) -> None:
33
+ """Copy text to the host clipboard without shell interpolation.
34
+
35
+ The ordered backends cover macOS, Windows/WSL, Wayland, and X11. PowerShell
36
+ receives base64-encoded UTF-8 so non-ASCII selections survive the WSL/Windows
37
+ process boundary without depending on the active console code page.
38
+ """
39
+ value = str(text or "")
40
+ if not value:
41
+ raise ClipboardError("Cannot copy empty text.")
42
+
43
+ utf8_payload = value.encode("utf-8")
44
+ candidates: list[tuple[str, list[str], bytes]] = [
45
+ ("pbcopy", ["pbcopy"], utf8_payload),
46
+ (
47
+ "powershell.exe",
48
+ [
49
+ "powershell.exe",
50
+ "-NoProfile",
51
+ "-NonInteractive",
52
+ "-Command",
53
+ (
54
+ "$b=[Console]::In.ReadToEnd(); "
55
+ "$t=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b)); "
56
+ "Set-Clipboard -Value $t"
57
+ ),
58
+ ],
59
+ base64.b64encode(utf8_payload),
60
+ ),
61
+ ("clip.exe", ["clip.exe"], utf8_payload),
62
+ ("wl-copy", ["wl-copy", "--type", "text/plain;charset=utf-8"], utf8_payload),
63
+ ("xclip", ["xclip", "-selection", "clipboard", "-in"], utf8_payload),
64
+ ("xsel", ["xsel", "--clipboard", "--input"], utf8_payload),
65
+ ]
66
+ errors: list[str] = []
67
+ for executable, command, payload in candidates:
68
+ if shutil.which(executable) is None:
69
+ continue
70
+ error = _run_clipboard_writer(command, payload)
71
+ if error is None:
72
+ return
73
+ errors.append(f"{executable}: {error}")
74
+ detail = "; ".join(errors) if errors else "no supported clipboard command found"
75
+ raise ClipboardError(f"Could not copy text to the system clipboard: {detail}")
76
+
77
+
78
+ def _default_output_path(root: Path) -> Path:
79
+ ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
80
+ return root / ".alysis_images" / f"clipboard_{ts}_{uuid.uuid4().hex[:6]}.png"
81
+
82
+
83
+ def _decode_text(blob: bytes) -> str:
84
+ try:
85
+ return blob.decode("utf-8").strip()
86
+ except UnicodeDecodeError:
87
+ return blob.decode("utf-16-le", errors="ignore").strip()
88
+
89
+
90
+ def _read_png_wl_paste() -> bytes:
91
+ if shutil.which("wl-paste") is None:
92
+ raise ClipboardError("wl-paste is not installed.")
93
+ proc = subprocess.run(
94
+ ["wl-paste", "--type", "image/png", "--no-newline"],
95
+ capture_output=True,
96
+ check=False,
97
+ timeout=20,
98
+ )
99
+ if proc.returncode != 0 or not proc.stdout:
100
+ raise ClipboardError("wl-paste clipboard does not contain PNG image data.")
101
+ return proc.stdout
102
+
103
+
104
+ def _read_png_xclip() -> bytes:
105
+ if shutil.which("xclip") is None:
106
+ raise ClipboardError("xclip is not installed.")
107
+ proc = subprocess.run(
108
+ ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"],
109
+ capture_output=True,
110
+ check=False,
111
+ timeout=20,
112
+ )
113
+ if proc.returncode != 0 or not proc.stdout:
114
+ raise ClipboardError("xclip clipboard does not contain PNG image data.")
115
+ return proc.stdout
116
+
117
+
118
+ def _read_png_powershell() -> bytes:
119
+ if shutil.which("powershell.exe") is None:
120
+ raise ClipboardError("powershell.exe is not available.")
121
+ script = (
122
+ "$img = Get-Clipboard -Format Image -ErrorAction SilentlyContinue; "
123
+ "if ($null -eq $img) { exit 3 }; "
124
+ "$ms = New-Object System.IO.MemoryStream; "
125
+ "$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); "
126
+ "[Console]::Out.Write([Convert]::ToBase64String($ms.ToArray()))"
127
+ )
128
+ proc = subprocess.run(
129
+ ["powershell.exe", "-NoProfile", "-Command", script],
130
+ capture_output=True,
131
+ check=False,
132
+ timeout=20,
133
+ )
134
+ if proc.returncode != 0 or not proc.stdout:
135
+ raise ClipboardError("PowerShell clipboard does not contain image data.")
136
+ encoded = _decode_text(proc.stdout)
137
+ if not encoded:
138
+ raise ClipboardError("PowerShell clipboard returned empty image data.")
139
+ try:
140
+ return base64.b64decode(encoded, validate=True)
141
+ except Exception as e: # noqa: BLE001
142
+ raise ClipboardError("PowerShell clipboard returned invalid image data.") from e
143
+
144
+
145
+ def _read_clipboard_png() -> bytes:
146
+ errors: list[str] = []
147
+ for reader in (_read_png_wl_paste, _read_png_xclip, _read_png_powershell):
148
+ try:
149
+ return reader()
150
+ except ClipboardError as e:
151
+ errors.append(str(e))
152
+ detail = "; ".join(errors)
153
+ raise ClipboardError(
154
+ "Could not read an image from clipboard. "
155
+ "Try installing wl-paste or xclip, or ensure Windows clipboard has an image. "
156
+ f"Details: {detail}"
157
+ )
158
+
159
+
160
+ def paste_clipboard_image(*, root: Path, output_path: str | None = None) -> Path:
161
+ root = root.resolve()
162
+ if output_path:
163
+ p = Path(output_path).expanduser()
164
+ dest = p if p.is_absolute() else root / p
165
+ else:
166
+ dest = _default_output_path(root)
167
+ data = _read_clipboard_png()
168
+ if not data:
169
+ raise ClipboardError("Clipboard image data is empty.")
170
+ dest.parent.mkdir(parents=True, exist_ok=True)
171
+ dest.write_bytes(data)
172
+ return dest.resolve()