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,487 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import tomllib
5
+ import warnings
6
+ from pathlib import Path, PurePosixPath, PureWindowsPath
7
+ from typing import Annotated, Any, Literal
8
+
9
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
10
+ from packaging.version import InvalidVersion, Version
11
+ from pydantic import (
12
+ AliasChoices,
13
+ BaseModel,
14
+ ConfigDict,
15
+ Field,
16
+ HttpUrl,
17
+ TypeAdapter,
18
+ ValidationError,
19
+ field_validator,
20
+ model_validator,
21
+ )
22
+
23
+ MANIFEST_FILENAME = "alysis-plugin.toml"
24
+ # Third-party plugins published before the rename ship this filename; they are
25
+ # out of our control, so both spellings stay loadable.
26
+ LEGACY_MANIFEST_FILENAME = "sylliptor-plugin.toml"
27
+ MAX_MANIFEST_BYTES = 64 * 1024
28
+
29
+ _PLUGIN_ID_PATTERN = r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_-]*$"
30
+ _ENV_NAME_PATTERN = r"^[A-Z][A-Z0-9_]*$"
31
+ _KEYWORD_PATTERN = r"^[a-z0-9_-]+$"
32
+ _EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
33
+ _COMMON_SPDX_LICENSE_IDS = frozenset(
34
+ {
35
+ "Apache-2.0",
36
+ "BSD-2-Clause",
37
+ "BSD-3-Clause",
38
+ "GPL-3.0-only",
39
+ "GPL-3.0-or-later",
40
+ "ISC",
41
+ "LGPL-3.0-only",
42
+ "LGPL-3.0-or-later",
43
+ "MIT",
44
+ "MPL-2.0",
45
+ "Proprietary",
46
+ "Unlicense",
47
+ }
48
+ )
49
+ _CONTACT_URL_ADAPTER = TypeAdapter(HttpUrl)
50
+
51
+ PluginId = Annotated[str, Field(max_length=64, pattern=_PLUGIN_ID_PATTERN)]
52
+ PluginName = Annotated[str, Field(min_length=1, max_length=60)]
53
+ Text200 = Annotated[str, Field(min_length=1, max_length=200)]
54
+ NonEmptyString = Annotated[str, Field(min_length=1)]
55
+ Keyword = Annotated[str, Field(min_length=1, max_length=32, pattern=_KEYWORD_PATTERN)]
56
+ ComponentPath = Annotated[str, Field(min_length=1)]
57
+ OptionalComponentId = Annotated[str, Field(min_length=1)] | None
58
+ EnvName = Annotated[str, Field(pattern=_ENV_NAME_PATTERN)]
59
+ ScopeName = Annotated[str, Field(min_length=1)]
60
+ CommandArg = Annotated[str, Field(min_length=1)]
61
+
62
+
63
+ class PluginManifestError(Exception):
64
+ def __init__(self, errors: list[str] | tuple[str, ...]) -> None:
65
+ self.errors = tuple(str(error) for error in errors)
66
+ if self.errors:
67
+ message = "Invalid plugin manifest:\n" + "\n".join(
68
+ f" - {error}" for error in self.errors
69
+ )
70
+ else:
71
+ message = "Invalid plugin manifest."
72
+ super().__init__(message)
73
+
74
+
75
+ class PluginMeta(BaseModel):
76
+ model_config = ConfigDict(extra="forbid")
77
+
78
+ id: PluginId
79
+ name: PluginName
80
+ version: NonEmptyString
81
+ description: Text200
82
+ author: Text200
83
+ license: NonEmptyString
84
+ homepage: HttpUrl | None = None
85
+ repository: HttpUrl | None = None
86
+ keywords: list[Keyword] = Field(default_factory=list, max_length=8)
87
+
88
+ @field_validator("version")
89
+ @classmethod
90
+ def _validate_version(cls, value: str) -> str:
91
+ try:
92
+ Version(value)
93
+ except InvalidVersion as exc:
94
+ raise ValueError("must be a valid PEP 440 / SemVer version") from exc
95
+ return value
96
+
97
+ @field_validator("license")
98
+ @classmethod
99
+ def _validate_license(cls, value: str) -> str:
100
+ if not value.strip():
101
+ raise ValueError("must not be empty")
102
+ return value
103
+
104
+
105
+ class Compatibility(BaseModel):
106
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
107
+
108
+ # Manifests written before the rename declare this key as ``sylliptor``.
109
+ # With extra="forbid" they would fail validation outright, so the old
110
+ # spelling stays accepted as an alias.
111
+ alysis: NonEmptyString = Field(
112
+ validation_alias=AliasChoices("alysis", "sylliptor"),
113
+ serialization_alias="alysis",
114
+ )
115
+ platforms: list[Literal["linux", "darwin", "windows"]] = Field(
116
+ default_factory=lambda: ["linux", "darwin", "windows"]
117
+ )
118
+
119
+ @field_validator("alysis")
120
+ @classmethod
121
+ def _validate_alysis(cls, value: str) -> str:
122
+ try:
123
+ SpecifierSet(value)
124
+ except InvalidSpecifier as exc:
125
+ raise ValueError("must be a valid PEP 440 specifier set") from exc
126
+ return value
127
+
128
+
129
+ class SkillComponent(BaseModel):
130
+ model_config = ConfigDict(extra="forbid")
131
+
132
+ path: ComponentPath
133
+ id: OptionalComponentId = None
134
+ enabled: bool = True
135
+
136
+
137
+ class ToolComponent(BaseModel):
138
+ model_config = ConfigDict(extra="forbid")
139
+
140
+ path: ComponentPath
141
+ id: OptionalComponentId = None
142
+ enabled: bool = True
143
+ description: Text200
144
+ required_env: list[EnvName] = Field(default_factory=list)
145
+ network: bool
146
+ filesystem: Literal["none", "read", "write"]
147
+ timeout_sec: Annotated[int, Field(ge=1, le=600)] = 60
148
+
149
+
150
+ class McpServerComponent(BaseModel):
151
+ model_config = ConfigDict(extra="forbid")
152
+
153
+ id: Annotated[str, Field(min_length=1)]
154
+ enabled: bool = True
155
+ transport: Literal["stdio", "http"]
156
+ command: list[CommandArg] | None = Field(default=None, min_length=1)
157
+ url: HttpUrl | None = None
158
+ env: list[EnvName] = Field(default_factory=list)
159
+ scopes: list[ScopeName] = Field(min_length=1, max_length=32)
160
+ oauth: dict[str, Any] | None = None
161
+
162
+ @model_validator(mode="after")
163
+ def _validate_transport(self) -> McpServerComponent:
164
+ if self.transport == "stdio":
165
+ if self.command is None:
166
+ raise ValueError("command is required when transport is 'stdio'")
167
+ if self.url is not None:
168
+ raise ValueError("url is not allowed when transport is 'stdio'")
169
+ return self
170
+
171
+ if self.url is None:
172
+ raise ValueError("url is required when transport is 'http'")
173
+ if self.command is not None:
174
+ raise ValueError("command is not allowed when transport is 'http'")
175
+
176
+ scheme = str(self.url.scheme).lower()
177
+ host = str(self.url.host or "").lower()
178
+ if scheme == "http" and host not in {"localhost", "127.0.0.1"}:
179
+ raise ValueError("url must use https unless host is localhost or 127.0.0.1")
180
+ return self
181
+
182
+
183
+ class HookComponent(BaseModel):
184
+ model_config = ConfigDict(extra="forbid")
185
+
186
+ event: Literal["PreToolUse", "PostToolUse", "PostWrite", "SessionStart", "SessionStop"]
187
+ path: ComponentPath
188
+ id: OptionalComponentId = None
189
+ enabled: bool = True
190
+
191
+
192
+ class Components(BaseModel):
193
+ model_config = ConfigDict(extra="forbid")
194
+
195
+ skill: list[SkillComponent] = Field(default_factory=list)
196
+ tool: list[ToolComponent] = Field(default_factory=list)
197
+ mcp_server: list[McpServerComponent] = Field(default_factory=list)
198
+ hook: list[HookComponent] = Field(default_factory=list)
199
+
200
+
201
+ class Security(BaseModel):
202
+ model_config = ConfigDict(extra="forbid")
203
+
204
+ contact: NonEmptyString
205
+ policy_url: HttpUrl | None = None
206
+ disclosure_days: Annotated[int, Field(ge=1, le=365)] | None = None
207
+
208
+ @field_validator("contact")
209
+ @classmethod
210
+ def _validate_contact(cls, value: str) -> str:
211
+ if _EMAIL_PATTERN.fullmatch(value):
212
+ return value
213
+ try:
214
+ parsed = _CONTACT_URL_ADAPTER.validate_python(value)
215
+ except ValidationError as exc:
216
+ raise ValueError("must be an email address or https URL") from exc
217
+ if str(parsed.scheme).lower() != "https":
218
+ raise ValueError("must be an email address or https URL")
219
+ return value
220
+
221
+
222
+ class PluginManifest(BaseModel):
223
+ model_config = ConfigDict(extra="forbid")
224
+
225
+ schema_version: Literal[1]
226
+ plugin: PluginMeta
227
+ compatibility: Compatibility
228
+ components: Components = Field(default_factory=Components)
229
+ security: Security | None = None
230
+
231
+
232
+ def resolve_manifest_path(plugin_root: Path) -> Path:
233
+ """Locate a plugin manifest, preferring the current filename.
234
+
235
+ Falls back to the pre-rebrand name so an already-published plugin keeps
236
+ installing. The current name is returned when neither exists, so the
237
+ "missing manifest" error names the file authors should be writing.
238
+ """
239
+ current = plugin_root / MANIFEST_FILENAME
240
+ if current.is_file():
241
+ return current
242
+ legacy = plugin_root / LEGACY_MANIFEST_FILENAME
243
+ if legacy.is_file():
244
+ return legacy
245
+ return current
246
+
247
+
248
+ def load_manifest(plugin_root: Path) -> PluginManifest:
249
+ manifest_path = resolve_manifest_path(plugin_root)
250
+ text = _read_manifest_text(manifest_path)
251
+
252
+ try:
253
+ raw = tomllib.loads(text)
254
+ except tomllib.TOMLDecodeError as exc:
255
+ raise PluginManifestError([_format_toml_error(exc)]) from exc
256
+
257
+ try:
258
+ manifest = PluginManifest.model_validate(raw)
259
+ except ValidationError as exc:
260
+ raise PluginManifestError(_format_validation_errors(exc)) from exc
261
+
262
+ invariant_messages = _validate_manifest_invariants(manifest, plugin_root)
263
+ warning_messages = [
264
+ message.removeprefix("WARN: ").strip()
265
+ for message in invariant_messages
266
+ if message.startswith("WARN: ")
267
+ ]
268
+ error_messages = [message for message in invariant_messages if not message.startswith("WARN: ")]
269
+
270
+ license_warning = _license_warning(manifest)
271
+ if license_warning is not None:
272
+ warning_messages.append(license_warning)
273
+
274
+ if error_messages:
275
+ raise PluginManifestError(
276
+ [*error_messages, *(f"WARN: {message}" for message in warning_messages)]
277
+ )
278
+
279
+ for message in warning_messages:
280
+ warnings.warn(message, stacklevel=2)
281
+
282
+ return manifest
283
+
284
+
285
+ def _validate_manifest_invariants(manifest: PluginManifest, plugin_root: Path) -> list[str]:
286
+ messages: list[str] = []
287
+ total_components = (
288
+ len(manifest.components.skill)
289
+ + len(manifest.components.tool)
290
+ + len(manifest.components.mcp_server)
291
+ + len(manifest.components.hook)
292
+ )
293
+ if total_components > 32:
294
+ messages.append(
295
+ f"components: total component count {total_components} exceeds the cap of 32"
296
+ )
297
+ if total_components == 0:
298
+ messages.append(
299
+ "WARN: components: manifest declares no components; metadata-only plugins are allowed "
300
+ "but contribute nothing."
301
+ )
302
+
303
+ plugin_root_resolved = plugin_root.resolve()
304
+ for collection_name, index, raw_path in _iter_component_paths(manifest):
305
+ path_label = f"components.{collection_name}[{index}].path"
306
+ if _is_path_absolute(raw_path):
307
+ messages.append(f"{path_label}: absolute paths are not allowed")
308
+ continue
309
+ if _contains_parent_reference(raw_path):
310
+ messages.append(f"{path_label}: path escapes or could resolve outside plugin root")
311
+ continue
312
+
313
+ component_path = _resolve_component_path(plugin_root_resolved, raw_path)
314
+ try:
315
+ component_path.relative_to(plugin_root_resolved)
316
+ except ValueError:
317
+ messages.append(f"{path_label}: resolved path is outside plugin root: {raw_path}")
318
+ continue
319
+
320
+ if not component_path.exists():
321
+ messages.append(f"{path_label}: path does not exist: {raw_path}")
322
+
323
+ for collection_name, ids in _effective_ids_by_type(manifest).items():
324
+ seen: dict[str, int] = {}
325
+ for index, effective_id in enumerate(ids):
326
+ if effective_id in seen:
327
+ messages.append(
328
+ "components."
329
+ f"{collection_name}[{index}]: duplicate effective id {effective_id!r} "
330
+ f"within {collection_name}"
331
+ )
332
+ continue
333
+ seen[effective_id] = index
334
+
335
+ return messages
336
+
337
+
338
+ def _read_manifest_text(manifest_path: Path) -> str:
339
+ try:
340
+ size = manifest_path.stat().st_size
341
+ except FileNotFoundError as exc:
342
+ raise PluginManifestError([f"manifest file not found: {manifest_path}"]) from exc
343
+ except OSError as exc:
344
+ raise PluginManifestError([f"failed to stat manifest file: {manifest_path}"]) from exc
345
+
346
+ if size > MAX_MANIFEST_BYTES:
347
+ raise PluginManifestError(
348
+ [f"manifest file exceeds the {MAX_MANIFEST_BYTES}-byte limit: {size} bytes"]
349
+ )
350
+
351
+ try:
352
+ raw_bytes = manifest_path.read_bytes()
353
+ except OSError as exc:
354
+ raise PluginManifestError([f"failed to read manifest file: {manifest_path}"]) from exc
355
+
356
+ try:
357
+ return raw_bytes.decode("utf-8")
358
+ except UnicodeDecodeError as exc:
359
+ raise PluginManifestError(["manifest file is not valid UTF-8"]) from exc
360
+
361
+
362
+ def _format_toml_error(exc: tomllib.TOMLDecodeError) -> str:
363
+ line = getattr(exc, "lineno", None)
364
+ column = getattr(exc, "colno", None)
365
+ if line is None or column is None:
366
+ position = getattr(exc, "pos", None)
367
+ document = getattr(exc, "doc", None)
368
+ if isinstance(position, int) and isinstance(document, str):
369
+ prefix = document[:position]
370
+ line = prefix.count("\n") + 1
371
+ column = position - prefix.rfind("\n")
372
+ location = ""
373
+ if isinstance(line, int) and isinstance(column, int):
374
+ location = f" at line {line}, column {column}"
375
+ reason = getattr(exc, "msg", None) or str(exc)
376
+ return f"toml parse error{location}: {reason}"
377
+
378
+
379
+ def _format_validation_errors(exc: ValidationError) -> list[str]:
380
+ formatted: list[str] = []
381
+ for error in exc.errors():
382
+ location = _format_error_location(error.get("loc", ()))
383
+ message = _normalize_validation_message(str(error.get("msg", "Invalid value")))
384
+ if location:
385
+ formatted.append(f"{location}: {message}")
386
+ else:
387
+ formatted.append(message)
388
+ return formatted
389
+
390
+
391
+ def _format_error_location(location: tuple[Any, ...] | list[Any]) -> str:
392
+ parts: list[str] = []
393
+ for item in location:
394
+ if isinstance(item, int):
395
+ if parts:
396
+ parts[-1] = f"{parts[-1]}[{item}]"
397
+ else:
398
+ parts.append(f"[{item}]")
399
+ continue
400
+ parts.append(str(item))
401
+ return ".".join(parts)
402
+
403
+
404
+ def _normalize_validation_message(message: str) -> str:
405
+ return message.removeprefix("Value error, ")
406
+
407
+
408
+ def _iter_component_paths(manifest: PluginManifest) -> list[tuple[str, int, str]]:
409
+ paths: list[tuple[str, int, str]] = []
410
+ for collection_name, components in (
411
+ ("skill", manifest.components.skill),
412
+ ("tool", manifest.components.tool),
413
+ ("hook", manifest.components.hook),
414
+ ):
415
+ for index, component in enumerate(components):
416
+ paths.append((collection_name, index, component.path))
417
+ return paths
418
+
419
+
420
+ def _effective_ids_by_type(manifest: PluginManifest) -> dict[str, list[str]]:
421
+ return {
422
+ "skill": [
423
+ component.id or _default_component_id(component.path)
424
+ for component in manifest.components.skill
425
+ ],
426
+ "tool": [
427
+ component.id or _default_component_id(component.path)
428
+ for component in manifest.components.tool
429
+ ],
430
+ "mcp_server": [component.id for component in manifest.components.mcp_server],
431
+ "hook": [
432
+ component.id or _default_component_id(component.path)
433
+ for component in manifest.components.hook
434
+ ],
435
+ }
436
+
437
+
438
+ def _default_component_id(path_value: str) -> str:
439
+ return PureWindowsPath(path_value).name or PurePosixPath(path_value).name
440
+
441
+
442
+ def _is_path_absolute(path_value: str) -> bool:
443
+ posix_path = PurePosixPath(path_value)
444
+ windows_path = PureWindowsPath(path_value)
445
+ return bool(posix_path.is_absolute() or windows_path.is_absolute() or windows_path.drive)
446
+
447
+
448
+ def _contains_parent_reference(path_value: str) -> bool:
449
+ posix_parts = PurePosixPath(path_value).parts
450
+ windows_parts = PureWindowsPath(path_value).parts
451
+ return ".." in posix_parts or ".." in windows_parts
452
+
453
+
454
+ def _resolve_component_path(plugin_root: Path, path_value: str) -> Path:
455
+ windows_path = PureWindowsPath(path_value)
456
+ parts = [part for part in windows_path.parts if part not in {"", "."}]
457
+ return plugin_root.joinpath(*parts).resolve()
458
+
459
+
460
+ def _license_warning(manifest: PluginManifest) -> str | None:
461
+ license_id = manifest.plugin.license.strip()
462
+ if license_id in _COMMON_SPDX_LICENSE_IDS:
463
+ return None
464
+ return (
465
+ f"plugin.license: {license_id!r} is not in the common SPDX allow-list; "
466
+ "double-check the identifier."
467
+ )
468
+
469
+
470
+ __all__ = [
471
+ "Compatibility",
472
+ "Components",
473
+ "HookComponent",
474
+ "LEGACY_MANIFEST_FILENAME",
475
+ "MANIFEST_FILENAME",
476
+ "resolve_manifest_path",
477
+ "MAX_MANIFEST_BYTES",
478
+ "McpServerComponent",
479
+ "PluginManifest",
480
+ "PluginManifestError",
481
+ "PluginMeta",
482
+ "Security",
483
+ "SkillComponent",
484
+ "ToolComponent",
485
+ "_validate_manifest_invariants",
486
+ "load_manifest",
487
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ _NON_ALNUM_RE = re.compile(r"[^a-z0-9]+")
9
+ _REPEATED_DASH_RE = re.compile(r"-+")
10
+
11
+
12
+ def normalize_extension_id(value: str) -> str:
13
+ return value.strip().lower()
14
+
15
+
16
+ def plugin_slug_from_id(ext_id: str) -> str:
17
+ slug = _NON_ALNUM_RE.sub("-", normalize_extension_id(ext_id))
18
+ slug = _REPEATED_DASH_RE.sub("-", slug).strip("-")
19
+ return slug or "ext"
20
+
21
+
22
+ class RegistryEntry(BaseModel):
23
+ model_config = ConfigDict(extra="allow")
24
+
25
+ id: str
26
+ name: str
27
+ description: str
28
+ repo: str
29
+ commit: str
30
+ version: str | None = None
31
+ tags: list[str] = Field(default_factory=list)
32
+ permissions: list[str] = Field(default_factory=list)
33
+ manifest_path: str | None = None
34
+ sha256: str | None = None
35
+
36
+
37
+ class RegistryFile(BaseModel):
38
+ model_config = ConfigDict(extra="allow")
39
+
40
+ schema_version: int = 1
41
+ extensions: list[RegistryEntry] = Field(default_factory=list)
42
+
43
+
44
+ class InstalledExtensionState(BaseModel):
45
+ model_config = ConfigDict(extra="allow")
46
+
47
+ id: str | None = None
48
+ version: str | None = None
49
+ commit: str | None = None
50
+ source: str | None = None
51
+ trust: str | None = None
52
+ enabled: bool = False
53
+ manifest_sha256: str | None = None
54
+ installed_at: str | None = None
55
+ source_url: str | None = None
56
+ scope: Literal["user", "project"] | None = None
57
+ component_ids: dict[str, list[str]] = Field(default_factory=dict)
58
+
59
+
60
+ class ExtensionState(BaseModel):
61
+ model_config = ConfigDict(extra="allow")
62
+
63
+ schema_version: int = 1
64
+ installed: dict[str, InstalledExtensionState] = Field(default_factory=dict)
65
+ enabled: list[str] = Field(default_factory=list)
66
+
67
+
68
+ class ProjectExtensionOverrides(BaseModel):
69
+ model_config = ConfigDict(extra="allow")
70
+
71
+ schema_version: int = 1
72
+ enabled: list[str] = Field(default_factory=list)
73
+ disabled: list[str] = Field(default_factory=list)
74
+ allow_overrides: bool = False
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ..branding import canonical_user_config_dir, canonical_user_data_dir, env_get
6
+ from .models import plugin_slug_from_id
7
+
8
+
9
+ def _config_dir() -> Path:
10
+ override = env_get("ALYSIS_CONFIG_DIR")
11
+ if override:
12
+ return Path(override)
13
+ return canonical_user_config_dir()
14
+
15
+
16
+ def _data_dir() -> Path:
17
+ override = env_get("ALYSIS_DATA_DIR")
18
+ if override:
19
+ return Path(override)
20
+ return canonical_user_data_dir()
21
+
22
+
23
+ def extensions_data_dir() -> Path:
24
+ return _data_dir() / "extensions"
25
+
26
+
27
+ def extensions_state_path() -> Path:
28
+ return extensions_data_dir() / "state.json"
29
+
30
+
31
+ def extensions_cache_dir() -> Path:
32
+ return extensions_data_dir() / "cache"
33
+
34
+
35
+ def project_extensions_path(repo_root: Path) -> Path:
36
+ return repo_root.resolve() / ".alysis" / "extensions.json"
37
+
38
+
39
+ def workspace_trust_path(*, user_config_dir: Path | None = None) -> Path:
40
+ if user_config_dir is not None:
41
+ return user_config_dir.expanduser().resolve() / "extensions" / "workspace_trust.json"
42
+ return extensions_data_dir() / "workspace_trust.json"
43
+
44
+
45
+ def installed_plugin_root(
46
+ plugin_id: str,
47
+ *,
48
+ project: bool,
49
+ repo_root: Path | None,
50
+ ) -> Path:
51
+ slug = plugin_slug_from_id(plugin_id)
52
+ if project:
53
+ if repo_root is None:
54
+ raise ValueError("repo_root is required for project plugin installs.")
55
+ return repo_root.resolve() / ".alysis" / "plugins" / slug
56
+ return extensions_data_dir() / "installed" / slug
@@ -0,0 +1,4 @@
1
+ {
2
+ "schema_version": 1,
3
+ "extensions": []
4
+ }
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from importlib import resources
5
+
6
+ from .models import RegistryEntry, RegistryFile, normalize_extension_id
7
+
8
+
9
+ def load_registry() -> RegistryFile:
10
+ try:
11
+ text = (
12
+ resources.files("alysis_code.extensions")
13
+ .joinpath("registry.json")
14
+ .read_text(encoding="utf-8")
15
+ )
16
+ except FileNotFoundError as e:
17
+ raise RuntimeError("Bundled extensions registry is missing.") from e
18
+
19
+ try:
20
+ raw = json.loads(text)
21
+ except json.JSONDecodeError as e:
22
+ raise RuntimeError("Bundled extensions registry is not valid JSON.") from e
23
+
24
+ if not isinstance(raw, dict):
25
+ raise RuntimeError("Bundled extensions registry must be a JSON object.")
26
+ return RegistryFile.model_validate(raw)
27
+
28
+
29
+ def find_by_id(registry: RegistryFile, ext_id: str) -> RegistryEntry | None:
30
+ target = normalize_extension_id(ext_id)
31
+ for entry in registry.extensions:
32
+ if normalize_extension_id(entry.id) == target:
33
+ return entry
34
+ return None
35
+
36
+
37
+ def search(registry: RegistryFile, query: str) -> list[RegistryEntry]:
38
+ needle = query.strip().casefold()
39
+ if not needle:
40
+ return list(registry.extensions)
41
+
42
+ matches: list[RegistryEntry] = []
43
+ for entry in registry.extensions:
44
+ haystack = [
45
+ entry.id,
46
+ entry.name,
47
+ entry.description,
48
+ *entry.tags,
49
+ ]
50
+ if any(needle in str(field).casefold() for field in haystack):
51
+ matches.append(entry)
52
+ return matches