ha-mcp-dev 7.14.2.dev2122__tar.gz → 7.14.2.dev2126__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {ha_mcp_dev-7.14.2.dev2122/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2126}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_addons.py +89 -9
  4. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_calendar.py +92 -14
  5. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  6. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/LICENSE +0 -0
  7. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/MANIFEST.in +0 -0
  8. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/README.md +0 -0
  9. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/setup.cfg +0 -0
  10. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/__init__.py +0 -0
  11. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/__main__.py +0 -0
  12. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/_pypi_marker +0 -0
  13. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/_version.py +0 -0
  14. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/auth/__init__.py +0 -0
  15. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/auth/consent_form.py +0 -0
  16. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/auth/provider.py +0 -0
  17. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/backup_manager.py +0 -0
  18. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/browser_landing.py +0 -0
  19. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/client/__init__.py +0 -0
  20. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/client/rest_client.py +0 -0
  21. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/client/supervisor_client.py +0 -0
  22. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/client/websocket_client.py +0 -0
  23. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/client/websocket_listener.py +0 -0
  24. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/config.py +0 -0
  25. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  26. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  27. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  28. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  29. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  30. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  31. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/errors.py +0 -0
  32. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/llm_exposure.py +0 -0
  33. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/__init__.py +0 -0
  34. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/approval_queue.py +0 -0
  35. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/evaluator.py +0 -0
  36. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/handlers.py +0 -0
  37. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/middleware.py +0 -0
  38. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/model.py +0 -0
  39. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/persistence.py +0 -0
  40. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/policy/value_sources.py +0 -0
  41. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/py.typed +0 -0
  42. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/read_only.py +0 -0
  43. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  44. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  45. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  46. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  47. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  48. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  49. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  50. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  51. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  52. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  53. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  54. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  55. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  56. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  57. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  58. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  59. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  60. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  61. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  62. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  63. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  64. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  65. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  66. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  67. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  68. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/server.py +0 -0
  69. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/__init__.py +0 -0
  70. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  71. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  72. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  73. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  74. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  75. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  76. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  77. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  78. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  79. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_theme.py +0 -0
  80. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  81. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  82. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  83. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  84. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  85. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
  86. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/settings.css +0 -0
  87. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/settings.html +0 -0
  88. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/settings_ui/settings.js +0 -0
  89. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/smoke_test.py +0 -0
  90. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  91. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/strict_bps.py +0 -0
  92. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/__init__.py +0 -0
  93. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/auto_backup.py +0 -0
  94. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/backup.py +0 -0
  95. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  96. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/component_api.py +0 -0
  97. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/component_config_reads.py +0 -0
  98. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/component_devices.py +0 -0
  99. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/component_registries.py +0 -0
  100. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  101. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  102. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/device_control.py +0 -0
  103. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/enhanced.py +0 -0
  104. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/helpers.py +0 -0
  105. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/__init__.py +0 -0
  106. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/base.py +0 -0
  107. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/matter.py +0 -0
  108. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/thread.py +0 -0
  109. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  110. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/radio/zwave.py +0 -0
  111. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/reference_validator.py +0 -0
  112. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/registry.py +0 -0
  113. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  114. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  115. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  116. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  117. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  118. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  119. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  120. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  121. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  122. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  123. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_areas.py +0 -0
  124. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  125. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  126. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_camera.py +0 -0
  127. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_categories.py +0 -0
  128. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_code.py +0 -0
  129. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  130. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  131. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  132. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  133. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  134. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  135. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_dev.py +0 -0
  136. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_energy.py +0 -0
  137. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_entities.py +0 -0
  138. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  139. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_groups.py +0 -0
  140. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_hacs.py +0 -0
  141. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_history.py +0 -0
  142. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_integrations.py +0 -0
  143. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_labels.py +0 -0
  144. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_radio.py +0 -0
  145. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_registry.py +0 -0
  146. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_resources.py +0 -0
  147. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_search.py +0 -0
  148. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_service.py +0 -0
  149. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_services.py +0 -0
  150. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_system.py +0 -0
  151. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_themes.py +0 -0
  152. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_todo.py +0 -0
  153. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_traces.py +0 -0
  154. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_updates.py +0 -0
  155. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_utility.py +0 -0
  156. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  157. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  158. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  159. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/tools_zones.py +0 -0
  160. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/util_helpers.py +0 -0
  161. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/tools/validation_middleware.py +0 -0
  162. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/transforms/__init__.py +0 -0
  163. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/transforms/categorized_search.py +0 -0
  164. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/config_write_lock.py +0 -0
  170. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/data_paths.py +0 -0
  171. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/domain_handlers.py +0 -0
  172. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  173. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  174. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/operation_manager.py +0 -0
  175. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/python_sandbox.py +0 -0
  176. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/skill_loader.py +0 -0
  177. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/utils/usage_logger.py +0 -0
  178. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/visibility/__init__.py +0 -0
  179. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/visibility/enforcement.py +0 -0
  180. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/visibility/model.py +0 -0
  181. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/visibility/persistence.py +0 -0
  182. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp/visibility/resolver.py +0 -0
  183. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  184. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  185. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  186. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  187. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  188. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/tests/__init__.py +0 -0
  189. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/tests/test_constants.py +0 -0
  190. {ha_mcp_dev-7.14.2.dev2122 → ha_mcp_dev-7.14.2.dev2126}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.14.2.dev2122
