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,293 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import zipfile
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
from time import strftime
|
|
10
|
+
|
|
11
|
+
from .paths import global_managed_skill_root, project_managed_skill_root, skill_bundle_dir_name
|
|
12
|
+
from .state import (
|
|
13
|
+
ManagedSkillRecord,
|
|
14
|
+
SkillLifecycleError,
|
|
15
|
+
load_global_skill_state,
|
|
16
|
+
load_project_skill_state,
|
|
17
|
+
resolve_managed_bundle_path,
|
|
18
|
+
save_global_skill_state,
|
|
19
|
+
save_project_skill_state,
|
|
20
|
+
with_managed_install,
|
|
21
|
+
without_managed_install,
|
|
22
|
+
)
|
|
23
|
+
from .transactions import commit_managed_bundle_removal, commit_managed_bundle_update
|
|
24
|
+
from .validation import SkillValidationResult, validate_skill_bundle
|
|
25
|
+
|
|
26
|
+
MAX_SKILL_ZIP_FILES = 256
|
|
27
|
+
MAX_SKILL_ZIP_UNCOMPRESSED_BYTES = 8 * 1024 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SkillInstallResult:
|
|
32
|
+
bundle_path: Path
|
|
33
|
+
installed_name: str
|
|
34
|
+
source_kind: str
|
|
35
|
+
source_commit: str | None = None
|
|
36
|
+
validation: SkillValidationResult | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class SkillRemoveResult:
|
|
41
|
+
bundle_path: Path | None
|
|
42
|
+
removed_name: str
|
|
43
|
+
scope: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def install_skill_bundle(
|
|
47
|
+
*,
|
|
48
|
+
source: str,
|
|
49
|
+
workspace_root: Path,
|
|
50
|
+
project: bool = False,
|
|
51
|
+
subdir: str | None = None,
|
|
52
|
+
force: bool = False,
|
|
53
|
+
user_config_dir: Path | None = None,
|
|
54
|
+
) -> SkillInstallResult:
|
|
55
|
+
with tempfile.TemporaryDirectory(prefix="alysis-skill-install-") as temp_dir:
|
|
56
|
+
temp_root = Path(temp_dir)
|
|
57
|
+
source_kind, source_root, source_commit = _materialize_install_source(
|
|
58
|
+
source=source,
|
|
59
|
+
temp_root=temp_root,
|
|
60
|
+
)
|
|
61
|
+
bundle = _resolve_install_bundle(source_root=source_root, subdir=subdir)
|
|
62
|
+
validation = validate_skill_bundle(bundle)
|
|
63
|
+
if not validation.valid:
|
|
64
|
+
raise SkillLifecycleError(_format_validation_failure(validation))
|
|
65
|
+
target_root = (
|
|
66
|
+
project_managed_skill_root(workspace_root)
|
|
67
|
+
if project
|
|
68
|
+
else global_managed_skill_root(user_config_dir=user_config_dir)
|
|
69
|
+
)
|
|
70
|
+
bundle_dir = skill_bundle_dir_name(validation.name)
|
|
71
|
+
target_path = resolve_managed_bundle_path(managed_root=target_root, bundle_dir=bundle_dir)
|
|
72
|
+
record = ManagedSkillRecord(
|
|
73
|
+
name=validation.name,
|
|
74
|
+
bundle_dir=bundle_dir,
|
|
75
|
+
scope=("project" if project else "user"),
|
|
76
|
+
source_kind=source_kind,
|
|
77
|
+
source=source,
|
|
78
|
+
source_subdir=(str(subdir).strip() or None),
|
|
79
|
+
source_commit=source_commit,
|
|
80
|
+
installed_at=strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
81
|
+
)
|
|
82
|
+
if project:
|
|
83
|
+
state = with_managed_install(load_project_skill_state(workspace_root), record)
|
|
84
|
+
commit_managed_bundle_update(
|
|
85
|
+
target_path=target_path,
|
|
86
|
+
force=force,
|
|
87
|
+
stage_bundle=lambda staged: shutil.copytree(bundle, staged),
|
|
88
|
+
persist_state=lambda: save_project_skill_state(workspace_root, state),
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
state = with_managed_install(
|
|
92
|
+
load_global_skill_state(user_config_dir=user_config_dir),
|
|
93
|
+
record,
|
|
94
|
+
)
|
|
95
|
+
commit_managed_bundle_update(
|
|
96
|
+
target_path=target_path,
|
|
97
|
+
force=force,
|
|
98
|
+
stage_bundle=lambda staged: shutil.copytree(bundle, staged),
|
|
99
|
+
persist_state=lambda: save_global_skill_state(
|
|
100
|
+
state,
|
|
101
|
+
user_config_dir=user_config_dir,
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
return SkillInstallResult(
|
|
105
|
+
bundle_path=target_path,
|
|
106
|
+
installed_name=validation.name,
|
|
107
|
+
source_kind=source_kind,
|
|
108
|
+
source_commit=source_commit,
|
|
109
|
+
validation=validation,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def remove_managed_skill(
|
|
114
|
+
*,
|
|
115
|
+
name: str,
|
|
116
|
+
workspace_root: Path,
|
|
117
|
+
project: bool = False,
|
|
118
|
+
user_config_dir: Path | None = None,
|
|
119
|
+
) -> SkillRemoveResult:
|
|
120
|
+
key = str(name or "").strip().casefold()
|
|
121
|
+
if not key:
|
|
122
|
+
raise SkillLifecycleError("Skill name cannot be empty.")
|
|
123
|
+
if project:
|
|
124
|
+
state = load_project_skill_state(workspace_root)
|
|
125
|
+
record = state.managed_installs.get(key)
|
|
126
|
+
if record is None:
|
|
127
|
+
raise SkillLifecycleError(f"Managed project skill not found: {name}")
|
|
128
|
+
bundle_path = resolve_managed_bundle_path(
|
|
129
|
+
managed_root=project_managed_skill_root(workspace_root),
|
|
130
|
+
bundle_dir=record.bundle_dir,
|
|
131
|
+
)
|
|
132
|
+
commit_managed_bundle_removal(
|
|
133
|
+
target_path=bundle_path,
|
|
134
|
+
persist_state=lambda: save_project_skill_state(
|
|
135
|
+
workspace_root,
|
|
136
|
+
without_managed_install(state, name),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
return SkillRemoveResult(bundle_path=bundle_path, removed_name=record.name, scope="project")
|
|
140
|
+
state = load_global_skill_state(user_config_dir=user_config_dir)
|
|
141
|
+
record = state.managed_installs.get(key)
|
|
142
|
+
if record is None:
|
|
143
|
+
raise SkillLifecycleError(f"Managed user skill not found: {name}")
|
|
144
|
+
bundle_path = resolve_managed_bundle_path(
|
|
145
|
+
managed_root=global_managed_skill_root(user_config_dir=user_config_dir),
|
|
146
|
+
bundle_dir=record.bundle_dir,
|
|
147
|
+
)
|
|
148
|
+
commit_managed_bundle_removal(
|
|
149
|
+
target_path=bundle_path,
|
|
150
|
+
persist_state=lambda: save_global_skill_state(
|
|
151
|
+
without_managed_install(state, name),
|
|
152
|
+
user_config_dir=user_config_dir,
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
return SkillRemoveResult(bundle_path=bundle_path, removed_name=record.name, scope="user")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _materialize_install_source(
|
|
159
|
+
*,
|
|
160
|
+
source: str,
|
|
161
|
+
temp_root: Path,
|
|
162
|
+
) -> tuple[str, Path, str | None]:
|
|
163
|
+
raw_source = str(source or "").strip()
|
|
164
|
+
if not raw_source:
|
|
165
|
+
raise SkillLifecycleError("Install source cannot be empty.")
|
|
166
|
+
source_path = Path(raw_source).expanduser()
|
|
167
|
+
if source_path.exists() and source_path.is_dir():
|
|
168
|
+
return "dir", source_path.resolve(), None
|
|
169
|
+
if source_path.exists() and source_path.is_file() and source_path.suffix.casefold() == ".zip":
|
|
170
|
+
extract_root = temp_root / "zip"
|
|
171
|
+
extract_root.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
_extract_skill_zip(source_path.resolve(), extract_root)
|
|
173
|
+
return "zip", extract_root, None
|
|
174
|
+
return _clone_git_source(raw_source, temp_root / "git")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _resolve_install_bundle(*, source_root: Path, subdir: str | None) -> Path:
|
|
178
|
+
root = source_root.resolve()
|
|
179
|
+
if subdir:
|
|
180
|
+
candidate = (root / Path(subdir)).resolve()
|
|
181
|
+
try:
|
|
182
|
+
candidate.relative_to(root)
|
|
183
|
+
except ValueError as exc:
|
|
184
|
+
raise SkillLifecycleError("Install subdir escapes the source root.") from exc
|
|
185
|
+
root = candidate
|
|
186
|
+
if (root / "SKILL.md").is_file():
|
|
187
|
+
return root
|
|
188
|
+
candidates: list[Path] = []
|
|
189
|
+
for entry in root.rglob("SKILL.md"):
|
|
190
|
+
if ".git" in entry.parts:
|
|
191
|
+
continue
|
|
192
|
+
candidates.append(entry.parent)
|
|
193
|
+
if not candidates:
|
|
194
|
+
raise SkillLifecycleError("No skill bundle found in install source.")
|
|
195
|
+
if len(candidates) > 1:
|
|
196
|
+
raise SkillLifecycleError(
|
|
197
|
+
"Install source contains multiple skill bundles; use --subdir to choose one."
|
|
198
|
+
)
|
|
199
|
+
return candidates[0]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _extract_skill_zip(zip_path: Path, target_root: Path) -> None:
|
|
203
|
+
try:
|
|
204
|
+
archive = zipfile.ZipFile(zip_path)
|
|
205
|
+
except OSError as exc:
|
|
206
|
+
raise SkillLifecycleError(f"Failed to open zip archive: {zip_path}") from exc
|
|
207
|
+
with archive:
|
|
208
|
+
file_infos: list[tuple[zipfile.ZipInfo, list[str]]] = []
|
|
209
|
+
total_uncompressed = 0
|
|
210
|
+
for info in archive.infolist():
|
|
211
|
+
if _is_zip_symlink(info):
|
|
212
|
+
raise SkillLifecycleError(
|
|
213
|
+
"Zip archive contains symlink entries, which are not allowed."
|
|
214
|
+
)
|
|
215
|
+
if info.is_dir():
|
|
216
|
+
continue
|
|
217
|
+
parts = _sanitize_zip_parts(info.filename)
|
|
218
|
+
if not parts:
|
|
219
|
+
continue
|
|
220
|
+
file_infos.append((info, parts))
|
|
221
|
+
if len(file_infos) > MAX_SKILL_ZIP_FILES:
|
|
222
|
+
raise SkillLifecycleError(
|
|
223
|
+
f"Zip archive exceeds file-count limit ({MAX_SKILL_ZIP_FILES} files)."
|
|
224
|
+
)
|
|
225
|
+
total_uncompressed += max(0, int(info.file_size))
|
|
226
|
+
if total_uncompressed > MAX_SKILL_ZIP_UNCOMPRESSED_BYTES:
|
|
227
|
+
raise SkillLifecycleError(
|
|
228
|
+
"Zip archive exceeds uncompressed-size limit "
|
|
229
|
+
f"({MAX_SKILL_ZIP_UNCOMPRESSED_BYTES} bytes)."
|
|
230
|
+
)
|
|
231
|
+
for info, parts in file_infos:
|
|
232
|
+
destination = _resolve_zip_target(target_root, parts)
|
|
233
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
with archive.open(info, "r") as src, destination.open("wb") as dst:
|
|
235
|
+
shutil.copyfileobj(src, dst)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _clone_git_source(source: str, target_root: Path) -> tuple[str, Path, str | None]:
|
|
239
|
+
if shutil.which("git") is None:
|
|
240
|
+
raise SkillLifecycleError("git is required for git-based skill installs.")
|
|
241
|
+
try:
|
|
242
|
+
subprocess.run(
|
|
243
|
+
["git", "clone", "--depth", "1", source, str(target_root)],
|
|
244
|
+
check=True,
|
|
245
|
+
capture_output=True,
|
|
246
|
+
text=True,
|
|
247
|
+
)
|
|
248
|
+
except subprocess.CalledProcessError as exc:
|
|
249
|
+
stderr = str(exc.stderr or "").strip()
|
|
250
|
+
raise SkillLifecycleError(f"Failed to clone git skill source: {stderr or source}") from exc
|
|
251
|
+
commit = None
|
|
252
|
+
try:
|
|
253
|
+
completed = subprocess.run(
|
|
254
|
+
["git", "-C", str(target_root), "rev-parse", "HEAD"],
|
|
255
|
+
check=True,
|
|
256
|
+
capture_output=True,
|
|
257
|
+
text=True,
|
|
258
|
+
)
|
|
259
|
+
commit = str(completed.stdout or "").strip() or None
|
|
260
|
+
except subprocess.CalledProcessError:
|
|
261
|
+
commit = None
|
|
262
|
+
return "git", target_root, commit
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _is_zip_symlink(info: zipfile.ZipInfo) -> bool:
|
|
266
|
+
return bool((info.external_attr >> 16) & 0o170000 == 0o120000)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _sanitize_zip_parts(name: str) -> list[str]:
|
|
270
|
+
clean_name = str(name or "").replace("\\", "/")
|
|
271
|
+
path = PurePosixPath(clean_name)
|
|
272
|
+
parts = [part for part in path.parts if part not in {"", "."}]
|
|
273
|
+
if any(part == ".." for part in parts):
|
|
274
|
+
raise SkillLifecycleError("Zip archive entry escapes extraction root.")
|
|
275
|
+
if path.is_absolute():
|
|
276
|
+
raise SkillLifecycleError("Zip archive contains absolute paths, which are not allowed.")
|
|
277
|
+
return parts
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _resolve_zip_target(base: Path, rel_parts: list[str]) -> Path:
|
|
281
|
+
target = base.joinpath(*rel_parts).resolve()
|
|
282
|
+
try:
|
|
283
|
+
target.relative_to(base.resolve())
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
raise SkillLifecycleError("Zip archive entry escapes extraction root.") from exc
|
|
286
|
+
return target
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _format_validation_failure(result: SkillValidationResult) -> str:
|
|
290
|
+
if not result.issues:
|
|
291
|
+
return f"Skill validation failed: {result.bundle_path}"
|
|
292
|
+
first = result.issues[0]
|
|
293
|
+
return f"Skill validation failed: {first.message} ({first.path})"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .models import SkillBundle, SkillDiscoveryIssue, SkillSourceKind, SkillSourceScope
|
|
7
|
+
from .validation import validate_skill_bundle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SkillReadError(RuntimeError):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_skill_bundle(
|
|
15
|
+
*,
|
|
16
|
+
bundle_path: Path,
|
|
17
|
+
source_scope: SkillSourceScope,
|
|
18
|
+
source_kind: SkillSourceKind,
|
|
19
|
+
source_family: str,
|
|
20
|
+
ancestor_distance: int | None,
|
|
21
|
+
) -> tuple[SkillBundle | None, SkillDiscoveryIssue | None]:
|
|
22
|
+
validation = validate_skill_bundle(bundle_path)
|
|
23
|
+
if not validation.valid:
|
|
24
|
+
first_error = next(iter(validation.errors))
|
|
25
|
+
return None, SkillDiscoveryIssue(
|
|
26
|
+
source_path=bundle_path,
|
|
27
|
+
message=first_error.message,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
bundle_name = bundle_path.name.strip() or validation.name
|
|
31
|
+
aliases = _ordered_unique_strings(
|
|
32
|
+
[validation.name, bundle_name, *_metadata_aliases(validation.metadata)]
|
|
33
|
+
)
|
|
34
|
+
return (
|
|
35
|
+
SkillBundle(
|
|
36
|
+
name=validation.name,
|
|
37
|
+
description=validation.description,
|
|
38
|
+
instructions=validation.instructions,
|
|
39
|
+
bundle_name=bundle_name,
|
|
40
|
+
bundle_path=bundle_path,
|
|
41
|
+
entry_path=validation.entry_path,
|
|
42
|
+
source_scope=source_scope,
|
|
43
|
+
source_kind=source_kind,
|
|
44
|
+
source_family=source_family,
|
|
45
|
+
source_path=bundle_path,
|
|
46
|
+
trust_level="untrusted",
|
|
47
|
+
ancestor_distance=ancestor_distance,
|
|
48
|
+
aliases=tuple(aliases),
|
|
49
|
+
metadata={
|
|
50
|
+
key: str(value)
|
|
51
|
+
for key, value in validation.metadata.items()
|
|
52
|
+
if key in {"name", "description"} and str(value).strip()
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
None,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_skill_bundle_file(skill: SkillBundle, path: str | None = None) -> dict[str, Any]:
|
|
60
|
+
requested_path = str(path or "").strip()
|
|
61
|
+
if not requested_path:
|
|
62
|
+
target_path = skill.entry_path
|
|
63
|
+
relative_path = "SKILL.md"
|
|
64
|
+
else:
|
|
65
|
+
candidate = Path(requested_path)
|
|
66
|
+
if candidate.is_absolute():
|
|
67
|
+
raise SkillReadError("skill_read path must be relative to the skill bundle root")
|
|
68
|
+
target_path = (skill.bundle_path / candidate).resolve()
|
|
69
|
+
bundle_root = skill.bundle_path.resolve()
|
|
70
|
+
try:
|
|
71
|
+
target_path.relative_to(bundle_root)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise SkillReadError("skill_read path escapes the skill bundle root") from exc
|
|
74
|
+
try:
|
|
75
|
+
relative_path = target_path.relative_to(bundle_root).as_posix()
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
raise SkillReadError("skill_read path escapes the skill bundle root") from exc
|
|
78
|
+
if not target_path.exists() or not target_path.is_file():
|
|
79
|
+
raise SkillReadError(f"Skill file not found: {requested_path or 'SKILL.md'}")
|
|
80
|
+
try:
|
|
81
|
+
content = target_path.read_text(encoding="utf-8")
|
|
82
|
+
except UnicodeDecodeError as exc:
|
|
83
|
+
raise SkillReadError(f"Skill file is not readable as UTF-8 text: {relative_path}") from exc
|
|
84
|
+
except OSError as exc:
|
|
85
|
+
raise SkillReadError(f"Failed to read skill file: {relative_path}") from exc
|
|
86
|
+
return {
|
|
87
|
+
"name": skill.name,
|
|
88
|
+
"bundle_name": skill.bundle_name,
|
|
89
|
+
"path": relative_path,
|
|
90
|
+
"content": content,
|
|
91
|
+
"source_scope": skill.source_scope,
|
|
92
|
+
"source_kind": skill.source_kind,
|
|
93
|
+
"source_family": skill.source_family,
|
|
94
|
+
"source_path": skill.source_path.as_posix(),
|
|
95
|
+
"trust_level": skill.trust_level,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _ordered_unique_strings(values: list[str]) -> list[str]:
|
|
100
|
+
out: list[str] = []
|
|
101
|
+
seen: set[str] = set()
|
|
102
|
+
for raw in values:
|
|
103
|
+
value = str(raw or "").strip()
|
|
104
|
+
if not value:
|
|
105
|
+
continue
|
|
106
|
+
lowered = value.casefold()
|
|
107
|
+
if lowered in seen:
|
|
108
|
+
continue
|
|
109
|
+
seen.add(lowered)
|
|
110
|
+
out.append(value)
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _metadata_aliases(metadata: dict[str, Any]) -> list[str]:
|
|
115
|
+
raw = metadata.get("aliases")
|
|
116
|
+
if isinstance(raw, list):
|
|
117
|
+
return [str(item).strip() for item in raw if str(item).strip()]
|
|
118
|
+
return []
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .models import SkillBundle, SkillMatch
|
|
6
|
+
|
|
7
|
+
_TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_./+-]*", re.IGNORECASE)
|
|
8
|
+
_STOPWORDS = {
|
|
9
|
+
"a",
|
|
10
|
+
"an",
|
|
11
|
+
"and",
|
|
12
|
+
"build",
|
|
13
|
+
"do",
|
|
14
|
+
"for",
|
|
15
|
+
"help",
|
|
16
|
+
"i",
|
|
17
|
+
"in",
|
|
18
|
+
"is",
|
|
19
|
+
"it",
|
|
20
|
+
"me",
|
|
21
|
+
"my",
|
|
22
|
+
"of",
|
|
23
|
+
"on",
|
|
24
|
+
"please",
|
|
25
|
+
"the",
|
|
26
|
+
"this",
|
|
27
|
+
"to",
|
|
28
|
+
"we",
|
|
29
|
+
"with",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def match_skills(
|
|
34
|
+
request: str,
|
|
35
|
+
*,
|
|
36
|
+
skills: list[SkillBundle] | tuple[SkillBundle, ...],
|
|
37
|
+
max_results: int = 5,
|
|
38
|
+
) -> tuple[SkillMatch, ...]:
|
|
39
|
+
request_text = str(request or "").strip()
|
|
40
|
+
if not request_text:
|
|
41
|
+
return ()
|
|
42
|
+
request_tokens = _tokenize(request_text)
|
|
43
|
+
if not request_tokens:
|
|
44
|
+
return ()
|
|
45
|
+
matches: list[SkillMatch] = []
|
|
46
|
+
request_lower = request_text.casefold()
|
|
47
|
+
for skill in skills:
|
|
48
|
+
name_tokens = _tokenize(skill.name)
|
|
49
|
+
description_tokens = _tokenize(skill.description)
|
|
50
|
+
score = 0
|
|
51
|
+
matched_terms: list[str] = []
|
|
52
|
+
if skill.name.casefold() in request_lower or skill.bundle_name.casefold() in request_lower:
|
|
53
|
+
score += 12
|
|
54
|
+
matched_terms.append(skill.name)
|
|
55
|
+
overlap = sorted(request_tokens & name_tokens)
|
|
56
|
+
if overlap:
|
|
57
|
+
score += len(overlap) * 5
|
|
58
|
+
matched_terms.extend(overlap)
|
|
59
|
+
desc_overlap = sorted(request_tokens & description_tokens)
|
|
60
|
+
if desc_overlap:
|
|
61
|
+
score += len(desc_overlap) * 2
|
|
62
|
+
matched_terms.extend(desc_overlap)
|
|
63
|
+
if score <= 0:
|
|
64
|
+
continue
|
|
65
|
+
matches.append(
|
|
66
|
+
SkillMatch(
|
|
67
|
+
skill=skill,
|
|
68
|
+
score=score,
|
|
69
|
+
matched_terms=tuple(_ordered_unique(matched_terms)),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
matches.sort(
|
|
73
|
+
key=lambda item: (
|
|
74
|
+
-item.score,
|
|
75
|
+
item.skill.name.casefold(),
|
|
76
|
+
item.skill.bundle_path.as_posix(),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return tuple(matches[: max(0, max_results)])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _tokenize(text: str) -> set[str]:
|
|
83
|
+
tokens = {
|
|
84
|
+
token.casefold()
|
|
85
|
+
for token in _TOKEN_RE.findall(str(text or ""))
|
|
86
|
+
if token and token.casefold() not in _STOPWORDS
|
|
87
|
+
}
|
|
88
|
+
return {token for token in tokens if len(token) > 1}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _ordered_unique(values: list[str]) -> list[str]:
|
|
92
|
+
out: list[str] = []
|
|
93
|
+
seen: set[str] = set()
|
|
94
|
+
for raw in values:
|
|
95
|
+
value = str(raw or "").strip()
|
|
96
|
+
if not value:
|
|
97
|
+
continue
|
|
98
|
+
lowered = value.casefold()
|
|
99
|
+
if lowered in seen:
|
|
100
|
+
continue
|
|
101
|
+
seen.add(lowered)
|
|
102
|
+
out.append(value)
|
|
103
|
+
return out
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
SkillSourceScope = Literal["project", "user"]
|
|
8
|
+
SkillSourceKind = Literal["native", "interop"]
|
|
9
|
+
SkillTrustLevel = Literal["untrusted"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SkillBundle:
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
instructions: str
|
|
17
|
+
bundle_name: str
|
|
18
|
+
bundle_path: Path
|
|
19
|
+
entry_path: Path
|
|
20
|
+
source_scope: SkillSourceScope
|
|
21
|
+
source_kind: SkillSourceKind
|
|
22
|
+
source_family: str
|
|
23
|
+
source_path: Path
|
|
24
|
+
trust_level: SkillTrustLevel
|
|
25
|
+
ancestor_distance: int | None = None
|
|
26
|
+
aliases: tuple[str, ...] = ()
|
|
27
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def source_label(self) -> str:
|
|
31
|
+
return f"{self.source_scope}/{self.source_kind}"
|
|
32
|
+
|
|
33
|
+
def lookup_keys(self) -> tuple[str, ...]:
|
|
34
|
+
keys: list[str] = []
|
|
35
|
+
for value in (self.name, self.bundle_name, *self.aliases):
|
|
36
|
+
candidate = str(value or "").strip()
|
|
37
|
+
if not candidate:
|
|
38
|
+
continue
|
|
39
|
+
lowered = candidate.casefold()
|
|
40
|
+
if lowered in keys:
|
|
41
|
+
continue
|
|
42
|
+
keys.append(lowered)
|
|
43
|
+
return tuple(keys)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class SkillDiscoveryIssue:
|
|
48
|
+
source_path: Path
|
|
49
|
+
message: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class DiscoveredSkills:
|
|
54
|
+
skills: dict[str, SkillBundle]
|
|
55
|
+
ordered: tuple[SkillBundle, ...]
|
|
56
|
+
issues: tuple[SkillDiscoveryIssue, ...] = ()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class SkillMatch:
|
|
61
|
+
skill: SkillBundle
|
|
62
|
+
score: int
|
|
63
|
+
matched_terms: tuple[str, ...] = ()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class ConventionDocument:
|
|
68
|
+
name: str
|
|
69
|
+
path: Path
|
|
70
|
+
content: str
|
|
71
|
+
trust_level: SkillTrustLevel = "untrusted"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from ..branding import canonical_user_config_dir
|
|
8
|
+
|
|
9
|
+
ProjectSkillFamily = Literal["native", "agents", "claude", "github"]
|
|
10
|
+
|
|
11
|
+
_PROJECT_FAMILY_ROOTS: dict[ProjectSkillFamily, tuple[str, ...]] = {
|
|
12
|
+
"native": (".alysis_skills",),
|
|
13
|
+
"agents": (".agents", "skills"),
|
|
14
|
+
"claude": (".claude", "skills"),
|
|
15
|
+
"github": (".github", "skills"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def global_managed_skill_root(*, user_config_dir: Path | None = None) -> Path:
|
|
20
|
+
base = (
|
|
21
|
+
user_config_dir.expanduser().resolve() if user_config_dir else canonical_user_config_dir()
|
|
22
|
+
)
|
|
23
|
+
return base / "skills"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def global_skills_state_path(*, user_config_dir: Path | None = None) -> Path:
|
|
27
|
+
base = (
|
|
28
|
+
user_config_dir.expanduser().resolve() if user_config_dir else canonical_user_config_dir()
|
|
29
|
+
)
|
|
30
|
+
return base / "skills.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def project_managed_skill_root(workspace_root: Path) -> Path:
|
|
34
|
+
return workspace_root.expanduser().resolve() / ".alysis_skills"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def project_skills_state_path(workspace_root: Path) -> Path:
|
|
38
|
+
return workspace_root.expanduser().resolve() / ".alysis" / "skills.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def project_skill_root_for_family(workspace_root: Path, *, family: ProjectSkillFamily) -> Path:
|
|
42
|
+
return workspace_root.expanduser().resolve().joinpath(*_PROJECT_FAMILY_ROOTS[family])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def normalize_project_skill_family(raw: str | None) -> ProjectSkillFamily:
|
|
46
|
+
normalized = str(raw or "native").strip().lower()
|
|
47
|
+
if normalized in _PROJECT_FAMILY_ROOTS:
|
|
48
|
+
return normalized # type: ignore[return-value]
|
|
49
|
+
raise ValueError("Unsupported skill family. Expected one of: native, agents, claude, github.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def skill_bundle_dir_name(raw_name: str) -> str:
|
|
53
|
+
candidate = re.sub(r"[^a-z0-9._-]+", "-", str(raw_name or "").strip().casefold()).strip("-")
|
|
54
|
+
if not candidate:
|
|
55
|
+
raise ValueError("Skill name must contain at least one letter or digit.")
|
|
56
|
+
return candidate
|