neoagent 2.2.1-beta.8 → 2.3.1-beta.0
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/README.md +1 -1
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +5 -1
- package/docs/integrations.md +1 -0
- package/package.json +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/services/integrations/env.js +9 -0
- package/server/services/integrations/home_assistant/provider.js +350 -0
- package/server/services/integrations/registry.js +2 -0
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ neoagent logs
|
|
|
36
36
|
|
|
37
37
|
## Links
|
|
38
38
|
|
|
39
|
-
[Docs](https://neolabs-systems.github.io/NeoAgent/) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
|
|
39
|
+
[Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Homepage](https://neolabs-systems.github.io/NeoAgent/) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
package/docs/capabilities.md
CHANGED
|
@@ -75,10 +75,10 @@ The agent tool `read_health_data` returns summaries and recent samples. It is de
|
|
|
75
75
|
|
|
76
76
|
NeoAgent has two separate integration layers:
|
|
77
77
|
|
|
78
|
-
- Official integrations expose structured tools for Google Workspace, Microsoft 365, Notion, Slack, Figma, and a separate personal WhatsApp connection.
|
|
78
|
+
- Official integrations expose structured tools for Google Workspace, Microsoft 365, Notion, Slack, Figma, Home Assistant, and a separate personal WhatsApp connection.
|
|
79
79
|
- Messaging platforms let the agent talk through WhatsApp, Telegram, Discord, Slack, Google Chat, Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Twitch, LINE, Mattermost, configurable webhook bridges, and Telnyx Voice.
|
|
80
80
|
|
|
81
|
-
Official integration examples include Gmail thread search and send mail, Google Calendar events, Drive upload/download/export/share links, Docs create/append/replace, Sheets read/update/append/create, Microsoft Outlook/Calendar/OneDrive/Teams tools, Notion search/page/block/database tools, Slack conversation/message tools, Figma file/node/comment/image tools, and a personal WhatsApp integration with isolated chat read/send tools and per-account read-only versus read/write access.
|
|
81
|
+
Official integration examples include Gmail thread search and send mail, Google Calendar events, Drive upload/download/export/share links, Docs create/append/replace, Sheets read/update/append/create, Microsoft Outlook/Calendar/OneDrive/Teams tools, Notion search/page/block/database tools, Slack conversation/message tools, Figma file/node/comment/image tools, Home Assistant entity/config reads and service calls, and a personal WhatsApp integration with isolated chat read/send tools and per-account read-only versus read/write access.
|
|
82
82
|
|
|
83
83
|
Messaging examples include Telegram and Discord messages, Slack channel replies, Matrix room messages, Google Chat and Teams webhook delivery, Signal bridge delivery, iMessage/BlueBubbles sends, WhatsApp text and media sends, Telnyx inbound voice, Telnyx outbound calls, and scheduled-task call delivery.
|
|
84
84
|
|
package/docs/configuration.md
CHANGED
|
@@ -68,7 +68,7 @@ Recording insight generation is controlled in app AI settings with `auto_recordi
|
|
|
68
68
|
|
|
69
69
|
## Official Integrations
|
|
70
70
|
|
|
71
|
-
Official integrations use OAuth or provider-native account linking and expose structured tools to the agent. The built-in registry currently covers Google Workspace, Notion, Microsoft 365, Slack, Figma, and personal WhatsApp.
|
|
71
|
+
Official integrations use OAuth or provider-native account linking and expose structured tools to the agent. The built-in registry currently covers Google Workspace, Notion, Microsoft 365, Slack, Figma, Home Assistant, and personal WhatsApp.
|
|
72
72
|
|
|
73
73
|
All OAuth callbacks default to `PUBLIC_URL + /api/integrations/oauth/callback` unless you set a provider-specific redirect URI.
|
|
74
74
|
|
|
@@ -90,6 +90,10 @@ All OAuth callbacks default to `PUBLIC_URL + /api/integrations/oauth/callback` u
|
|
|
90
90
|
| `FIGMA_OAUTH_CLIENT_ID` | Figma OAuth client ID |
|
|
91
91
|
| `FIGMA_OAUTH_CLIENT_SECRET` | Figma OAuth client secret |
|
|
92
92
|
| `FIGMA_OAUTH_REDIRECT_URI` | Optional Figma OAuth callback URL |
|
|
93
|
+
| `HOME_ASSISTANT_BASE_URL` | Home Assistant base URL, for example `https://ha.example.com` |
|
|
94
|
+
| `HOME_ASSISTANT_OAUTH_CLIENT_ID` | Home Assistant OAuth client ID |
|
|
95
|
+
| `HOME_ASSISTANT_OAUTH_CLIENT_SECRET` | Home Assistant OAuth client secret |
|
|
96
|
+
| `HOME_ASSISTANT_OAUTH_REDIRECT_URI` | Optional Home Assistant OAuth callback URL |
|
|
93
97
|
|
|
94
98
|
## Messaging
|
|
95
99
|
|
package/docs/integrations.md
CHANGED
|
@@ -13,6 +13,7 @@ The built-in registry includes:
|
|
|
13
13
|
| Microsoft 365 | Outlook, Calendar, OneDrive, Teams, and Microsoft Graph requests |
|
|
14
14
|
| Slack | Conversations, history, posting, search, user info, and Slack Web API requests |
|
|
15
15
|
| Figma | Current user, files, nodes, rendered images, comments, and Figma REST requests |
|
|
16
|
+
| Home Assistant | Entity/config reads, service calls, and Home Assistant REST API requests |
|
|
16
17
|
|
|
17
18
|
OAuth app credentials are configured through server environment variables. Account connections are created in the Flutter UI under **Integrations**. Connected tools are exposed to the agent as structured tools, so prefer them over browser automation when they can do the job.
|
|
18
19
|
|
package/package.json
CHANGED
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "714146073" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -56,6 +56,14 @@ function resolveMicrosoftOAuthConfig() {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function resolveHomeAssistantOAuthConfig() {
|
|
60
|
+
const base = resolveOAuthConfig('HOME_ASSISTANT');
|
|
61
|
+
return {
|
|
62
|
+
...base,
|
|
63
|
+
baseUrl: trimEnv('HOME_ASSISTANT_BASE_URL').replace(/\/$/, ''),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
function describeEnvStatus(config, options = {}) {
|
|
60
68
|
const label = String(options.label || 'This integration').trim() || 'This integration';
|
|
61
69
|
if (config.configured) {
|
|
@@ -76,6 +84,7 @@ function describeEnvStatus(config, options = {}) {
|
|
|
76
84
|
module.exports = {
|
|
77
85
|
describeEnvStatus,
|
|
78
86
|
resolveFigmaOAuthConfig,
|
|
87
|
+
resolveHomeAssistantOAuthConfig,
|
|
79
88
|
resolveMicrosoftOAuthConfig,
|
|
80
89
|
resolveNotionOAuthConfig,
|
|
81
90
|
resolveOAuthConfig,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const {
|
|
5
|
+
describeEnvStatus,
|
|
6
|
+
resolveHomeAssistantOAuthConfig,
|
|
7
|
+
} = require('../env');
|
|
8
|
+
const {
|
|
9
|
+
appendQuery,
|
|
10
|
+
createOAuthProvider,
|
|
11
|
+
fetchJson,
|
|
12
|
+
} = require('../oauth_provider');
|
|
13
|
+
|
|
14
|
+
const HOME_ASSISTANT_APPS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'home_assistant',
|
|
17
|
+
label: 'Home Assistant',
|
|
18
|
+
description: 'Connect Home Assistant for entity state queries, service calls, and API access.',
|
|
19
|
+
scopes: ['homeassistant'],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const homeAssistantToolDefinitions = [
|
|
24
|
+
{
|
|
25
|
+
appId: 'home_assistant',
|
|
26
|
+
name: 'home_assistant_get_config',
|
|
27
|
+
access: 'read',
|
|
28
|
+
description: 'Get Home Assistant configuration details for the connected instance.',
|
|
29
|
+
parameters: { type: 'object', properties: {} },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
appId: 'home_assistant',
|
|
33
|
+
name: 'home_assistant_list_states',
|
|
34
|
+
access: 'read',
|
|
35
|
+
description: 'List entity states from Home Assistant. Optional filters are applied client-side.',
|
|
36
|
+
parameters: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
domain: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Optional entity domain filter, for example light, climate, or switch.',
|
|
42
|
+
},
|
|
43
|
+
limit: {
|
|
44
|
+
type: 'number',
|
|
45
|
+
description: 'Optional max entities to return, default 100.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
appId: 'home_assistant',
|
|
52
|
+
name: 'home_assistant_get_state',
|
|
53
|
+
access: 'read',
|
|
54
|
+
description: 'Get state for a single Home Assistant entity by entity_id.',
|
|
55
|
+
parameters: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
entity_id: { type: 'string', description: 'Entity ID, for example light.living_room.' },
|
|
59
|
+
},
|
|
60
|
+
required: ['entity_id'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
appId: 'home_assistant',
|
|
65
|
+
name: 'home_assistant_call_service',
|
|
66
|
+
access: 'write',
|
|
67
|
+
description: 'Call a Home Assistant service such as light.turn_on or climate.set_temperature.',
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
domain: { type: 'string', description: 'Service domain, for example light, climate, or switch.' },
|
|
72
|
+
service: { type: 'string', description: 'Service name, for example turn_on, turn_off, or set_temperature.' },
|
|
73
|
+
service_data: { type: 'object', description: 'Optional Home Assistant service data payload.' },
|
|
74
|
+
},
|
|
75
|
+
required: ['domain', 'service'],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
appId: 'home_assistant',
|
|
80
|
+
name: 'home_assistant_api_request',
|
|
81
|
+
access: 'dynamic_http_method',
|
|
82
|
+
description: 'Make an authenticated Home Assistant REST API request for advanced operations.',
|
|
83
|
+
parameters: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
|
|
87
|
+
path: { type: 'string', description: 'API path under the configured instance, for example /api/history/period.' },
|
|
88
|
+
query: { type: 'object', description: 'Optional query parameters.' },
|
|
89
|
+
body: { type: 'object', description: 'Optional JSON body.' },
|
|
90
|
+
},
|
|
91
|
+
required: ['method', 'path'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function requireText(value, label) {
|
|
97
|
+
const text = String(value || '').trim();
|
|
98
|
+
if (!text) throw new Error(`${label} is required.`);
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function homeAssistantUrl(baseUrl, path, query) {
|
|
103
|
+
const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
|
|
104
|
+
if (!normalizedBase) {
|
|
105
|
+
throw new Error('HOME_ASSISTANT_BASE_URL is required.');
|
|
106
|
+
}
|
|
107
|
+
const url = new URL(
|
|
108
|
+
String(path || '').startsWith('http')
|
|
109
|
+
? String(path)
|
|
110
|
+
: `${normalizedBase}${String(path || '').startsWith('/') ? '' : '/'}${path}`,
|
|
111
|
+
);
|
|
112
|
+
if (new URL(normalizedBase).origin !== url.origin) {
|
|
113
|
+
throw new Error('Home Assistant request URL must stay on HOME_ASSISTANT_BASE_URL origin.');
|
|
114
|
+
}
|
|
115
|
+
for (const [key, value] of Object.entries(query || {})) {
|
|
116
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
117
|
+
}
|
|
118
|
+
return url.toString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function homeAssistantRequest(credentials, options = {}) {
|
|
122
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
123
|
+
const accessToken = String(credentials?.access_token || '').trim();
|
|
124
|
+
if (!accessToken) {
|
|
125
|
+
throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
|
|
126
|
+
}
|
|
127
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
128
|
+
return fetchJson(
|
|
129
|
+
homeAssistantUrl(config.baseUrl, options.path, options.query),
|
|
130
|
+
{
|
|
131
|
+
method,
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${accessToken}`,
|
|
134
|
+
},
|
|
135
|
+
...(options.body === undefined ? {} : { json: options.body }),
|
|
136
|
+
},
|
|
137
|
+
{ serviceName: 'Home Assistant' },
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
142
|
+
switch (toolName) {
|
|
143
|
+
case 'home_assistant_get_config':
|
|
144
|
+
return {
|
|
145
|
+
result: await homeAssistantRequest(credentials, { path: '/api/config' }),
|
|
146
|
+
};
|
|
147
|
+
case 'home_assistant_list_states': {
|
|
148
|
+
const states = await homeAssistantRequest(credentials, { path: '/api/states' });
|
|
149
|
+
const domain = String(args.domain || '').trim().toLowerCase();
|
|
150
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
|
|
151
|
+
const filtered = Array.isArray(states)
|
|
152
|
+
? states.filter((item) => {
|
|
153
|
+
if (!domain) return true;
|
|
154
|
+
const entityId = String(item?.entity_id || '').toLowerCase();
|
|
155
|
+
return entityId.startsWith(`${domain}.`);
|
|
156
|
+
})
|
|
157
|
+
: [];
|
|
158
|
+
return {
|
|
159
|
+
result: filtered.slice(0, limit),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
case 'home_assistant_get_state':
|
|
163
|
+
return {
|
|
164
|
+
result: await homeAssistantRequest(credentials, {
|
|
165
|
+
path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
case 'home_assistant_call_service':
|
|
169
|
+
return {
|
|
170
|
+
result: await homeAssistantRequest(credentials, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
|
|
173
|
+
body: args.service_data || {},
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
case 'home_assistant_api_request':
|
|
177
|
+
return {
|
|
178
|
+
result: await homeAssistantRequest(credentials, {
|
|
179
|
+
method: args.method,
|
|
180
|
+
path: requireText(args.path, 'path'),
|
|
181
|
+
query: args.query,
|
|
182
|
+
body: args.body,
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
default:
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveHomeAssistantEnvStatus() {
|
|
191
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
192
|
+
const missing = config.missing.slice();
|
|
193
|
+
if (!config.baseUrl) {
|
|
194
|
+
missing.push('HOME_ASSISTANT_BASE_URL');
|
|
195
|
+
}
|
|
196
|
+
return describeEnvStatus(
|
|
197
|
+
{
|
|
198
|
+
configured: missing.length === 0,
|
|
199
|
+
missing,
|
|
200
|
+
},
|
|
201
|
+
{ label: 'Home Assistant' },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeCurrentUser(currentUser) {
|
|
206
|
+
const payload = currentUser && typeof currentUser === 'object' ? currentUser : {};
|
|
207
|
+
return {
|
|
208
|
+
id: payload.id || null,
|
|
209
|
+
name: payload.name || null,
|
|
210
|
+
username: payload.username || null,
|
|
211
|
+
email: payload.email || null,
|
|
212
|
+
isOwner: Boolean(payload.is_owner),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stableAccountEmailLikeIdentifier(user, config) {
|
|
217
|
+
const normalized = normalizeCurrentUser(user);
|
|
218
|
+
const preferred = [normalized.email, normalized.username, normalized.id, normalized.name]
|
|
219
|
+
.map((value) => String(value || '').trim())
|
|
220
|
+
.find(Boolean);
|
|
221
|
+
if (preferred) {
|
|
222
|
+
return preferred;
|
|
223
|
+
}
|
|
224
|
+
const host = new URL(config.baseUrl).host;
|
|
225
|
+
return `homeassistant@${host}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function fetchCurrentUser(token) {
|
|
229
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
230
|
+
return fetchJson(
|
|
231
|
+
homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
|
|
232
|
+
{
|
|
233
|
+
method: 'GET',
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${token}`,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{ serviceName: 'Home Assistant' },
|
|
239
|
+
).catch(() => null);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createHomeAssistantProvider() {
|
|
243
|
+
return createOAuthProvider({
|
|
244
|
+
key: 'home_assistant',
|
|
245
|
+
label: 'Home Assistant',
|
|
246
|
+
description:
|
|
247
|
+
'Official Home Assistant account connections for entity state reads, service control, and automation support.',
|
|
248
|
+
icon: 'home_assistant',
|
|
249
|
+
apps: HOME_ASSISTANT_APPS,
|
|
250
|
+
toolDefinitions: homeAssistantToolDefinitions,
|
|
251
|
+
connectPrompt:
|
|
252
|
+
'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
|
|
253
|
+
getEnvStatus() {
|
|
254
|
+
return resolveHomeAssistantEnvStatus();
|
|
255
|
+
},
|
|
256
|
+
async beginOAuth({ state, codeVerifier, app }) {
|
|
257
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
258
|
+
const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
|
|
259
|
+
return {
|
|
260
|
+
url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
|
|
261
|
+
response_type: 'code',
|
|
262
|
+
client_id: config.clientId,
|
|
263
|
+
redirect_uri: config.redirectUri,
|
|
264
|
+
state,
|
|
265
|
+
scope: app.scopes.join(' '),
|
|
266
|
+
code_challenge: codeChallenge,
|
|
267
|
+
code_challenge_method: 'S256',
|
|
268
|
+
}),
|
|
269
|
+
appId: app.id,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
async finishOAuth({ code, codeVerifier, app }) {
|
|
273
|
+
const config = resolveHomeAssistantOAuthConfig();
|
|
274
|
+
const token = await fetchJson(
|
|
275
|
+
homeAssistantUrl(config.baseUrl, '/auth/token'),
|
|
276
|
+
{
|
|
277
|
+
method: 'POST',
|
|
278
|
+
form: {
|
|
279
|
+
grant_type: 'authorization_code',
|
|
280
|
+
client_id: config.clientId,
|
|
281
|
+
client_secret: config.clientSecret,
|
|
282
|
+
code,
|
|
283
|
+
code_verifier: codeVerifier,
|
|
284
|
+
redirect_uri: config.redirectUri,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{ serviceName: 'Home Assistant' },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const accessToken = String(token?.access_token || '').trim();
|
|
291
|
+
if (!accessToken) {
|
|
292
|
+
throw new Error('Home Assistant OAuth did not return an access token.');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const refreshToken = String(token?.refresh_token || '').trim();
|
|
296
|
+
if (!refreshToken) {
|
|
297
|
+
throw new Error('Home Assistant OAuth did not return a refresh token.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const currentUser = await fetchCurrentUser(accessToken);
|
|
301
|
+
const normalizedUser = normalizeCurrentUser(currentUser);
|
|
302
|
+
const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
appId: app.id,
|
|
306
|
+
accountEmail,
|
|
307
|
+
credentials: {
|
|
308
|
+
access_token: accessToken,
|
|
309
|
+
refresh_token: refreshToken,
|
|
310
|
+
token_type: token?.token_type || 'Bearer',
|
|
311
|
+
expires_in: token?.expires_in || null,
|
|
312
|
+
scope: token?.scope || app.scopes.join(' '),
|
|
313
|
+
},
|
|
314
|
+
scopes: Array.isArray(token?.scope)
|
|
315
|
+
? token.scope
|
|
316
|
+
: String(token?.scope || app.scopes.join(' '))
|
|
317
|
+
.split(/\s+/)
|
|
318
|
+
.map((scope) => scope.trim())
|
|
319
|
+
.filter(Boolean),
|
|
320
|
+
metadata: {
|
|
321
|
+
homeAssistantUrl: config.baseUrl,
|
|
322
|
+
userId: normalizedUser.id,
|
|
323
|
+
name: normalizedUser.name,
|
|
324
|
+
username: normalizedUser.username,
|
|
325
|
+
email: normalizedUser.email,
|
|
326
|
+
isOwner: normalizedUser.isOwner,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
executeTool: executeHomeAssistantTool,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function base64UrlSha256(value) {
|
|
335
|
+
return crypto
|
|
336
|
+
.createHash('sha256')
|
|
337
|
+
.update(String(value || ''))
|
|
338
|
+
.digest('base64')
|
|
339
|
+
.replace(/\+/g, '-')
|
|
340
|
+
.replace(/\//g, '_')
|
|
341
|
+
.replace(/=+$/g, '');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function codeChallengeForVerifier(codeVerifier) {
|
|
345
|
+
return base64UrlSha256(String(codeVerifier || ''));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
createHomeAssistantProvider,
|
|
350
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { createFigmaProvider } = require('./figma/provider');
|
|
4
4
|
const { createGoogleWorkspaceProvider } = require('./google/provider');
|
|
5
|
+
const { createHomeAssistantProvider } = require('./home_assistant/provider');
|
|
5
6
|
const { createMicrosoftProvider } = require('./microsoft/provider');
|
|
6
7
|
const { createNotionProvider } = require('./notion/provider');
|
|
7
8
|
const { createSlackProvider } = require('./slack/provider');
|
|
@@ -14,6 +15,7 @@ function createIntegrationRegistry(options = {}) {
|
|
|
14
15
|
createMicrosoftProvider(),
|
|
15
16
|
createSlackProvider(),
|
|
16
17
|
createFigmaProvider(),
|
|
18
|
+
createHomeAssistantProvider(),
|
|
17
19
|
createWhatsAppPersonalProvider(options),
|
|
18
20
|
];
|
|
19
21
|
const byKey = new Map(providers.map((provider) => [provider.key, provider]));
|