3
+ Version: 7.14.2.dev2126
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.14.2.dev2122"
7
+ version = "7.14.2.dev2126"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -210,6 +210,19 @@ def _merge_options(base: dict, override: dict) -> dict:
210
210
  return merged
211
211
 
212
212
 
213
+ # Supervisor's per-job-group rejection, matched case-insensitively, plus the
214
+ # bounded window and backoff used to ride it out. See _supervisor_api_call.
215
+ # The "for job group" tail is load-bearing: JobGroup.acquire raises
216
+ # "Another job is running for job group <name>" for the transient per-add-on
217
+ # collision, while the job-level JobConcurrency.REJECT path raises a bare
218
+ # "Another job is running" for long operations (OS update, data-disk wipe)
219
+ # that must NOT be retried on this schedule.
220
+ _JOB_COLLISION_MARKER = "another job is running for job group"
221
+ _JOB_COLLISION_RETRY_WINDOW = 60.0
222
+ _JOB_COLLISION_RETRY_INITIAL_DELAY = 1.0
223
+ _JOB_COLLISION_RETRY_MAX_DELAY = 5.0
224
+
225
+
213
226
  async def _supervisor_api_call(
214
227
  client: HomeAssistantClient,
215
228
  endpoint: str,
@@ -228,8 +241,13 @@ async def _supervisor_api_call(
228
241
  data: Optional request body data
229
242
  timeout: Optional timeout override
230
243
 
244
+ A transient Supervisor job-group collision is retried with backoff, so a
245
+ contended add-on endpoint can block for up to
246
+ ``_JOB_COLLISION_RETRY_WINDOW`` before returning or raising.
247
+
231
248
  Returns:
232
- The "result" field from a successful response, or an error dict.
249
+ ``{"success": True, "result": ...}``. Every failure raises this
250
+ never returns an error dict.
233
251
  """
234
252
  try:
235
253
  kwargs: dict[str, Any] = {"endpoint": endpoint, "method": method}
@@ -252,16 +270,78 @@ async def _supervisor_api_call(
252
270
  # ``{"success": False, "error": ...}``; re-raise it as the same
253
271
  # ``HomeAssistantCommandError`` the dedicated send_command used to raise
254
272
  # so the classifier below maps schema/not-found/etc. identically.
255
- result = await client.send_websocket_message(
256
- {"type": "supervisor/api", "_wait_timeout": wait_timeout, **kwargs}
257
- )
273
+ #
274
+ # Supervisor serialises jobs per add-on job group and rejects a
275
+ # state-changing call outright while a still-settling job (a watchdog
276
+ # restart, a prior start/stop, a store reload) holds that group. The
277
+ # rejection happens before the job body runs, so nothing was applied
278
+ # and retrying cannot double-execute. The holder clears within seconds
279
+ # and the caller can do nothing useful with the failure, so ride it out
280
+ # inside one bounded window. The window is total rather than
281
+ # per-attempt, so a wedged group gives up once the window elapses
282
+ # instead of after some fixed retry count. Any other failure raises
283
+ # immediately, without retrying.
284
+ deadline = time.monotonic() + _JOB_COLLISION_RETRY_WINDOW
285
+ delay = _JOB_COLLISION_RETRY_INITIAL_DELAY
286
+ attempts = 0
287
+ while True:
288
+ attempts += 1
289
+ result = await client.send_websocket_message(
290
+ {"type": "supervisor/api", "_wait_timeout": wait_timeout, **kwargs}
291
+ )
292
+
293
+ if result.get("success"):
294
+ return {"success": True, "result": result.get("result", {})}
295
+
296
+ error_text = str(
297
+ result.get("error", f"Supervisor API call failed: {endpoint}")
298
+ )
299
+ if _JOB_COLLISION_MARKER not in error_text.lower():
300
+ raise HomeAssistantCommandError(error_text)
301
+
302
+ remaining = deadline - time.monotonic()
303
+ if remaining <= 0:
304
+ # A group still held after the whole window is wedged, not
305
+ # settling. Raise a ToolError here (the guard below re-raises
306
+ # it untouched) so the caller gets guidance about the stuck
307
+ # job instead of the generic connectivity suggestion the
308
+ # exception handler attaches to every other failure.
309
+ waited = _JOB_COLLISION_RETRY_WINDOW - remaining
310
+ logger.warning(
311
+ "Supervisor job group still busy on %s after %.0fs "
312
+ "(%d attempts); giving up: %s",
313
+ endpoint,
314
+ waited,
315
+ attempts,
316
+ error_text,
317
+ )
318
+ raise_tool_error(
319
+ create_error_response(
320
+ ErrorCode.SERVICE_CALL_FAILED,
321
+ error_text,
322
+ context={
323
+ "endpoint": endpoint,
324
+ "attempts": attempts,
325
+ "waited_seconds": round(waited, 1),
326
+ },
327
+ suggestions=[
328
+ "Another job has held this add-on's job group for "
329
+ f"over {_JOB_COLLISION_RETRY_WINDOW:.0f}s — check "
330
+ "Supervisor logs for a stuck or long-running job",
331
+ "Retry once the in-flight add-on operation "
332
+ "(install, update, restart or backup) finishes",
333
+ ],
334
+ )
335
+ )
258
336
 
259
- if not result.get("success"):
260
- raise HomeAssistantCommandError(
261
- str(result.get("error", f"Supervisor API call failed: {endpoint}"))
337
+ logger.info(
338
+ "Supervisor job-group collision on %s; retrying in %.1fs (%s)",
339
+ endpoint,
340
+ delay,
341
+ error_text,
262
342
  )
263
-
264
- return {"success": True, "result": result.get("result", {})}
343
+ await asyncio.sleep(min(delay, remaining))
344
+ delay = min(delay * 2, _JOB_COLLISION_RETRY_MAX_DELAY)
265
345
 
266
346
  except ToolError:
267
347
  raise
@@ -232,12 +232,16 @@ class CalendarTools:
232
232
  location: str | None,
233
233
  ) -> Any:
234
234
  """Create a one-off calendar event via the calendar.create_event service."""
235
+ start_is_date = self._is_date_only(start)
236
+
235
237
  service_data: dict[str, Any] = {
236
238
  "entity_id": entity_id,
237
239
  "summary": summary,
238
- "start_date_time": start,
239
- "end_date_time": end,
240
240
  }
241
+ if start_is_date:
242
+ service_data.update({"start_date": start, "end_date": end})
243
+ else:
244
+ service_data.update({"start_date_time": start, "end_date_time": end})
241
245
 
242
246
  if description is not None:
243
247
  service_data["description"] = description
@@ -246,8 +250,19 @@ class CalendarTools:
246
250
 
247
251
  return await self._client.call_service("calendar", "create_event", service_data)
248
252
 
253
+ @staticmethod
254
+ def _is_date_only(value: str) -> bool:
255
+ """Return whether value is a valid date in strict YYYY-MM-DD form."""
256
+ try:
257
+ return (
258
+ len(value) == 10
259
+ and datetime.strptime(value, "%Y-%m-%d").date().isoformat() == value
260
+ )
261
+ except ValueError:
262
+ return False
263
+
249
264
  def _build_set_calendar_event_error_suggestions(
250
- self, entity_id: str, rrule: str | None, error: Exception
265
+ self, entity_id: str, rrule: str | None, error: Exception, start: str, end: str
251
266
  ) -> list[str]:
252
267
  """Build suggestions for a failed ha_config_set_calendar_event call."""
253
268
  if isinstance(error, HomeAssistantConnectionError):
@@ -274,8 +289,32 @@ class CalendarTools:
274
289
  error_str = str(error)
275
290
  if "404" in error_str or "not found" in error_str.lower():
276
291
  suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
292
+ # Only reachable on the WebSocket (rrule) path, which preserves HA's
293
+ # humanized message. The REST service path loses it: a read-only
294
+ # calendar raises ServiceNotSupported, which HA's handler does not map
295
+ # (see homeassistant/helpers/http.py — only vol.Invalid, ServiceNotFound
296
+ # and Unauthorized are), so the client sees a bodyless status line. Do
297
+ # NOT broaden this to match that generic shape: it would promote
298
+ # "does not support event creation" onto every validation failure. The
299
+ # REST path keeps the read-only hint via the unconditional
300
+ # "Some calendar integrations may be read-only" suggestion above.
277
301
  if "not supported" in error_str.lower():
278
302
  suggestions.insert(0, "This calendar does not support event creation")
303
+ # HA treats the all-day end date as exclusive and enforces
304
+ # MIN_NEW_EVENT_DURATION (1 second), so an all-day range with
305
+ # end <= start is rejected. Key this on the boundaries we already hold
306
+ # rather than on HA's wording: the REST service path raises a bare
307
+ # HTTPBadRequest, so voluptuous' "Expected minimum event duration"
308
+ # text never reaches the client (only the WebSocket rrule path keeps
309
+ # it). Strict YYYY-MM-DD sorts lexicographically, so comparing the
310
+ # strings is an exact date comparison here.
311
+ if self._is_date_only(start) and self._is_date_only(end) and end <= start:
312
+ suggestions.insert(
313
+ 0,
314
+ "For an all-day event the end date is exclusive — set end to "
315
+ "start + 1 day (a single-day all-day event cannot have "
316
+ "start == end)",
317
+ )
279
318
 
280
319
  return suggestions
281
320
 
@@ -305,8 +344,19 @@ class CalendarTools:
305
344
  str, Field(description="Calendar entity ID (e.g., 'calendar.family')")
306
345
  ],
307
346
  summary: Annotated[str, Field(description="Event title/summary")],
308
- start: Annotated[str, Field(description="Event start datetime in ISO format")],
309
- end: Annotated[str, Field(description="Event end datetime in ISO format")],
347
+ start: Annotated[
348
+ str, Field(description="Event start date or datetime in ISO format")
349
+ ],
350
+ end: Annotated[
351
+ str,
352
+ Field(
353
+ description=(
354
+ "Event end date or datetime in ISO format. For all-day "
355
+ "events (date-only) the end date is exclusive; a "
356
+ "single-day all-day event needs end = start + 1 day."
357
+ )
358
+ ),
359
+ ],
310
360
  description: Annotated[
311
361
  str | None,
312
362
  Field(description="Optional event description", default=None),
@@ -334,14 +384,9 @@ class CalendarTools:
334
384
  when ``rrule`` is provided (the REST service schema does not accept
335
385
  recurrence rules).
336
386
 
337
- **Parameters:**
338
- - entity_id: Calendar entity ID (e.g., 'calendar.family')
339
- - summary: Event title/summary
340
- - start: Event start datetime in ISO format
341
- - end: Event end datetime in ISO format
342
- - description: Optional event description
343
- - location: Optional event location
344
- - rrule: Optional RFC 5545 recurrence rule (creates a recurring series)
387
+ **When NOT to use:**
388
+ - To retrieve calendar events, use ``ha_config_get_calendar_events``.
389
+ - To delete an event, use ``ha_config_remove_calendar_event``.
345
390
 
346
391
  **Example Usage:**
347
392
  ```python
@@ -361,9 +406,25 @@ class CalendarTools:
361
406
  end="2024-01-15T11:00:00",
362
407
  rrule="FREQ=WEEKLY;BYDAY=MO;COUNT=10"
363
408
  )
409
+
410
+ # Create an all-day event (date-only, no time component). The end
411
+ # date is EXCLUSIVE, so this spans 2026-07-04 through 2026-07-10.
412
+ result = ha_config_set_calendar_event(
413
+ "calendar.family",
414
+ summary="Vacation",
415
+ start="2026-07-04",
416
+ end="2026-07-11"
417
+ )
364
418
  ```
365
419
 
366
420
  **Note:**
421
+ Passing date-only values (``YYYY-MM-DD``) for both ``start`` and
422
+ ``end`` creates an all-day event; passing full ISO datetimes creates
423
+ a timed event. The two forms cannot be mixed — a date-only ``start``
424
+ with a datetime ``end`` (or vice versa) is rejected. Because the
425
+ all-day ``end`` date is exclusive, a single-day all-day event must
426
+ set ``end`` to ``start + 1 day``.
427
+
367
428
  Not every calendar integration supports event creation; recurring
368
429
  events additionally require the integration to support recurrence
369
430
  (the built-in Local Calendar does).
@@ -386,6 +447,23 @@ class CalendarTools:
386
447
  )
387
448
  )
388
449
 
450
+ # Reject mixed date/datetime up front so both the simple and the
451
+ # recurring (rrule) paths give the same clear validation error —
452
+ # the rrule branch does not route through
453
+ # ``_create_simple_calendar_event`` where this used to live.
454
+ if self._is_date_only(start) != self._is_date_only(end):
455
+ raise_tool_error(
456
+ create_error_response(
457
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
458
+ "Calendar event start and end must both be dates or both be datetimes",
459
+ context={"start": start, "end": end},
460
+ suggestions=[
461
+ "Use YYYY-MM-DD for both values to create an all-day event",
462
+ "Use ISO 8601 datetimes for both values to create a timed event",
463
+ ],
464
+ )
465
+ )
466
+
389
467
  if rrule:
390
468
  result = await self._create_recurring_calendar_event(
391
469
  entity_id, summary, start, end, description, location, rrule
@@ -416,7 +494,7 @@ class CalendarTools:
416
494
  logger.error(f"Failed to create calendar event in {entity_id}: {error}")
417
495
 
418
496
  suggestions = self._build_set_calendar_event_error_suggestions(
419
- entity_id, rrule, error
497
+ entity_id, rrule, error, start, end
420
498
  )
421
499
 
422
500
  exception_to_structured_error(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.14.2.dev2122
3
+ Version: 7.14.2.dev2126
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT