neoagent 2.3.1-beta.4 → 2.3.1-beta.41
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.
- package/.env.example +45 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +12 -5
- package/docs/hardware.md +1 -1
- package/docs/integrations.md +2 -3
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +99 -0
- package/flutter_app/lib/main_account_settings.dart +1250 -0
- package/flutter_app/lib/main_admin.dart +886 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_chat.dart +3352 -0
- package/flutter_app/lib/main_controller.dart +6781 -0
- package/flutter_app/lib/main_devices.dart +2301 -0
- package/flutter_app/lib/main_integrations.dart +1129 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3546 -0
- package/flutter_app/lib/main_navigation.dart +193 -0
- package/flutter_app/lib/main_operations.dart +4851 -0
- package/flutter_app/lib/main_recordings.dart +870 -0
- package/flutter_app/lib/main_runtime.dart +806 -0
- package/flutter_app/lib/main_settings.dart +2024 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +957 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/install_helpers.js +31 -0
- package/lib/manager.js +227 -6
- package/package.json +3 -1
- package/server/db/database.js +110 -0
- package/server/http/middleware.js +55 -2
- package/server/http/routes.js +1 -0
- package/server/index.js +3 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +74324 -73132
- package/server/routes/integrations.js +108 -1
- package/server/routes/memory.js +11 -2
- package/server/{http/routes → routes}/screenHistory.js +2 -2
- package/server/routes/settings.js +75 -2
- package/server/{http/routes → routes}/triggers.js +2 -2
- package/server/routes/wearable.js +67 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/toolSelector.js +14 -1
- package/server/services/ai/tools.js +77 -4
- package/server/services/desktop/screenRecorder.js +65 -9
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/figma/provider.js +1 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/google/provider.js +1 -0
- package/server/services/integrations/home_assistant/provider.js +325 -26
- package/server/services/integrations/manager.js +88 -12
- package/server/services/integrations/microsoft/provider.js +1 -0
- package/server/services/integrations/oauth_provider.js +25 -8
- package/server/services/integrations/provider_config_store.js +85 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/spotify/provider.js +1 -0
- package/server/services/integrations/trello/provider.js +842 -0
- package/server/services/manager.js +46 -1
- package/server/services/mcp/client.js +120 -23
- package/server/services/memory/manager.js +39 -2
- package/server/services/messaging/access_policy.js +10 -0
- package/server/services/messaging/manager.js +49 -0
- package/server/services/messaging/meshtastic.js +260 -0
- package/server/services/messaging/meshtastic_env.js +100 -0
- package/server/services/messaging/meshtastic_protocol.js +476 -0
- package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/openaiSpeech.js +6 -1
- package/server/services/voice/providers.js +52 -12
- package/server/services/voice/runtimeManager.js +136 -19
- package/server/services/voice/turnRunner.js +29 -9
- package/server/services/wearable/firmware_manifest.js +370 -0
- package/server/services/wearable/gateway.js +350 -0
- package/server/services/wearable/protocol.js +45 -0
- package/server/services/wearable/service.js +244 -0
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -300,6 +300,7 @@ function createGoogleWorkspaceProvider() {
|
|
|
300
300
|
description:
|
|
301
301
|
'Official Gmail, Calendar, Drive, Docs, and Sheets integrations with app-specific accounts.',
|
|
302
302
|
icon: 'google',
|
|
303
|
+
requiresRefreshToken: true,
|
|
303
304
|
apps: GOOGLE_WORKSPACE_APPS.map(({ id, label, description }) => ({
|
|
304
305
|
id,
|
|
305
306
|
label,
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const ipaddr = require('ipaddr.js');
|
|
6
|
+
const { resolveAgentId } = require('../../agents/manager');
|
|
4
7
|
const {
|
|
5
8
|
describeEnvStatus,
|
|
6
9
|
resolveHomeAssistantOAuthConfig,
|
|
7
10
|
} = require('../env');
|
|
11
|
+
const {
|
|
12
|
+
deleteProviderConfig,
|
|
13
|
+
getProviderConfig,
|
|
14
|
+
setProviderConfig,
|
|
15
|
+
} = require('../provider_config_store');
|
|
8
16
|
const {
|
|
9
17
|
appendQuery,
|
|
10
18
|
createOAuthProvider,
|
|
@@ -93,6 +101,253 @@ const homeAssistantToolDefinitions = [
|
|
|
93
101
|
},
|
|
94
102
|
];
|
|
95
103
|
|
|
104
|
+
function trimText(value) {
|
|
105
|
+
return String(value || '').trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isTruthyEnv(name) {
|
|
109
|
+
const value = String(process.env[name] || '').trim().toLowerCase();
|
|
110
|
+
return value === '1' || value === 'true' || value === 'yes' || value === 'on';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isLikelyLocalHostname(hostname) {
|
|
114
|
+
const host = String(hostname || '').trim().toLowerCase();
|
|
115
|
+
if (!host) return false;
|
|
116
|
+
if (host === 'localhost' || host === 'host.docker.internal') return true;
|
|
117
|
+
if (host.endsWith('.localhost')) return true;
|
|
118
|
+
if (host.endsWith('.local') || host.endsWith('.lan') || host.endsWith('.internal')) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPrivateIpv4(hostname) {
|
|
125
|
+
const parts = String(hostname || '').split('.').map((part) => Number(part));
|
|
126
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const [a, b] = parts;
|
|
130
|
+
if (a === 10) return true;
|
|
131
|
+
if (a === 127) return true;
|
|
132
|
+
if (a === 169 && b === 254) return true;
|
|
133
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
134
|
+
if (a === 192 && b === 168) return true;
|
|
135
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
136
|
+
if (a === 0) return true;
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isPrivateIpv6(hostname) {
|
|
141
|
+
const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
|
|
142
|
+
if (!host) return false;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = ipaddr.parse(host);
|
|
145
|
+
if (parsed.kind() !== 'ipv6') return false;
|
|
146
|
+
|
|
147
|
+
// Normalize IPv4-mapped IPv6 literals and enforce the same local/private rules.
|
|
148
|
+
if (parsed.isIPv4MappedAddress()) {
|
|
149
|
+
return isPrivateIpv4(parsed.toIPv4Address().toString());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const range = parsed.range();
|
|
153
|
+
return range === 'loopback' || range === 'uniqueLocal' || range === 'linkLocal' || range === 'unspecified';
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isPrivateOrLocalIp(hostname) {
|
|
160
|
+
const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
|
|
161
|
+
const kind = net.isIP(host);
|
|
162
|
+
if (kind === 4) return isPrivateIpv4(host);
|
|
163
|
+
if (kind === 6) return isPrivateIpv6(host);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateHomeAssistantBaseUrlSafety(parsedUrl) {
|
|
168
|
+
const allowPrivate = isTruthyEnv('HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL');
|
|
169
|
+
const host = String(parsedUrl.hostname || '').trim();
|
|
170
|
+
const localHostname = isLikelyLocalHostname(host);
|
|
171
|
+
const localIp = isPrivateOrLocalIp(host);
|
|
172
|
+
const isLocalTarget = localHostname || localIp;
|
|
173
|
+
|
|
174
|
+
if (isLocalTarget && !allowPrivate) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
'Home Assistant base URL cannot target localhost/private network addresses unless HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1 is set on the server.',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (parsedUrl.protocol === 'http:' && !isLocalTarget) {
|
|
181
|
+
throw new Error('Home Assistant base URL must use HTTPS for non-local hosts.');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeBaseUrl(value) {
|
|
186
|
+
const text = trimText(value);
|
|
187
|
+
if (!text) return '';
|
|
188
|
+
return text.replace(/\/$/, '');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeOptionalAbsoluteUrl(value, label) {
|
|
192
|
+
const text = trimText(value);
|
|
193
|
+
if (!text) return '';
|
|
194
|
+
try {
|
|
195
|
+
const parsed = new URL(text);
|
|
196
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
197
|
+
throw new Error(`${label} must use http or https.`);
|
|
198
|
+
}
|
|
199
|
+
return parsed.toString().replace(/\/$/, '');
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(`${label} must be a valid absolute URL.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeHomeAssistantBaseUrl(value) {
|
|
206
|
+
const text = trimText(value);
|
|
207
|
+
if (!text) return '';
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = new URL(text);
|
|
211
|
+
} catch {
|
|
212
|
+
throw new Error('Home Assistant base URL must be a valid absolute URL.');
|
|
213
|
+
}
|
|
214
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
215
|
+
throw new Error('Home Assistant base URL must use http or https.');
|
|
216
|
+
}
|
|
217
|
+
validateHomeAssistantBaseUrlSafety(parsed);
|
|
218
|
+
return parsed.toString().replace(/\/$/, '');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeUserHomeAssistantConfig(rawConfig) {
|
|
222
|
+
const source = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
223
|
+
return {
|
|
224
|
+
baseUrl: normalizeBaseUrl(source.baseUrl),
|
|
225
|
+
clientId: trimText(source.clientId),
|
|
226
|
+
clientSecret: trimText(source.clientSecret),
|
|
227
|
+
redirectUri: trimText(source.redirectUri),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveUserHomeAssistantConfig(userId, agentId = null) {
|
|
232
|
+
const userConfig = normalizeUserHomeAssistantConfig(
|
|
233
|
+
Number.isInteger(Number(userId)) && Number(userId) > 0
|
|
234
|
+
? getProviderConfig(Number(userId), 'home_assistant', agentId)
|
|
235
|
+
: {},
|
|
236
|
+
);
|
|
237
|
+
const envConfig = resolveHomeAssistantOAuthConfig();
|
|
238
|
+
return {
|
|
239
|
+
baseUrl: userConfig.baseUrl || envConfig.baseUrl,
|
|
240
|
+
clientId: userConfig.clientId || envConfig.clientId,
|
|
241
|
+
clientSecret: userConfig.clientSecret || envConfig.clientSecret,
|
|
242
|
+
redirectUri: userConfig.redirectUri || envConfig.redirectUri,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validateResolvedConfig(config) {
|
|
247
|
+
const missing = [];
|
|
248
|
+
if (!trimText(config.baseUrl)) missing.push('baseUrl');
|
|
249
|
+
if (!trimText(config.clientId)) missing.push('clientId');
|
|
250
|
+
if (!trimText(config.clientSecret)) missing.push('clientSecret');
|
|
251
|
+
return {
|
|
252
|
+
configured: missing.length === 0,
|
|
253
|
+
missing,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveHomeAssistantConfigForUser(userId, agentId = null) {
|
|
258
|
+
const merged = resolveUserHomeAssistantConfig(userId, agentId);
|
|
259
|
+
const validatedBaseUrl = merged.baseUrl
|
|
260
|
+
? normalizeHomeAssistantBaseUrl(merged.baseUrl)
|
|
261
|
+
: '';
|
|
262
|
+
const validatedRedirectUri = merged.redirectUri
|
|
263
|
+
? normalizeOptionalAbsoluteUrl(
|
|
264
|
+
merged.redirectUri,
|
|
265
|
+
'Home Assistant OAuth redirect URI',
|
|
266
|
+
)
|
|
267
|
+
: '';
|
|
268
|
+
const result = {
|
|
269
|
+
baseUrl: validatedBaseUrl,
|
|
270
|
+
clientId: trimText(merged.clientId),
|
|
271
|
+
clientSecret: trimText(merged.clientSecret),
|
|
272
|
+
redirectUri: validatedRedirectUri,
|
|
273
|
+
};
|
|
274
|
+
const status = validateResolvedConfig(result);
|
|
275
|
+
return {
|
|
276
|
+
...result,
|
|
277
|
+
configured: status.configured,
|
|
278
|
+
missing: status.missing,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sanitizeHomeAssistantUserConfigForClient(rawConfig) {
|
|
283
|
+
const config = normalizeUserHomeAssistantConfig(rawConfig);
|
|
284
|
+
return {
|
|
285
|
+
baseUrl: config.baseUrl,
|
|
286
|
+
clientId: config.clientId,
|
|
287
|
+
redirectUri: config.redirectUri,
|
|
288
|
+
hasClientSecret: Boolean(config.clientSecret),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseHomeAssistantConfigInput(input, existingConfig = {}) {
|
|
293
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
294
|
+
const baseUrl = normalizeHomeAssistantBaseUrl(source.baseUrl);
|
|
295
|
+
const clientId = trimText(source.clientId);
|
|
296
|
+
const clientSecret =
|
|
297
|
+
trimText(source.clientSecret) || trimText(existingConfig.clientSecret);
|
|
298
|
+
const redirectUri = source.redirectUri
|
|
299
|
+
? normalizeOptionalAbsoluteUrl(
|
|
300
|
+
source.redirectUri,
|
|
301
|
+
'Home Assistant OAuth redirect URI',
|
|
302
|
+
)
|
|
303
|
+
: '';
|
|
304
|
+
|
|
305
|
+
if (!baseUrl) {
|
|
306
|
+
throw new Error('Home Assistant base URL is required.');
|
|
307
|
+
}
|
|
308
|
+
if (!clientId) {
|
|
309
|
+
throw new Error('Home Assistant OAuth client ID is required.');
|
|
310
|
+
}
|
|
311
|
+
if (!clientSecret) {
|
|
312
|
+
throw new Error('Home Assistant OAuth client secret is required.');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
baseUrl,
|
|
317
|
+
clientId,
|
|
318
|
+
clientSecret,
|
|
319
|
+
redirectUri,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function saveHomeAssistantUserConfig(userId, agentId = null, input) {
|
|
324
|
+
const normalizedUserId = Number(userId);
|
|
325
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
326
|
+
throw new Error('A valid user is required to save Home Assistant configuration.');
|
|
327
|
+
}
|
|
328
|
+
const scopedAgentId = resolveAgentId(normalizedUserId, agentId);
|
|
329
|
+
const existingConfig = normalizeUserHomeAssistantConfig(
|
|
330
|
+
getProviderConfig(normalizedUserId, 'home_assistant', scopedAgentId),
|
|
331
|
+
);
|
|
332
|
+
const config = parseHomeAssistantConfigInput(input, existingConfig);
|
|
333
|
+
setProviderConfig(normalizedUserId, 'home_assistant', config, scopedAgentId);
|
|
334
|
+
return sanitizeHomeAssistantUserConfigForClient(config);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getHomeAssistantUserConfig(userId, agentId = null) {
|
|
338
|
+
const normalizedUserId = Number(userId);
|
|
339
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
340
|
+
return sanitizeHomeAssistantUserConfigForClient({});
|
|
341
|
+
}
|
|
342
|
+
return sanitizeHomeAssistantUserConfigForClient(
|
|
343
|
+
getProviderConfig(
|
|
344
|
+
normalizedUserId,
|
|
345
|
+
'home_assistant',
|
|
346
|
+
resolveAgentId(normalizedUserId, agentId),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
96
351
|
function requireText(value, label) {
|
|
97
352
|
const text = String(value || '').trim();
|
|
98
353
|
if (!text) throw new Error(`${label} is required.`);
|
|
@@ -102,7 +357,7 @@ function requireText(value, label) {
|
|
|
102
357
|
function homeAssistantUrl(baseUrl, path, query) {
|
|
103
358
|
const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
|
|
104
359
|
if (!normalizedBase) {
|
|
105
|
-
throw new Error('
|
|
360
|
+
throw new Error('Home Assistant base URL is required.');
|
|
106
361
|
}
|
|
107
362
|
const url = new URL(
|
|
108
363
|
String(path || '').startsWith('http')
|
|
@@ -119,7 +374,10 @@ function homeAssistantUrl(baseUrl, path, query) {
|
|
|
119
374
|
}
|
|
120
375
|
|
|
121
376
|
async function homeAssistantRequest(credentials, options = {}) {
|
|
122
|
-
const config =
|
|
377
|
+
const config = resolveHomeAssistantConfigForUser(
|
|
378
|
+
options.userId,
|
|
379
|
+
options.agentId,
|
|
380
|
+
);
|
|
123
381
|
const accessToken = String(credentials?.access_token || '').trim();
|
|
124
382
|
if (!accessToken) {
|
|
125
383
|
throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
|
|
@@ -138,14 +396,22 @@ async function homeAssistantRequest(credentials, options = {}) {
|
|
|
138
396
|
);
|
|
139
397
|
}
|
|
140
398
|
|
|
141
|
-
async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
399
|
+
async function executeHomeAssistantTool(toolName, args, { connection, credentials }) {
|
|
142
400
|
switch (toolName) {
|
|
143
401
|
case 'home_assistant_get_config':
|
|
144
402
|
return {
|
|
145
|
-
result: await homeAssistantRequest(credentials, {
|
|
403
|
+
result: await homeAssistantRequest(credentials, {
|
|
404
|
+
path: '/api/config',
|
|
405
|
+
userId: connection?.user_id,
|
|
406
|
+
agentId: connection?.agent_id,
|
|
407
|
+
}),
|
|
146
408
|
};
|
|
147
409
|
case 'home_assistant_list_states': {
|
|
148
|
-
const states = await homeAssistantRequest(credentials, {
|
|
410
|
+
const states = await homeAssistantRequest(credentials, {
|
|
411
|
+
path: '/api/states',
|
|
412
|
+
userId: connection?.user_id,
|
|
413
|
+
agentId: connection?.agent_id,
|
|
414
|
+
});
|
|
149
415
|
const domain = String(args.domain || '').trim().toLowerCase();
|
|
150
416
|
const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
|
|
151
417
|
const filtered = Array.isArray(states)
|
|
@@ -163,6 +429,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
163
429
|
return {
|
|
164
430
|
result: await homeAssistantRequest(credentials, {
|
|
165
431
|
path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
|
|
432
|
+
userId: connection?.user_id,
|
|
433
|
+
agentId: connection?.agent_id,
|
|
166
434
|
}),
|
|
167
435
|
};
|
|
168
436
|
case 'home_assistant_call_service':
|
|
@@ -171,6 +439,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
171
439
|
method: 'POST',
|
|
172
440
|
path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
|
|
173
441
|
body: args.service_data || {},
|
|
442
|
+
userId: connection?.user_id,
|
|
443
|
+
agentId: connection?.agent_id,
|
|
174
444
|
}),
|
|
175
445
|
};
|
|
176
446
|
case 'home_assistant_api_request':
|
|
@@ -180,6 +450,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
180
450
|
path: requireText(args.path, 'path'),
|
|
181
451
|
query: args.query,
|
|
182
452
|
body: args.body,
|
|
453
|
+
userId: connection?.user_id,
|
|
454
|
+
agentId: connection?.agent_id,
|
|
183
455
|
}),
|
|
184
456
|
};
|
|
185
457
|
default:
|
|
@@ -187,19 +459,25 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
187
459
|
}
|
|
188
460
|
}
|
|
189
461
|
|
|
190
|
-
function resolveHomeAssistantEnvStatus() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
462
|
+
function resolveHomeAssistantEnvStatus(userId, agentId = null) {
|
|
463
|
+
try {
|
|
464
|
+
const config = resolveHomeAssistantConfigForUser(userId, agentId);
|
|
465
|
+
return {
|
|
466
|
+
configured: config.configured,
|
|
467
|
+
missing: config.missing,
|
|
468
|
+
summary: config.configured
|
|
469
|
+
? 'Home Assistant is ready for account connections.'
|
|
470
|
+
: 'Complete your personal Home Assistant setup to connect an account.',
|
|
471
|
+
setupMode: 'user',
|
|
472
|
+
};
|
|
473
|
+
} catch (error) {
|
|
474
|
+
return {
|
|
475
|
+
configured: false,
|
|
476
|
+
missing: ['baseUrl'],
|
|
477
|
+
summary: `Home Assistant setup is invalid: ${error?.message || 'unknown error'}`,
|
|
478
|
+
setupMode: 'user',
|
|
479
|
+
};
|
|
195
480
|
}
|
|
196
|
-
return describeEnvStatus(
|
|
197
|
-
{
|
|
198
|
-
configured: missing.length === 0,
|
|
199
|
-
missing,
|
|
200
|
-
},
|
|
201
|
-
{ label: 'Home Assistant' },
|
|
202
|
-
);
|
|
203
481
|
}
|
|
204
482
|
|
|
205
483
|
function normalizeCurrentUser(currentUser) {
|
|
@@ -225,8 +503,8 @@ function stableAccountEmailLikeIdentifier(user, config) {
|
|
|
225
503
|
return `homeassistant@${host}`;
|
|
226
504
|
}
|
|
227
505
|
|
|
228
|
-
async function fetchCurrentUser(token) {
|
|
229
|
-
const config =
|
|
506
|
+
async function fetchCurrentUser(token, userId, agentId = null) {
|
|
507
|
+
const config = resolveHomeAssistantConfigForUser(userId, agentId);
|
|
230
508
|
return fetchJson(
|
|
231
509
|
homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
|
|
232
510
|
{
|
|
@@ -246,15 +524,16 @@ function createHomeAssistantProvider() {
|
|
|
246
524
|
description:
|
|
247
525
|
'Official Home Assistant account connections for entity state reads, service control, and automation support.',
|
|
248
526
|
icon: 'home_assistant',
|
|
527
|
+
requiresRefreshToken: true,
|
|
249
528
|
apps: HOME_ASSISTANT_APPS,
|
|
250
529
|
toolDefinitions: homeAssistantToolDefinitions,
|
|
251
530
|
connectPrompt:
|
|
252
531
|
'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
|
|
253
|
-
getEnvStatus() {
|
|
254
|
-
return resolveHomeAssistantEnvStatus();
|
|
532
|
+
getEnvStatus(context = {}) {
|
|
533
|
+
return resolveHomeAssistantEnvStatus(context.userId, context.agentId);
|
|
255
534
|
},
|
|
256
|
-
async beginOAuth({ state, codeVerifier, app }) {
|
|
257
|
-
const config =
|
|
535
|
+
async beginOAuth({ state, codeVerifier, app, userId, agentId }) {
|
|
536
|
+
const config = resolveHomeAssistantConfigForUser(userId, agentId);
|
|
258
537
|
const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
|
|
259
538
|
return {
|
|
260
539
|
url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
|
|
@@ -269,8 +548,8 @@ function createHomeAssistantProvider() {
|
|
|
269
548
|
appId: app.id,
|
|
270
549
|
};
|
|
271
550
|
},
|
|
272
|
-
async finishOAuth({ code, codeVerifier, app }) {
|
|
273
|
-
const config =
|
|
551
|
+
async finishOAuth({ code, codeVerifier, app, userId, agentId }) {
|
|
552
|
+
const config = resolveHomeAssistantConfigForUser(userId, agentId);
|
|
274
553
|
const token = await fetchJson(
|
|
275
554
|
homeAssistantUrl(config.baseUrl, '/auth/token'),
|
|
276
555
|
{
|
|
@@ -297,7 +576,7 @@ function createHomeAssistantProvider() {
|
|
|
297
576
|
throw new Error('Home Assistant OAuth did not return a refresh token.');
|
|
298
577
|
}
|
|
299
578
|
|
|
300
|
-
const currentUser = await fetchCurrentUser(accessToken);
|
|
579
|
+
const currentUser = await fetchCurrentUser(accessToken, userId, agentId);
|
|
301
580
|
const normalizedUser = normalizeCurrentUser(currentUser);
|
|
302
581
|
const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
|
|
303
582
|
|
|
@@ -328,6 +607,24 @@ function createHomeAssistantProvider() {
|
|
|
328
607
|
};
|
|
329
608
|
},
|
|
330
609
|
executeTool: executeHomeAssistantTool,
|
|
610
|
+
getUserConfig({ userId, agentId }) {
|
|
611
|
+
return getHomeAssistantUserConfig(userId, agentId);
|
|
612
|
+
},
|
|
613
|
+
saveUserConfig({ userId, agentId, config }) {
|
|
614
|
+
return saveHomeAssistantUserConfig(userId, agentId, config);
|
|
615
|
+
},
|
|
616
|
+
clearUserConfig({ userId, agentId }) {
|
|
617
|
+
const normalizedUserId = Number(userId);
|
|
618
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
619
|
+
throw new Error('A valid user is required to clear Home Assistant configuration.');
|
|
620
|
+
}
|
|
621
|
+
deleteProviderConfig(
|
|
622
|
+
normalizedUserId,
|
|
623
|
+
'home_assistant',
|
|
624
|
+
resolveAgentId(normalizedUserId, agentId),
|
|
625
|
+
);
|
|
626
|
+
return { cleared: true };
|
|
627
|
+
},
|
|
331
628
|
});
|
|
332
629
|
}
|
|
333
630
|
|
|
@@ -346,5 +643,7 @@ function codeChallengeForVerifier(codeVerifier) {
|
|
|
346
643
|
}
|
|
347
644
|
|
|
348
645
|
module.exports = {
|
|
646
|
+
getHomeAssistantUserConfig,
|
|
647
|
+
saveHomeAssistantUserConfig,
|
|
349
648
|
createHomeAssistantProvider,
|
|
350
649
|
};
|
|
@@ -13,6 +13,43 @@ const {
|
|
|
13
13
|
|
|
14
14
|
const OAUTH_STATE_PATTERN = /^[a-f0-9]{32,128}$/i;
|
|
15
15
|
|
|
16
|
+
function isLikelyExpiredConnectionError(error) {
|
|
17
|
+
const message = String(error?.message || error || '').toLowerCase();
|
|
18
|
+
if (!message) return false;
|
|
19
|
+
return [
|
|
20
|
+
'invalid_grant',
|
|
21
|
+
'token refresh failed',
|
|
22
|
+
'token expired',
|
|
23
|
+
'access token is missing',
|
|
24
|
+
'refresh token is missing',
|
|
25
|
+
'reconnect this integration account',
|
|
26
|
+
'account is no longer authorized',
|
|
27
|
+
'reauthorize',
|
|
28
|
+
're-authorize',
|
|
29
|
+
].some((hint) => message.includes(hint));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertDurableOAuthCredentials(provider, credentials) {
|
|
33
|
+
const label = String(provider?.label || 'This integration').trim() || 'This integration';
|
|
34
|
+
const normalizedCredentials =
|
|
35
|
+
credentials && typeof credentials === 'object' ? credentials : {};
|
|
36
|
+
|
|
37
|
+
if (!String(normalizedCredentials.access_token || '').trim()) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`${label} did not return an access token, so the connection could not be completed.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
provider?.requiresRefreshToken === true &&
|
|
45
|
+
!String(normalizedCredentials.refresh_token || '').trim()
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
16
53
|
class IntegrationManager {
|
|
17
54
|
constructor(options = {}) {
|
|
18
55
|
this.app = options.app || null;
|
|
@@ -147,7 +184,12 @@ class IntegrationManager {
|
|
|
147
184
|
|
|
148
185
|
return this.registry
|
|
149
186
|
.list()
|
|
150
|
-
.map((provider) =>
|
|
187
|
+
.map((provider) =>
|
|
188
|
+
provider.buildSnapshot(rowsByProvider.get(provider.key) || [], {
|
|
189
|
+
userId,
|
|
190
|
+
agentId: scopedAgentId,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
151
193
|
}
|
|
152
194
|
|
|
153
195
|
async beginOAuth(userId, providerKey, options = {}) {
|
|
@@ -163,7 +205,10 @@ class IntegrationManager {
|
|
|
163
205
|
throw new Error(`Unknown ${provider.label} app: ${appKey || 'missing app key'}`);
|
|
164
206
|
}
|
|
165
207
|
|
|
166
|
-
const env = provider.getEnvStatus(
|
|
208
|
+
const env = provider.getEnvStatus({
|
|
209
|
+
userId,
|
|
210
|
+
agentId,
|
|
211
|
+
});
|
|
167
212
|
if (!env.configured) {
|
|
168
213
|
throw new Error(env.summary);
|
|
169
214
|
}
|
|
@@ -193,6 +238,7 @@ class IntegrationManager {
|
|
|
193
238
|
state,
|
|
194
239
|
codeVerifier,
|
|
195
240
|
userId,
|
|
241
|
+
agentId,
|
|
196
242
|
appKey,
|
|
197
243
|
});
|
|
198
244
|
|
|
@@ -264,6 +310,7 @@ class IntegrationManager {
|
|
|
264
310
|
|
|
265
311
|
const result = await provider.finishOAuth({
|
|
266
312
|
userId: stateRow.user_id,
|
|
313
|
+
agentId: stateRow.agent_id || resolveAgentId(stateRow.user_id, null),
|
|
267
314
|
state: stateRow.state,
|
|
268
315
|
code: normalizedCode,
|
|
269
316
|
codeVerifier: decryptValue(stateRow.code_verifier),
|
|
@@ -277,11 +324,7 @@ class IntegrationManager {
|
|
|
277
324
|
result.accountEmail,
|
|
278
325
|
result.credentials,
|
|
279
326
|
);
|
|
280
|
-
|
|
281
|
-
throw new Error(
|
|
282
|
-
`${provider.label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
327
|
+
assertDurableOAuthCredentials(provider, mergedCredentials);
|
|
285
328
|
|
|
286
329
|
db.prepare(
|
|
287
330
|
`INSERT INTO integration_connections (
|
|
@@ -389,7 +432,10 @@ class IntegrationManager {
|
|
|
389
432
|
getToolDefinitions(userId, agentId = null) {
|
|
390
433
|
const definitions = [];
|
|
391
434
|
for (const provider of this.registry.list()) {
|
|
392
|
-
const env = provider.getEnvStatus(
|
|
435
|
+
const env = provider.getEnvStatus({
|
|
436
|
+
userId,
|
|
437
|
+
agentId,
|
|
438
|
+
});
|
|
393
439
|
if (!env.configured) continue;
|
|
394
440
|
const connections = this.listConnections(userId, provider.key, agentId);
|
|
395
441
|
const connectedAppIds = Array.from(
|
|
@@ -411,9 +457,15 @@ class IntegrationManager {
|
|
|
411
457
|
if (!provider) {
|
|
412
458
|
throw new Error(`Unknown integration provider: ${providerKey}`);
|
|
413
459
|
}
|
|
414
|
-
const env = provider.getEnvStatus(
|
|
460
|
+
const env = provider.getEnvStatus({
|
|
461
|
+
userId,
|
|
462
|
+
agentId,
|
|
463
|
+
});
|
|
415
464
|
const connections = this.listConnections(userId, provider.key, agentId);
|
|
416
|
-
const snapshot = provider.buildSnapshot(connections
|
|
465
|
+
const snapshot = provider.buildSnapshot(connections, {
|
|
466
|
+
userId,
|
|
467
|
+
agentId,
|
|
468
|
+
});
|
|
417
469
|
const connectedAppIds = snapshot.apps
|
|
418
470
|
.filter((app) => app.connection.connected)
|
|
419
471
|
.map((app) => app.id);
|
|
@@ -582,7 +634,10 @@ class IntegrationManager {
|
|
|
582
634
|
for (const provider of this.registry.list()) {
|
|
583
635
|
if (!provider.supportsTool(toolName)) continue;
|
|
584
636
|
foundSupportingProvider = true;
|
|
585
|
-
const env = provider.getEnvStatus(
|
|
637
|
+
const env = provider.getEnvStatus({
|
|
638
|
+
userId,
|
|
639
|
+
agentId,
|
|
640
|
+
});
|
|
586
641
|
if (!env.configured) {
|
|
587
642
|
return { error: env.summary };
|
|
588
643
|
}
|
|
@@ -607,6 +662,17 @@ class IntegrationManager {
|
|
|
607
662
|
selection.connection,
|
|
608
663
|
);
|
|
609
664
|
} catch (err) {
|
|
665
|
+
if (isLikelyExpiredConnectionError(err)) {
|
|
666
|
+
db.prepare(
|
|
667
|
+
`UPDATE integration_connections
|
|
668
|
+
SET status = 'expired', updated_at = datetime('now')
|
|
669
|
+
WHERE id = ? AND user_id = ? AND agent_id = ?`,
|
|
670
|
+
).run(
|
|
671
|
+
selection.connection.id,
|
|
672
|
+
userId,
|
|
673
|
+
resolveAgentId(userId, agentId),
|
|
674
|
+
);
|
|
675
|
+
}
|
|
610
676
|
return { error: err?.message || 'execution_error' };
|
|
611
677
|
}
|
|
612
678
|
if (!execution) {
|
|
@@ -641,7 +707,13 @@ class IntegrationManager {
|
|
|
641
707
|
summarizeConnectedProviders(userId, agentId = null) {
|
|
642
708
|
const providers = this.registry.list().map((provider) => ({
|
|
643
709
|
provider,
|
|
644
|
-
snapshot: provider.buildSnapshot(
|
|
710
|
+
snapshot: provider.buildSnapshot(
|
|
711
|
+
this.listConnections(userId, provider.key, agentId),
|
|
712
|
+
{
|
|
713
|
+
userId,
|
|
714
|
+
agentId,
|
|
715
|
+
},
|
|
716
|
+
),
|
|
645
717
|
}));
|
|
646
718
|
|
|
647
719
|
if (providers.length === 0) {
|
|
@@ -655,6 +727,9 @@ class IntegrationManager {
|
|
|
655
727
|
}
|
|
656
728
|
|
|
657
729
|
if (!snapshot?.env?.configured) {
|
|
730
|
+
if (snapshot?.env?.setupMode === 'user') {
|
|
731
|
+
return `${provider.label}: setup is not complete for this user yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
|
|
732
|
+
}
|
|
658
733
|
return `${provider.label}: available but not configured on the server yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
|
|
659
734
|
}
|
|
660
735
|
|
|
@@ -669,5 +744,6 @@ class IntegrationManager {
|
|
|
669
744
|
}
|
|
670
745
|
|
|
671
746
|
module.exports = {
|
|
747
|
+
assertDurableOAuthCredentials,
|
|
672
748
|
IntegrationManager,
|
|
673
749
|
};
|
|
@@ -346,6 +346,7 @@ function createMicrosoftProvider() {
|
|
|
346
346
|
description:
|
|
347
347
|
'Official Microsoft 365 OAuth account connections for Outlook, Calendar, OneDrive, and Teams.',
|
|
348
348
|
icon: 'microsoft',
|
|
349
|
+
requiresRefreshToken: true,
|
|
349
350
|
apps: MICROSOFT_APPS,
|
|
350
351
|
toolDefinitions: microsoftToolDefinitions,
|
|
351
352
|
connectPrompt:
|