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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- 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
|
+
]
|
alysis_code/clipboard.py
ADDED
|
@@ -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()
|