attune-ai 2.0.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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Hook Configuration Models
|
|
2
|
+
|
|
3
|
+
Pydantic models for hook system configuration.
|
|
4
|
+
Based on everything-claude-code hooks.json pattern.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart-AI-Memory
|
|
7
|
+
Licensed under Fair Source License 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HookEvent(str, Enum):
|
|
17
|
+
"""Hook event types matching Claude Code lifecycle."""
|
|
18
|
+
|
|
19
|
+
PRE_TOOL_USE = "PreToolUse"
|
|
20
|
+
POST_TOOL_USE = "PostToolUse"
|
|
21
|
+
SESSION_START = "SessionStart"
|
|
22
|
+
SESSION_END = "SessionEnd"
|
|
23
|
+
PRE_COMPACT = "PreCompact"
|
|
24
|
+
POST_COMPACT = "PostCompact"
|
|
25
|
+
PRE_COMMAND = "PreCommand"
|
|
26
|
+
POST_COMMAND = "PostCommand"
|
|
27
|
+
STOP = "Stop"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HookType(str, Enum):
|
|
31
|
+
"""Type of hook action."""
|
|
32
|
+
|
|
33
|
+
COMMAND = "command" # Run a command/script
|
|
34
|
+
PYTHON = "python" # Run a Python function
|
|
35
|
+
WEBHOOK = "webhook" # Call a webhook URL
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HookDefinition(BaseModel):
|
|
39
|
+
"""Definition of a single hook action.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
hook = HookDefinition(
|
|
43
|
+
type=HookType.PYTHON,
|
|
44
|
+
command="attune_llm.hooks.scripts.session_start:main",
|
|
45
|
+
description="Load previous context on session start"
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
type: HookType = Field(
|
|
50
|
+
default=HookType.PYTHON,
|
|
51
|
+
description="Type of hook action",
|
|
52
|
+
)
|
|
53
|
+
command: str = Field(
|
|
54
|
+
...,
|
|
55
|
+
description="Command to run, Python module:function path, or webhook URL",
|
|
56
|
+
)
|
|
57
|
+
description: str = Field(
|
|
58
|
+
default="",
|
|
59
|
+
description="Human-readable description of what this hook does",
|
|
60
|
+
)
|
|
61
|
+
timeout: int = Field(
|
|
62
|
+
default=30,
|
|
63
|
+
ge=1,
|
|
64
|
+
le=300,
|
|
65
|
+
description="Timeout in seconds for hook execution",
|
|
66
|
+
)
|
|
67
|
+
async_execution: bool = Field(
|
|
68
|
+
default=False,
|
|
69
|
+
description="Run hook asynchronously (don't wait for completion)",
|
|
70
|
+
)
|
|
71
|
+
on_error: str = Field(
|
|
72
|
+
default="log",
|
|
73
|
+
description="Error handling: 'log', 'raise', or 'ignore'",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HookMatcher(BaseModel):
|
|
78
|
+
"""Matcher for determining when a hook should fire.
|
|
79
|
+
|
|
80
|
+
Supports tool name matching, file path patterns, and custom conditions.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
matcher = HookMatcher(
|
|
84
|
+
tool="Edit",
|
|
85
|
+
file_pattern=r"\\.(ts|tsx|js|jsx)$",
|
|
86
|
+
description="Match TypeScript/JavaScript file edits"
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
tool: str | None = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description="Tool name to match (e.g., 'Bash', 'Edit', 'Read')",
|
|
93
|
+
)
|
|
94
|
+
file_pattern: str | None = Field(
|
|
95
|
+
default=None,
|
|
96
|
+
description="Regex pattern to match file paths",
|
|
97
|
+
)
|
|
98
|
+
command_pattern: str | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
description="Regex pattern to match command strings",
|
|
101
|
+
)
|
|
102
|
+
condition: str | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
description="Custom condition expression",
|
|
105
|
+
)
|
|
106
|
+
match_all: bool = Field(
|
|
107
|
+
default=False,
|
|
108
|
+
description="If True, matches all events (wildcard)",
|
|
109
|
+
)
|
|
110
|
+
description: str = Field(
|
|
111
|
+
default="",
|
|
112
|
+
description="Human-readable description of match criteria",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def matches(self, context: dict[str, Any]) -> bool:
|
|
116
|
+
"""Check if this matcher matches the given context.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
context: Event context with keys like 'tool', 'file_path', 'command'
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
True if matcher matches the context
|
|
123
|
+
|
|
124
|
+
"""
|
|
125
|
+
import re
|
|
126
|
+
|
|
127
|
+
# Wildcard matches everything
|
|
128
|
+
if self.match_all:
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
# Tool name matching
|
|
132
|
+
if self.tool and context.get("tool") != self.tool:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
# File path pattern matching
|
|
136
|
+
if self.file_pattern:
|
|
137
|
+
file_path = context.get("file_path", "")
|
|
138
|
+
if not re.search(self.file_pattern, file_path):
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# Command pattern matching
|
|
142
|
+
if self.command_pattern:
|
|
143
|
+
command = context.get("command", "")
|
|
144
|
+
if not re.search(self.command_pattern, command):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class HookRule(BaseModel):
|
|
151
|
+
"""A complete hook rule with matcher and actions.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
rule = HookRule(
|
|
155
|
+
matcher=HookMatcher(tool="Edit", file_pattern=r"\\.py$"),
|
|
156
|
+
hooks=[HookDefinition(type=HookType.PYTHON, command="format_python")],
|
|
157
|
+
description="Auto-format Python files after edits"
|
|
158
|
+
)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
matcher: HookMatcher = Field(
|
|
162
|
+
default_factory=lambda: HookMatcher(match_all=True),
|
|
163
|
+
description="Conditions for when this rule fires",
|
|
164
|
+
)
|
|
165
|
+
hooks: list[HookDefinition] = Field(
|
|
166
|
+
default_factory=list,
|
|
167
|
+
description="List of hooks to execute when matched",
|
|
168
|
+
)
|
|
169
|
+
enabled: bool = Field(
|
|
170
|
+
default=True,
|
|
171
|
+
description="Whether this rule is active",
|
|
172
|
+
)
|
|
173
|
+
priority: int = Field(
|
|
174
|
+
default=0,
|
|
175
|
+
description="Execution priority (higher = earlier)",
|
|
176
|
+
)
|
|
177
|
+
description: str = Field(
|
|
178
|
+
default="",
|
|
179
|
+
description="Human-readable description",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class HookConfig(BaseModel):
|
|
184
|
+
"""Complete hook configuration for an Empathy session.
|
|
185
|
+
|
|
186
|
+
Example YAML configuration:
|
|
187
|
+
hooks:
|
|
188
|
+
SessionStart:
|
|
189
|
+
- matcher:
|
|
190
|
+
match_all: true
|
|
191
|
+
hooks:
|
|
192
|
+
- type: python
|
|
193
|
+
command: attune_llm.hooks.scripts.session_start:main
|
|
194
|
+
description: Load previous context
|
|
195
|
+
PostToolUse:
|
|
196
|
+
- matcher:
|
|
197
|
+
tool: Edit
|
|
198
|
+
file_pattern: "\\.(py)$"
|
|
199
|
+
hooks:
|
|
200
|
+
- type: command
|
|
201
|
+
command: "ruff format {file_path}"
|
|
202
|
+
description: Auto-format Python files
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
hooks: dict[str, list[HookRule]] = Field(
|
|
206
|
+
default_factory=lambda: {event.value: [] for event in HookEvent},
|
|
207
|
+
description="Hooks organized by event type",
|
|
208
|
+
)
|
|
209
|
+
enabled: bool = Field(
|
|
210
|
+
default=True,
|
|
211
|
+
description="Global enable/disable for all hooks",
|
|
212
|
+
)
|
|
213
|
+
log_executions: bool = Field(
|
|
214
|
+
default=True,
|
|
215
|
+
description="Log hook executions for debugging",
|
|
216
|
+
)
|
|
217
|
+
default_timeout: int = Field(
|
|
218
|
+
default=30,
|
|
219
|
+
ge=1,
|
|
220
|
+
le=300,
|
|
221
|
+
description="Default timeout for hooks without explicit timeout",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def get_hooks_for_event(self, event: HookEvent) -> list[HookRule]:
|
|
225
|
+
"""Get all hook rules for a specific event type.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
event: The hook event type
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
List of HookRule objects for this event, sorted by priority
|
|
232
|
+
|
|
233
|
+
"""
|
|
234
|
+
rules = self.hooks.get(event.value, [])
|
|
235
|
+
return sorted(
|
|
236
|
+
[r for r in rules if r.enabled],
|
|
237
|
+
key=lambda r: -r.priority,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def add_hook(
|
|
241
|
+
self,
|
|
242
|
+
event: HookEvent,
|
|
243
|
+
hook: HookDefinition,
|
|
244
|
+
matcher: HookMatcher | None = None,
|
|
245
|
+
priority: int = 0,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Add a hook for an event.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
event: Event type to hook
|
|
251
|
+
hook: Hook definition to add
|
|
252
|
+
matcher: Optional matcher (defaults to match_all)
|
|
253
|
+
priority: Execution priority
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
if event.value not in self.hooks:
|
|
257
|
+
self.hooks[event.value] = []
|
|
258
|
+
|
|
259
|
+
rule = HookRule(
|
|
260
|
+
matcher=matcher or HookMatcher(match_all=True),
|
|
261
|
+
hooks=[hook],
|
|
262
|
+
priority=priority,
|
|
263
|
+
)
|
|
264
|
+
self.hooks[event.value].append(rule)
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def from_yaml(cls, yaml_path: str) -> "HookConfig":
|
|
268
|
+
"""Load hook configuration from a YAML file.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
yaml_path: Path to YAML configuration file
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
HookConfig instance
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
from pathlib import Path
|
|
278
|
+
|
|
279
|
+
import yaml
|
|
280
|
+
|
|
281
|
+
config_file = Path(yaml_path)
|
|
282
|
+
if not config_file.exists():
|
|
283
|
+
return cls()
|
|
284
|
+
|
|
285
|
+
with open(config_file) as f:
|
|
286
|
+
data = yaml.safe_load(f) or {}
|
|
287
|
+
|
|
288
|
+
hooks_data = data.get("hooks", {})
|
|
289
|
+
return cls.model_validate({"hooks": hooks_data, **data})
|
|
290
|
+
|
|
291
|
+
def to_yaml(self, yaml_path: str) -> None:
|
|
292
|
+
"""Save hook configuration to a YAML file.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
yaml_path: Path to write YAML configuration
|
|
296
|
+
|
|
297
|
+
"""
|
|
298
|
+
from pathlib import Path
|
|
299
|
+
|
|
300
|
+
import yaml
|
|
301
|
+
|
|
302
|
+
config_file = Path(yaml_path)
|
|
303
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
with open(config_file, "w") as f:
|
|
306
|
+
yaml.dump(self.model_dump(), f, default_flow_style=False)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Hook Executor
|
|
2
|
+
|
|
3
|
+
Executes hook actions (commands, Python functions, webhooks).
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from attune_llm.hooks.config import HookDefinition, HookType
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HookExecutor:
|
|
22
|
+
"""Executor for running hook actions.
|
|
23
|
+
|
|
24
|
+
Supports three hook types:
|
|
25
|
+
- COMMAND: Run shell commands with variable substitution
|
|
26
|
+
- PYTHON: Import and call Python functions
|
|
27
|
+
- WEBHOOK: POST to webhook URLs
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
executor = HookExecutor()
|
|
31
|
+
|
|
32
|
+
# Execute a command hook
|
|
33
|
+
result = await executor.execute(
|
|
34
|
+
HookDefinition(type=HookType.COMMAND, command="echo {file_path}"),
|
|
35
|
+
context={"file_path": "/path/to/file.py"}
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, python_handlers: dict[str, Callable] | None = None):
|
|
40
|
+
"""Initialize the executor.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
python_handlers: Map of handler IDs to Python callables
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
self._python_handlers = python_handlers or {}
|
|
47
|
+
|
|
48
|
+
async def execute(
|
|
49
|
+
self,
|
|
50
|
+
hook: HookDefinition,
|
|
51
|
+
context: dict[str, Any],
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Execute a hook action.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
hook: Hook definition to execute
|
|
57
|
+
context: Execution context with variables
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Execution result dictionary
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
if hook.async_execution:
|
|
67
|
+
# Fire and forget
|
|
68
|
+
asyncio.create_task(self._execute_internal(hook, context))
|
|
69
|
+
return {
|
|
70
|
+
"success": True,
|
|
71
|
+
"output": "Hook scheduled for async execution",
|
|
72
|
+
"async": True,
|
|
73
|
+
"duration_ms": 0,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
result = await asyncio.wait_for(
|
|
77
|
+
self._execute_internal(hook, context),
|
|
78
|
+
timeout=hook.timeout,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
82
|
+
return {
|
|
83
|
+
"success": True,
|
|
84
|
+
"output": result,
|
|
85
|
+
"duration_ms": round(duration_ms, 2),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
except asyncio.TimeoutError:
|
|
89
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
90
|
+
error_msg = f"Hook timed out after {hook.timeout}s"
|
|
91
|
+
logger.warning("%s: %s", error_msg, hook.command)
|
|
92
|
+
return {
|
|
93
|
+
"success": False,
|
|
94
|
+
"error": error_msg,
|
|
95
|
+
"duration_ms": round(duration_ms, 2),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
100
|
+
logger.error("Hook execution failed: %s - %s", hook.command, e)
|
|
101
|
+
return {
|
|
102
|
+
"success": False,
|
|
103
|
+
"error": str(e),
|
|
104
|
+
"duration_ms": round(duration_ms, 2),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async def _execute_internal(
|
|
108
|
+
self,
|
|
109
|
+
hook: HookDefinition,
|
|
110
|
+
context: dict[str, Any],
|
|
111
|
+
) -> Any:
|
|
112
|
+
"""Internal execution logic.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
hook: Hook definition
|
|
116
|
+
context: Execution context
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Hook output
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
if hook.type == HookType.COMMAND:
|
|
123
|
+
return await self._execute_command(hook.command, context)
|
|
124
|
+
elif hook.type == HookType.PYTHON:
|
|
125
|
+
return await self._execute_python(hook.command, context)
|
|
126
|
+
elif hook.type == HookType.WEBHOOK:
|
|
127
|
+
return await self._execute_webhook(hook.command, context)
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(f"Unknown hook type: {hook.type}")
|
|
130
|
+
|
|
131
|
+
async def _execute_command(
|
|
132
|
+
self,
|
|
133
|
+
command: str,
|
|
134
|
+
context: dict[str, Any],
|
|
135
|
+
) -> str:
|
|
136
|
+
"""Execute a shell command.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
command: Command string with optional {var} placeholders
|
|
140
|
+
context: Variables for substitution
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Command output
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
# Substitute context variables
|
|
147
|
+
try:
|
|
148
|
+
formatted_command = command.format(**context)
|
|
149
|
+
except KeyError as e:
|
|
150
|
+
raise ValueError(f"Missing context variable for command: {e}")
|
|
151
|
+
|
|
152
|
+
logger.debug("Executing command: %s", formatted_command)
|
|
153
|
+
|
|
154
|
+
# Run command asynchronously
|
|
155
|
+
process = await asyncio.create_subprocess_shell(
|
|
156
|
+
formatted_command,
|
|
157
|
+
stdout=asyncio.subprocess.PIPE,
|
|
158
|
+
stderr=asyncio.subprocess.PIPE,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
stdout, stderr = await process.communicate()
|
|
162
|
+
|
|
163
|
+
if process.returncode != 0:
|
|
164
|
+
error_output = stderr.decode().strip() or stdout.decode().strip()
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"Command failed with exit code {process.returncode}: {error_output}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return stdout.decode().strip()
|
|
170
|
+
|
|
171
|
+
async def _execute_python(
|
|
172
|
+
self,
|
|
173
|
+
command: str,
|
|
174
|
+
context: dict[str, Any],
|
|
175
|
+
) -> Any:
|
|
176
|
+
"""Execute a Python function.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
command: Either a handler ID or module.path:function format
|
|
180
|
+
context: Context passed as kwargs to function
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Function return value
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
# Check if it's a registered handler ID
|
|
187
|
+
if command in self._python_handlers:
|
|
188
|
+
handler = self._python_handlers[command]
|
|
189
|
+
return await self._call_handler(handler, context)
|
|
190
|
+
|
|
191
|
+
# Otherwise, import module:function
|
|
192
|
+
if ":" not in command:
|
|
193
|
+
raise ValueError(f"Python hook must be 'module.path:function' format: {command}")
|
|
194
|
+
|
|
195
|
+
module_path, func_name = command.rsplit(":", 1)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
module = importlib.import_module(module_path)
|
|
199
|
+
handler = getattr(module, func_name)
|
|
200
|
+
except (ImportError, AttributeError) as e:
|
|
201
|
+
raise ValueError(f"Failed to import hook function: {command}") from e
|
|
202
|
+
|
|
203
|
+
return await self._call_handler(handler, context)
|
|
204
|
+
|
|
205
|
+
async def _call_handler(
|
|
206
|
+
self,
|
|
207
|
+
handler: Callable,
|
|
208
|
+
context: dict[str, Any],
|
|
209
|
+
) -> Any:
|
|
210
|
+
"""Call a handler function (sync or async).
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
handler: Callable to invoke
|
|
214
|
+
context: Context passed as kwargs
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Handler return value
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
if asyncio.iscoroutinefunction(handler):
|
|
221
|
+
return await handler(**context)
|
|
222
|
+
else:
|
|
223
|
+
# Run sync function in thread pool
|
|
224
|
+
loop = asyncio.get_event_loop()
|
|
225
|
+
return await loop.run_in_executor(None, lambda: handler(**context))
|
|
226
|
+
|
|
227
|
+
async def _execute_webhook(
|
|
228
|
+
self,
|
|
229
|
+
url: str,
|
|
230
|
+
context: dict[str, Any],
|
|
231
|
+
) -> dict[str, Any]:
|
|
232
|
+
"""Execute a webhook call.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
url: Webhook URL
|
|
236
|
+
context: JSON payload to send
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Response data
|
|
240
|
+
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
import aiohttp
|
|
244
|
+
except ImportError:
|
|
245
|
+
raise ImportError("aiohttp required for webhook hooks: pip install aiohttp")
|
|
246
|
+
|
|
247
|
+
logger.debug("Calling webhook: %s", url)
|
|
248
|
+
|
|
249
|
+
async with aiohttp.ClientSession() as session:
|
|
250
|
+
async with session.post(
|
|
251
|
+
url,
|
|
252
|
+
json=context,
|
|
253
|
+
headers={"Content-Type": "application/json"},
|
|
254
|
+
) as response:
|
|
255
|
+
if response.status >= 400:
|
|
256
|
+
text = await response.text()
|
|
257
|
+
raise RuntimeError(f"Webhook failed with status {response.status}: {text}")
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
return await response.json()
|
|
261
|
+
except Exception:
|
|
262
|
+
return {"status": response.status, "text": await response.text()}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class HookExecutorSync:
|
|
266
|
+
"""Synchronous wrapper for HookExecutor.
|
|
267
|
+
|
|
268
|
+
For use in contexts where async is not available.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(self, python_handlers: dict[str, Callable] | None = None):
|
|
272
|
+
self._executor = HookExecutor(python_handlers)
|
|
273
|
+
|
|
274
|
+
def execute(
|
|
275
|
+
self,
|
|
276
|
+
hook: HookDefinition,
|
|
277
|
+
context: dict[str, Any],
|
|
278
|
+
) -> dict[str, Any]:
|
|
279
|
+
"""Execute a hook synchronously.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
hook: Hook definition
|
|
283
|
+
context: Execution context
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Execution result
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
return asyncio.run(self._executor.execute(hook, context))
|