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
alysis_code/__init__.py
ADDED
alysis_code/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Generated build identity. Do not edit by hand.
|
|
2
|
+
|
|
3
|
+
Regenerate with ``python3 scripts/generate_build_info.py``. The copy
|
|
4
|
+
committed to the repository is the dev default and deliberately reports an
|
|
5
|
+
unidentifiable build, so that an artifact built without running the
|
|
6
|
+
generator is refused by ``--require-clean-build`` instead of inheriting a
|
|
7
|
+
stale commit hash.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
BUILD_COMMIT = ""
|
|
11
|
+
BUILD_TIMESTAMP = ""
|
|
12
|
+
BUILD_DIRTY = True
|
|
13
|
+
BUILD_SOURCE = "dev-default"
|
|
14
|
+
BUILD_INFO_SCHEMA_VERSION = 1
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Device-flow login for Alysis Code Pro (`alysis login`).
|
|
2
|
+
|
|
3
|
+
The CLI asks the account backend for a short user code (``ABCD-EFGH``), opens
|
|
4
|
+
the website's /activate page where the signed-in user approves it, and polls
|
|
5
|
+
until approval — then receives the user's long-lived gateway key (``slk_…``),
|
|
6
|
+
stores it locally, and activates the ``alysis`` provider profile pointed at
|
|
7
|
+
the Alysis Code LLM gateway (OpenAI-compatible; credits are metered server-side).
|
|
8
|
+
|
|
9
|
+
The device_code secret and the gateway key only ever travel in HTTPS request
|
|
10
|
+
and response bodies — never in URLs. The short user_code does appear in the
|
|
11
|
+
opened URL, but it is single-use, expires in minutes, and is worthless without
|
|
12
|
+
the device_code it is paired with.
|
|
13
|
+
|
|
14
|
+
This flow needs no localhost listener, so it works over SSH and inside WSL the
|
|
15
|
+
same as on a desktop.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import math
|
|
22
|
+
import platform
|
|
23
|
+
import time
|
|
24
|
+
import urllib.error
|
|
25
|
+
import urllib.request
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from datetime import UTC, datetime
|
|
29
|
+
|
|
30
|
+
from . import alysis_cloud as cloud
|
|
31
|
+
from .config import (
|
|
32
|
+
AppConfig,
|
|
33
|
+
clear_persisted_profile_key,
|
|
34
|
+
load_persisted_profile_keys,
|
|
35
|
+
save_config,
|
|
36
|
+
save_persisted_profile_key,
|
|
37
|
+
)
|
|
38
|
+
from .host_browser import open_url
|
|
39
|
+
from .profile_presets import get_preset, make_profile_from_preset
|
|
40
|
+
from .profiles import ProfileSpec, add_profile, get_profile, set_active_profile
|
|
41
|
+
|
|
42
|
+
# Matches the server-side device-code expiry (15 min) so we poll until the code
|
|
43
|
+
# itself dies rather than giving up while the user is still signing in.
|
|
44
|
+
_DEFAULT_TIMEOUT_S = 900.0
|
|
45
|
+
_HTTP_TIMEOUT_S = 15.0
|
|
46
|
+
_DEFAULT_POLL_INTERVAL_S = 5.0
|
|
47
|
+
# Kept short: model discovery runs inline while rendering the interactive `/config`
|
|
48
|
+
# model picker, so an offline/slow gateway must not stall the menu for long.
|
|
49
|
+
_MODELS_TIMEOUT_S = 6.0
|
|
50
|
+
|
|
51
|
+
_FRESH_LOGIN_DEFAULT_MODEL = "deepseek-v4-flash"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AlysisLoginError(Exception):
|
|
55
|
+
"""Raised when the device login handshake fails."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class LoginResult:
|
|
60
|
+
email: str | None
|
|
61
|
+
profile_name: str
|
|
62
|
+
base_url: str
|
|
63
|
+
model: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class LoginStatus:
|
|
68
|
+
logged_in: bool
|
|
69
|
+
profile_name: str
|
|
70
|
+
base_url: str
|
|
71
|
+
active: bool
|
|
72
|
+
key_preview: str | None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class TrialStatus:
|
|
77
|
+
"""Account status snapshot (kept name-compatible with the old MiMo trial)."""
|
|
78
|
+
|
|
79
|
+
plan: str | None
|
|
80
|
+
email: str | None
|
|
81
|
+
trial_ends_at: str | None
|
|
82
|
+
tokens_total: int | None
|
|
83
|
+
tokens_used: int | None
|
|
84
|
+
tokens_remaining: int | None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Public API
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
def login(
|
|
91
|
+
cfg: AppConfig,
|
|
92
|
+
*,
|
|
93
|
+
timeout_s: float = _DEFAULT_TIMEOUT_S,
|
|
94
|
+
browser_opener: Callable[[str], bool] | None = None,
|
|
95
|
+
output_write: Callable[[str], None] | None = None,
|
|
96
|
+
) -> LoginResult:
|
|
97
|
+
"""Run the device-flow login and persist the resulting gateway key."""
|
|
98
|
+
writer = output_write or (lambda _msg: None)
|
|
99
|
+
|
|
100
|
+
# Fail fast on an insecure (non-https) cloud URL before starting, so
|
|
101
|
+
# neither the device_code nor the gateway key could leave over http://.
|
|
102
|
+
try:
|
|
103
|
+
cloud.device_code_url()
|
|
104
|
+
cloud.device_token_url()
|
|
105
|
+
cloud.gateway_base_url()
|
|
106
|
+
except cloud.AlysisCloudConfigError as exc:
|
|
107
|
+
raise AlysisLoginError(str(exc)) from exc
|
|
108
|
+
|
|
109
|
+
grant = _request_device_code()
|
|
110
|
+
|
|
111
|
+
writer(f"Your one-time code: [bold]{grant.user_code}[/bold]")
|
|
112
|
+
writer(f"Approve it in your browser:\n {grant.verification_url}")
|
|
113
|
+
opener = browser_opener or _open_browser
|
|
114
|
+
if not _safe_open(opener, grant.verification_url):
|
|
115
|
+
writer("Could not open a browser automatically. Open the URL above manually.")
|
|
116
|
+
writer("Waiting for approval…")
|
|
117
|
+
|
|
118
|
+
key = _poll_for_key(grant, timeout_s=timeout_s)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
# Create + activate the profile first (this saves config); only then
|
|
122
|
+
# persist the key, so a save failure never leaves a stored key pointing
|
|
123
|
+
# at an unconfigured profile. resolve_api_key then returns the gateway
|
|
124
|
+
# key as the Bearer for the `alysis` profile (no other wiring).
|
|
125
|
+
result = _activate_alysis_profile(cfg, email=None)
|
|
126
|
+
save_persisted_profile_key(cloud.PROFILE_KEY, key)
|
|
127
|
+
except OSError as exc:
|
|
128
|
+
raise AlysisLoginError(f"Logged in, but couldn't save your session locally: {exc}") from exc
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def logout(cfg: AppConfig) -> bool:
|
|
133
|
+
"""Revoke the gateway key server-side and forget it locally.
|
|
134
|
+
|
|
135
|
+
The server revocation is what actually ends access: any client still
|
|
136
|
+
holding the key in memory (including the current chat session) gets a
|
|
137
|
+
401 on its next request. Best-effort — offline logout still clears the
|
|
138
|
+
local key, and the orphaned server key can be revoked from the website
|
|
139
|
+
later. Returns True if a stored key was cleared.
|
|
140
|
+
"""
|
|
141
|
+
stored = load_persisted_profile_keys().get(cloud.PROFILE_KEY)
|
|
142
|
+
|
|
143
|
+
if stored:
|
|
144
|
+
try:
|
|
145
|
+
request = urllib.request.Request(
|
|
146
|
+
f"{cloud.gateway_base_url()}/logout",
|
|
147
|
+
data=b"{}",
|
|
148
|
+
headers={
|
|
149
|
+
"Authorization": f"Bearer {stored}",
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
method="POST",
|
|
153
|
+
)
|
|
154
|
+
urllib.request.urlopen(request, timeout=_HTTP_TIMEOUT_S).close()
|
|
155
|
+
except Exception: # noqa: BLE001 - revocation is best-effort
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
return clear_persisted_profile_key(cloud.PROFILE_KEY)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def login_status(cfg: AppConfig) -> LoginStatus:
|
|
162
|
+
"""Report whether an Alysis Code Pro session is connected."""
|
|
163
|
+
stored = load_persisted_profile_keys().get(cloud.PROFILE_KEY)
|
|
164
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "").strip()
|
|
165
|
+
profile = get_profile(cfg, cloud.PROFILE_KEY)
|
|
166
|
+
base_url = profile.base_url if profile is not None else cloud.gateway_base_url()
|
|
167
|
+
preview = None
|
|
168
|
+
if stored:
|
|
169
|
+
preview = stored[:8] + "…" if len(stored) > 8 else stored
|
|
170
|
+
return LoginStatus(
|
|
171
|
+
logged_in=bool(stored),
|
|
172
|
+
profile_name=cloud.PROFILE_KEY,
|
|
173
|
+
base_url=base_url,
|
|
174
|
+
active=active == cloud.PROFILE_KEY,
|
|
175
|
+
key_preview=preview,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def fetch_trial_status(cfg: AppConfig) -> TrialStatus | None:
|
|
180
|
+
"""Fetch live account status. Currently returns None (no status endpoint).
|
|
181
|
+
|
|
182
|
+
Plan status, credit balance, and usage live on the product site's /account
|
|
183
|
+
page (see ``alysis_cloud.account_url``). A CLI-visible status endpoint is planned;
|
|
184
|
+
until then this stays best-effort-None so callers degrade exactly like the
|
|
185
|
+
old offline path — `whoami` still shows local connection state.
|
|
186
|
+
"""
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def list_trial_models(cfg: AppConfig) -> list[str]:
|
|
191
|
+
"""List the model ids the gateway currently serves, via ``/v1/models``.
|
|
192
|
+
|
|
193
|
+
The gateway exposes its allowlist as an OpenAI-shaped
|
|
194
|
+
``{"data": [{"id": …}]}``. Surfacing it lets `/config` show the live model
|
|
195
|
+
list instead of pinning static ids. Best-effort: returns ``[]`` on any
|
|
196
|
+
failure (offline, non-200, malformed) so callers fall back to the static
|
|
197
|
+
preset models. The stored gateway key is sent when present (the gateway may
|
|
198
|
+
require it) and never required for the call to be attempted.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
url = cloud.models_url()
|
|
202
|
+
except cloud.AlysisCloudConfigError:
|
|
203
|
+
return []
|
|
204
|
+
headers = {}
|
|
205
|
+
access_key = load_persisted_profile_keys().get(cloud.PROFILE_KEY)
|
|
206
|
+
if access_key:
|
|
207
|
+
headers["Authorization"] = f"Bearer {access_key}"
|
|
208
|
+
request = urllib.request.Request(url, headers=headers, method="GET")
|
|
209
|
+
try:
|
|
210
|
+
with urllib.request.urlopen(request, timeout=_MODELS_TIMEOUT_S) as response:
|
|
211
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
212
|
+
except (urllib.error.URLError, TimeoutError, OSError, ValueError, UnicodeDecodeError):
|
|
213
|
+
return []
|
|
214
|
+
data = body.get("data") if isinstance(body, dict) else None
|
|
215
|
+
if not isinstance(data, list):
|
|
216
|
+
return []
|
|
217
|
+
models: list[str] = []
|
|
218
|
+
for item in data:
|
|
219
|
+
if not isinstance(item, dict):
|
|
220
|
+
continue
|
|
221
|
+
model_id = str(item.get("id") or "").strip()
|
|
222
|
+
if model_id and model_id not in models:
|
|
223
|
+
models.append(model_id)
|
|
224
|
+
return models
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def format_trial_status_line(status: TrialStatus) -> str | None:
|
|
228
|
+
"""A one-line account summary for `whoami`, or None if there's nothing to show."""
|
|
229
|
+
parts: list[str] = []
|
|
230
|
+
days = _days_left(status.trial_ends_at)
|
|
231
|
+
if days is not None:
|
|
232
|
+
parts.append("expired" if days <= 0 else f"{days} day{'s' if days != 1 else ''} left")
|
|
233
|
+
ends = _format_date(status.trial_ends_at)
|
|
234
|
+
if ends:
|
|
235
|
+
parts.append(f"ends {ends}")
|
|
236
|
+
tokens = _format_token_usage(status.tokens_used, status.tokens_total)
|
|
237
|
+
if tokens:
|
|
238
|
+
parts.append(tokens)
|
|
239
|
+
if not parts:
|
|
240
|
+
return None
|
|
241
|
+
label = (status.plan or "Account").capitalize()
|
|
242
|
+
return f"{label}: " + " · ".join(parts)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# Device flow internals
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
@dataclass(frozen=True)
|
|
249
|
+
class _DeviceGrant:
|
|
250
|
+
device_code: str
|
|
251
|
+
user_code: str
|
|
252
|
+
verification_url: str
|
|
253
|
+
interval_s: float
|
|
254
|
+
expires_in_s: float
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _function_headers() -> dict[str, str]:
|
|
258
|
+
# Supabase routes function calls with the public anon key; the device
|
|
259
|
+
# endpoints themselves are public by design (verify_jwt off server-side).
|
|
260
|
+
return {
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
"apikey": cloud.ANON_KEY,
|
|
263
|
+
"Authorization": f"Bearer {cloud.ANON_KEY}",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _post_json(url: str, payload: dict) -> dict:
|
|
268
|
+
request = urllib.request.Request(
|
|
269
|
+
url,
|
|
270
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
271
|
+
headers=_function_headers(),
|
|
272
|
+
method="POST",
|
|
273
|
+
)
|
|
274
|
+
try:
|
|
275
|
+
with urllib.request.urlopen(request, timeout=_HTTP_TIMEOUT_S) as response:
|
|
276
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
277
|
+
except urllib.error.HTTPError as exc:
|
|
278
|
+
raise AlysisLoginError(_http_error_message(exc)) from exc
|
|
279
|
+
except urllib.error.URLError as exc:
|
|
280
|
+
raise AlysisLoginError(
|
|
281
|
+
f"Could not reach the Alysis Code login service: {exc.reason}"
|
|
282
|
+
) from exc
|
|
283
|
+
except (ValueError, UnicodeDecodeError) as exc:
|
|
284
|
+
raise AlysisLoginError("Login service returned an invalid response.") from exc
|
|
285
|
+
except (TimeoutError, OSError) as exc:
|
|
286
|
+
raise AlysisLoginError(f"Login request failed: {exc}") from exc
|
|
287
|
+
if not isinstance(body, dict):
|
|
288
|
+
raise AlysisLoginError("Login service returned an invalid response.")
|
|
289
|
+
return body
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _http_error_message(exc: urllib.error.HTTPError) -> str:
|
|
293
|
+
try:
|
|
294
|
+
detail = json.loads(exc.read().decode("utf-8"))
|
|
295
|
+
message = detail.get("error")
|
|
296
|
+
if isinstance(message, dict):
|
|
297
|
+
message = message.get("message")
|
|
298
|
+
if message:
|
|
299
|
+
return str(message)
|
|
300
|
+
except Exception: # noqa: BLE001 - best-effort error extraction
|
|
301
|
+
pass
|
|
302
|
+
return f"Login request failed (HTTP {exc.code})."
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _request_device_code() -> _DeviceGrant:
|
|
306
|
+
client_name = f"alysis-cli @ {platform.node() or 'unknown-host'}"[:80]
|
|
307
|
+
body = _post_json(cloud.device_code_url(), {"client_name": client_name})
|
|
308
|
+
|
|
309
|
+
device_code = str(body.get("device_code") or "").strip()
|
|
310
|
+
user_code = str(body.get("user_code") or "").strip()
|
|
311
|
+
if not device_code or not user_code:
|
|
312
|
+
raise AlysisLoginError("Login service did not return a device code.")
|
|
313
|
+
|
|
314
|
+
verification_url = str(
|
|
315
|
+
body.get("verification_url_complete") or body.get("verification_url") or ""
|
|
316
|
+
).strip()
|
|
317
|
+
if not verification_url:
|
|
318
|
+
verification_url = f"{cloud.activate_url()}?code={user_code}"
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
interval_s = max(float(body.get("interval", _DEFAULT_POLL_INTERVAL_S)), 0.0)
|
|
322
|
+
except (TypeError, ValueError):
|
|
323
|
+
interval_s = _DEFAULT_POLL_INTERVAL_S
|
|
324
|
+
try:
|
|
325
|
+
expires_in_s = max(float(body.get("expires_in", _DEFAULT_TIMEOUT_S)), 1.0)
|
|
326
|
+
except (TypeError, ValueError):
|
|
327
|
+
expires_in_s = _DEFAULT_TIMEOUT_S
|
|
328
|
+
|
|
329
|
+
return _DeviceGrant(
|
|
330
|
+
device_code=device_code,
|
|
331
|
+
user_code=user_code,
|
|
332
|
+
verification_url=verification_url,
|
|
333
|
+
interval_s=interval_s,
|
|
334
|
+
expires_in_s=expires_in_s,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _poll_for_key(grant: _DeviceGrant, *, timeout_s: float) -> str:
|
|
339
|
+
deadline = time.monotonic() + max(1.0, min(timeout_s, grant.expires_in_s))
|
|
340
|
+
while True:
|
|
341
|
+
body = _post_json(cloud.device_token_url(), {"device_code": grant.device_code})
|
|
342
|
+
status = str(body.get("status") or "").strip().lower()
|
|
343
|
+
|
|
344
|
+
if status == "approved":
|
|
345
|
+
key = str(body.get("key") or "").strip()
|
|
346
|
+
if not key:
|
|
347
|
+
raise AlysisLoginError("Login service did not return a key.")
|
|
348
|
+
return key
|
|
349
|
+
if status == "denied":
|
|
350
|
+
raise AlysisLoginError("Login was rejected on the website.")
|
|
351
|
+
if status in {"expired", "not_found", "already_claimed"}:
|
|
352
|
+
raise AlysisLoginError("This login code expired. Run `alysis login` again.")
|
|
353
|
+
# status == "pending" (or anything unknown): keep waiting.
|
|
354
|
+
|
|
355
|
+
remaining = deadline - time.monotonic()
|
|
356
|
+
if remaining <= 0:
|
|
357
|
+
raise AlysisLoginError(
|
|
358
|
+
f"Timed out after {int(timeout_s)}s waiting for approval. Run `alysis login` again."
|
|
359
|
+
)
|
|
360
|
+
time.sleep(min(grant.interval_s, max(remaining, 0.0)) if grant.interval_s else 0.0)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
# Profile + formatting internals
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
def _str_or_none(value: object) -> str | None:
|
|
367
|
+
text = str(value).strip() if value is not None else ""
|
|
368
|
+
return text or None
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _int_or_none(value: object) -> int | None:
|
|
372
|
+
try:
|
|
373
|
+
return int(value) # type: ignore[arg-type]
|
|
374
|
+
except (TypeError, ValueError):
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _parse_iso(value: str | None) -> datetime | None:
|
|
379
|
+
if not value:
|
|
380
|
+
return None
|
|
381
|
+
try:
|
|
382
|
+
dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
383
|
+
except ValueError:
|
|
384
|
+
return None
|
|
385
|
+
return dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _days_left(ends_at: str | None) -> int | None:
|
|
389
|
+
ends = _parse_iso(ends_at)
|
|
390
|
+
if ends is None:
|
|
391
|
+
return None
|
|
392
|
+
seconds = (ends - datetime.now(UTC)).total_seconds()
|
|
393
|
+
return 0 if seconds <= 0 else math.ceil(seconds / 86400)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _format_date(value: str | None) -> str | None:
|
|
397
|
+
dt = _parse_iso(value)
|
|
398
|
+
return dt.date().isoformat() if dt is not None else None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _format_token_usage(used: int | None, total: int | None) -> str | None:
|
|
402
|
+
if used is None and total is None:
|
|
403
|
+
return None
|
|
404
|
+
if total:
|
|
405
|
+
return f"{used or 0:,} / {total:,} tokens used"
|
|
406
|
+
return f"{used or 0:,} tokens used"
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _activate_alysis_profile(cfg: AppConfig, *, email: str | None) -> LoginResult:
|
|
410
|
+
"""Create + activate the `alysis` profile, keeping any model the user chose.
|
|
411
|
+
|
|
412
|
+
Fresh logins default to the Pro flagship (``deepseek-v4-flash``) so
|
|
413
|
+
subscribe → login → chat works with zero extra steps. Re-logins preserve
|
|
414
|
+
whatever model the user selected since, so logging in again never undoes
|
|
415
|
+
their choice. Legacy MiMo ids (the retired Xiaomi trial) are remapped to
|
|
416
|
+
the current default via the preset's model_aliases at config load.
|
|
417
|
+
"""
|
|
418
|
+
preset = get_preset(cloud.PROFILE_KEY)
|
|
419
|
+
if preset is not None:
|
|
420
|
+
profile = make_profile_from_preset(preset, name=cloud.PROFILE_KEY)
|
|
421
|
+
else: # pragma: no cover - preset is always present
|
|
422
|
+
profile = ProfileSpec(name=cloud.PROFILE_KEY, protocol="openai_compat")
|
|
423
|
+
|
|
424
|
+
existing = get_profile(cfg, cloud.PROFILE_KEY)
|
|
425
|
+
existing_model = str(getattr(existing, "default_model", "") or "").strip() if existing else ""
|
|
426
|
+
chosen_model = existing_model or _FRESH_LOGIN_DEFAULT_MODEL
|
|
427
|
+
|
|
428
|
+
# Always pin to the live gateway URL (env-overridable for tests); keep the
|
|
429
|
+
# user's chosen model.
|
|
430
|
+
profile = ProfileSpec(
|
|
431
|
+
name=profile.name,
|
|
432
|
+
protocol=profile.protocol,
|
|
433
|
+
base_url=cloud.gateway_base_url(),
|
|
434
|
+
api_key_env=None,
|
|
435
|
+
extra_headers=dict(profile.extra_headers),
|
|
436
|
+
default_model=chosen_model,
|
|
437
|
+
reasoning_effort=(existing.reasoning_effort if existing is not None else None),
|
|
438
|
+
reasoning_trace_adapter=(
|
|
439
|
+
existing.reasoning_trace_adapter
|
|
440
|
+
if existing is not None
|
|
441
|
+
else profile.reasoning_trace_adapter
|
|
442
|
+
),
|
|
443
|
+
web_search_adapter=profile.web_search_adapter,
|
|
444
|
+
web_search_model=profile.web_search_model,
|
|
445
|
+
notes=profile.notes,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
add_profile(cfg, profile)
|
|
449
|
+
set_active_profile(cfg, profile.name)
|
|
450
|
+
save_config(cfg)
|
|
451
|
+
|
|
452
|
+
return LoginResult(
|
|
453
|
+
email=email,
|
|
454
|
+
profile_name=profile.name,
|
|
455
|
+
base_url=profile.base_url,
|
|
456
|
+
model=profile.default_model,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _open_browser(url: str) -> bool:
|
|
461
|
+
return open_url(url)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _safe_open(opener: Callable[[str], bool], url: str) -> bool:
|
|
465
|
+
try:
|
|
466
|
+
return bool(opener(url))
|
|
467
|
+
except Exception: # noqa: BLE001 - browser launch is best-effort
|
|
468
|
+
return False
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Agent Runtime
|
|
2
|
+
|
|
3
|
+
This package contains the core runtime for interactive chat, one-shot runs, and
|
|
4
|
+
managed execution flows.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- `session.py` builds session state from configuration, workspace context, and
|
|
9
|
+
runtime kind.
|
|
10
|
+
- `tools_assembly.py` builds the concrete tool surface for a session.
|
|
11
|
+
- `turn.py` runs the turn loop and tool-call iteration.
|
|
12
|
+
- `routing.py` handles routing and final response shaping.
|
|
13
|
+
- `prompt_context.py` prepares workspace, convention, skill, plugin, and
|
|
14
|
+
verification context.
|
|
15
|
+
- `verification.py` and `verification_commands.py` support verification flows.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
Tool exposure is not implied by imports. It depends on runtime kind, execution
|
|
20
|
+
mode, workspace policy, configuration, and readiness checks.
|
|
21
|
+
|
|
22
|
+
Repository files, web content, MCP output, OCR text, custom tool output, and
|
|
23
|
+
model output should remain treated as untrusted unless a higher layer has
|
|
24
|
+
explicitly established otherwise.
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
Prompt shape, tool ordering, and result summaries can affect behavior. Keep
|
|
29
|
+
changes narrow and test the runtime path being changed.
|
|
30
|
+
|
|
31
|
+
## See Also
|
|
32
|
+
|
|
33
|
+
- [Architecture](../../../docs/architecture.md)
|
|
34
|
+
- [Security model](../../../docs/security_model.md)
|
|
35
|
+
- [Forge](../../../docs/forge.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
_T = TypeVar("_T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _patchable(name: str, default: _T) -> _T:
|
|
10
|
+
mod = sys.modules.get("alysis_code.agent_loop")
|
|
11
|
+
return getattr(mod, name, default) if mod is not None else default
|