neoagent 2.3.0 → 2.3.1-beta.10

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 (60) hide show
  1. package/.env.example +13 -0
  2. package/README.md +3 -1
  3. package/docs/automation.md +1 -1
  4. package/docs/capabilities.md +2 -2
  5. package/docs/configuration.md +14 -1
  6. package/docs/integrations.md +6 -1
  7. package/lib/manager.js +127 -1
  8. package/package.json +2 -1
  9. package/server/db/database.js +68 -0
  10. package/server/http/middleware.js +50 -0
  11. package/server/http/routes.js +3 -1
  12. package/server/index.js +1 -0
  13. package/server/public/.last_build_id +1 -1
  14. package/server/public/assets/NOTICES +61 -0
  15. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  16. package/server/public/flutter_bootstrap.js +1 -1
  17. package/server/public/main.dart.js +61049 -60444
  18. package/server/routes/integrations.js +97 -8
  19. package/server/routes/memory.js +11 -2
  20. package/server/routes/screenHistory.js +46 -0
  21. package/server/routes/triggers.js +81 -0
  22. package/server/services/ai/engine.js +9 -0
  23. package/server/services/ai/models.js +30 -0
  24. package/server/services/ai/providers/githubCopilot.js +97 -0
  25. package/server/services/ai/providers/openaiCodex.js +26 -0
  26. package/server/services/ai/settings.js +20 -0
  27. package/server/services/ai/systemPrompt.js +1 -1
  28. package/server/services/ai/tools.js +150 -11
  29. package/server/services/browser/controller.js +47 -3
  30. package/server/services/desktop/screenRecorder.js +126 -0
  31. package/server/services/integrations/env.js +19 -0
  32. package/server/services/integrations/github/common.js +106 -0
  33. package/server/services/integrations/github/provider.js +499 -0
  34. package/server/services/integrations/github/repos.js +1124 -0
  35. package/server/services/integrations/home_assistant/provider.js +630 -0
  36. package/server/services/integrations/manager.js +63 -7
  37. package/server/services/integrations/oauth_provider.js +13 -6
  38. package/server/services/integrations/provider_config_store.js +76 -0
  39. package/server/services/integrations/registry.js +10 -0
  40. package/server/services/integrations/spotify/provider.js +487 -0
  41. package/server/services/integrations/weather/provider.js +559 -0
  42. package/server/services/integrations/whatsapp/provider.js +6 -2
  43. package/server/services/manager.js +22 -0
  44. package/server/services/memory/manager.js +39 -2
  45. package/server/services/messaging/manager.js +29 -7
  46. package/server/services/skills/base_catalog.js +33 -0
  47. package/server/services/tasks/adapters/index.js +2 -0
  48. package/server/services/tasks/adapters/manual.js +12 -0
  49. package/server/services/tasks/adapters/schedule.js +33 -5
  50. package/server/services/tasks/adapters/weather_event.js +84 -0
  51. package/server/services/tasks/integration_runtime.js +85 -0
  52. package/server/services/tasks/runtime.js +2 -2
  53. package/server/services/voice/agentBridge.js +20 -4
  54. package/server/services/voice/message.js +3 -0
  55. package/server/services/voice/openaiClient.js +4 -1
  56. package/server/services/voice/providers.js +2 -1
  57. package/server/services/voice/runtimeManager.js +136 -1
  58. package/server/services/widgets/service.js +49 -4
  59. package/server/utils/local_secrets.js +56 -0
  60. package/server/utils/logger.js +37 -9
@@ -0,0 +1,559 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const db = require('../../../db/database');
5
+ const { fetchJson } = require('../oauth_provider');
6
+
7
+ const WEATHER_APP = {
8
+ id: 'forecast',
9
+ label: 'Forecast',
10
+ description: 'Public weather data from Open-Meteo (no API key required).',
11
+ };
12
+
13
+ const WEATHER_TOOL_DEFINITIONS = [
14
+ {
15
+ appId: WEATHER_APP.id,
16
+ name: 'weather_search_locations',
17
+ access: 'read',
18
+ description: 'Search Open-Meteo locations by city or place name.',
19
+ parameters: {
20
+ type: 'object',
21
+ properties: {
22
+ query: { type: 'string', description: 'Location text like "Berlin" or "San Francisco".' },
23
+ limit: { type: 'number', description: 'Maximum locations to return, default 5.' },
24
+ },
25
+ required: ['query'],
26
+ },
27
+ },
28
+ {
29
+ appId: WEATHER_APP.id,
30
+ name: 'weather_get_current',
31
+ access: 'read',
32
+ description: 'Get current weather conditions for a location or coordinates.',
33
+ parameters: {
34
+ type: 'object',
35
+ properties: {
36
+ location: { type: 'string', description: 'Optional location name like "Tokyo".' },
37
+ latitude: { type: 'number', description: 'Latitude, required with longitude if location is omitted.' },
38
+ longitude: { type: 'number', description: 'Longitude, required with latitude if location is omitted.' },
39
+ },
40
+ },
41
+ },
42
+ {
43
+ appId: WEATHER_APP.id,
44
+ name: 'weather_get_forecast',
45
+ access: 'read',
46
+ description: 'Get hourly weather forecast for a location or coordinates.',
47
+ parameters: {
48
+ type: 'object',
49
+ properties: {
50
+ location: { type: 'string', description: 'Optional location name like "London".' },
51
+ latitude: { type: 'number', description: 'Latitude, required with longitude if location is omitted.' },
52
+ longitude: { type: 'number', description: 'Longitude, required with latitude if location is omitted.' },
53
+ forecast_hours: { type: 'number', description: 'Forecast horizon in hours (1-72), default 24.' },
54
+ },
55
+ },
56
+ },
57
+ ];
58
+
59
+ const WEATHER_CODE_LABELS = {
60
+ 0: 'Clear sky',
61
+ 1: 'Mainly clear',
62
+ 2: 'Partly cloudy',
63
+ 3: 'Overcast',
64
+ 45: 'Fog',
65
+ 48: 'Depositing rime fog',
66
+ 51: 'Light drizzle',
67
+ 53: 'Moderate drizzle',
68
+ 55: 'Dense drizzle',
69
+ 56: 'Light freezing drizzle',
70
+ 57: 'Dense freezing drizzle',
71
+ 61: 'Slight rain',
72
+ 63: 'Moderate rain',
73
+ 65: 'Heavy rain',
74
+ 66: 'Light freezing rain',
75
+ 67: 'Heavy freezing rain',
76
+ 71: 'Slight snowfall',
77
+ 73: 'Moderate snowfall',
78
+ 75: 'Heavy snowfall',
79
+ 77: 'Snow grains',
80
+ 80: 'Slight rain showers',
81
+ 81: 'Moderate rain showers',
82
+ 82: 'Violent rain showers',
83
+ 85: 'Slight snow showers',
84
+ 86: 'Heavy snow showers',
85
+ 95: 'Thunderstorm',
86
+ 96: 'Thunderstorm with slight hail',
87
+ 99: 'Thunderstorm with heavy hail',
88
+ };
89
+
90
+ function normalizeConnectionAccount(row, envStatus) {
91
+ if (!row) {
92
+ return {
93
+ id: null,
94
+ status: 'not_connected',
95
+ connected: false,
96
+ accountEmail: null,
97
+ lastConnectedAt: null,
98
+ accessMode: 'read_write',
99
+ };
100
+ }
101
+
102
+ return {
103
+ id: row.id || null,
104
+ status: row.status || 'not_connected',
105
+ connected: row.status === 'connected',
106
+ accountEmail: row.account_email || null,
107
+ lastConnectedAt: row.last_connected_at || null,
108
+ accessMode: 'read_write',
109
+ };
110
+ }
111
+
112
+ function sanitizeToolDefinition(definition) {
113
+ return {
114
+ ...definition,
115
+ description:
116
+ `${definition.description} When multiple Weather accounts are connected, set connection_id or account_email to choose which one to use.`,
117
+ parameters: {
118
+ ...(definition.parameters || { type: 'object', properties: {} }),
119
+ type: 'object',
120
+ properties: {
121
+ ...((definition.parameters && definition.parameters.properties) || {}),
122
+ connection_id: {
123
+ type: 'number',
124
+ description: 'Optional connected Weather account ID.',
125
+ },
126
+ account_email: {
127
+ type: 'string',
128
+ description: 'Optional connected Weather account identifier.',
129
+ },
130
+ },
131
+ required: Array.isArray(definition.parameters?.required)
132
+ ? definition.parameters.required.slice()
133
+ : [],
134
+ },
135
+ };
136
+ }
137
+
138
+ function toNumber(value, fallback = null) {
139
+ const num = Number(value);
140
+ return Number.isFinite(num) ? num : fallback;
141
+ }
142
+
143
+ function cleanLocationResult(item = {}) {
144
+ return {
145
+ name: item.name || null,
146
+ latitude: toNumber(item.latitude),
147
+ longitude: toNumber(item.longitude),
148
+ country: item.country || null,
149
+ admin1: item.admin1 || null,
150
+ timezone: item.timezone || null,
151
+ population: toNumber(item.population),
152
+ };
153
+ }
154
+
155
+ async function geocodeLocation(locationQuery, limit = 1) {
156
+ const query = String(locationQuery || '').trim();
157
+ if (!query) {
158
+ throw new Error('location is required when latitude/longitude are not provided.');
159
+ }
160
+ const result = await fetchJson(
161
+ `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=${Math.max(1, Math.min(Number(limit) || 1, 10))}&language=en&format=json`,
162
+ { method: 'GET' },
163
+ { serviceName: 'Open-Meteo geocoding' },
164
+ );
165
+ const matches = Array.isArray(result?.results) ? result.results : [];
166
+ if (matches.length === 0) {
167
+ throw new Error(`No location match found for "${query}".`);
168
+ }
169
+ return matches.map(cleanLocationResult);
170
+ }
171
+
172
+ async function resolveLocation(args = {}, connection = null) {
173
+ const latitude = toNumber(args.latitude, null);
174
+ const longitude = toNumber(args.longitude, null);
175
+ if (latitude !== null && longitude !== null) {
176
+ return {
177
+ latitude,
178
+ longitude,
179
+ label: `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`,
180
+ timezone: null,
181
+ };
182
+ }
183
+
184
+ const location = String(args.location || '').trim();
185
+ if (location) {
186
+ const [best] = await geocodeLocation(location, 1);
187
+ return {
188
+ latitude: best.latitude,
189
+ longitude: best.longitude,
190
+ label: [best.name, best.admin1, best.country].filter(Boolean).join(', ') || location,
191
+ timezone: best.timezone || null,
192
+ };
193
+ }
194
+
195
+ if (connection) {
196
+ try {
197
+ const metadata = JSON.parse(String(connection.metadata_json || '{}')) || {};
198
+ const metaLatitude = toNumber(metadata.latitude, null);
199
+ const metaLongitude = toNumber(metadata.longitude, null);
200
+ if (metaLatitude !== null && metaLongitude !== null) {
201
+ return {
202
+ latitude: metaLatitude,
203
+ longitude: metaLongitude,
204
+ label: String(metadata.locationLabel || `${metaLatitude.toFixed(4)}, ${metaLongitude.toFixed(4)}`),
205
+ timezone: String(metadata.timezone || '') || null,
206
+ };
207
+ }
208
+ } catch {
209
+ // Ignore malformed metadata and fail with a proper validation message below.
210
+ }
211
+ }
212
+
213
+ throw new Error('Provide either location or latitude/longitude for weather tools.');
214
+ }
215
+
216
+ function enrichCurrent(current = {}) {
217
+ const weatherCode = Number(current.weather_code);
218
+ return {
219
+ ...current,
220
+ weather_label: Number.isFinite(weatherCode) ? WEATHER_CODE_LABELS[weatherCode] || 'Unknown' : 'Unknown',
221
+ };
222
+ }
223
+
224
+ async function fetchForecastForLocation(location, forecastHours = 24) {
225
+ const horizon = Math.max(1, Math.min(Number(forecastHours) || 24, 72));
226
+ const url = new URL('https://api.open-meteo.com/v1/forecast');
227
+ url.searchParams.set('latitude', String(location.latitude));
228
+ url.searchParams.set('longitude', String(location.longitude));
229
+ url.searchParams.set(
230
+ 'current',
231
+ [
232
+ 'temperature_2m',
233
+ 'apparent_temperature',
234
+ 'precipitation',
235
+ 'rain',
236
+ 'showers',
237
+ 'snowfall',
238
+ 'weather_code',
239
+ 'wind_speed_10m',
240
+ 'wind_gusts_10m',
241
+ 'wind_direction_10m',
242
+ 'is_day',
243
+ ].join(','),
244
+ );
245
+ url.searchParams.set(
246
+ 'hourly',
247
+ [
248
+ 'temperature_2m',
249
+ 'precipitation',
250
+ 'rain',
251
+ 'showers',
252
+ 'snowfall',
253
+ 'weather_code',
254
+ 'wind_speed_10m',
255
+ 'wind_gusts_10m',
256
+ 'is_day',
257
+ ].join(','),
258
+ );
259
+ url.searchParams.set('forecast_hours', String(horizon));
260
+ url.searchParams.set('timezone', location.timezone || 'auto');
261
+
262
+ const result = await fetchJson(url.toString(), { method: 'GET' }, { serviceName: 'Open-Meteo forecast' });
263
+ const hourly = result?.hourly || {};
264
+ const times = Array.isArray(hourly.time) ? hourly.time : [];
265
+ const rows = times.map((time, index) => {
266
+ const weatherCode = Number(hourly.weather_code?.[index]);
267
+ return {
268
+ time,
269
+ temperature: toNumber(hourly.temperature_2m?.[index]),
270
+ precipitation: toNumber(hourly.precipitation?.[index], 0),
271
+ rain: toNumber(hourly.rain?.[index], 0),
272
+ showers: toNumber(hourly.showers?.[index], 0),
273
+ snowfall: toNumber(hourly.snowfall?.[index], 0),
274
+ windSpeed: toNumber(hourly.wind_speed_10m?.[index], 0),
275
+ windGust: toNumber(hourly.wind_gusts_10m?.[index], 0),
276
+ weatherCode: Number.isFinite(weatherCode) ? weatherCode : null,
277
+ weatherLabel: Number.isFinite(weatherCode) ? WEATHER_CODE_LABELS[weatherCode] || 'Unknown' : 'Unknown',
278
+ isDay: Number(hourly.is_day?.[index]) === 1,
279
+ };
280
+ });
281
+
282
+ return {
283
+ location: {
284
+ label: location.label,
285
+ latitude: toNumber(result?.latitude, location.latitude),
286
+ longitude: toNumber(result?.longitude, location.longitude),
287
+ timezone: result?.timezone || location.timezone || null,
288
+ elevation: toNumber(result?.elevation),
289
+ },
290
+ current: enrichCurrent(result?.current || {}),
291
+ hourly: rows,
292
+ };
293
+ }
294
+
295
+ class WeatherProvider {
296
+ constructor() {
297
+ this.key = 'weather';
298
+ this.label = 'Weather';
299
+ this.description = 'Official weather integration powered by Open-Meteo public APIs (no API key required).';
300
+ this.icon = 'weather';
301
+ this.apps = [{ ...WEATHER_APP }];
302
+ this.connectPrompt =
303
+ 'Connect Weather to use keyless forecast and current-condition tools, then trigger tasks on weather events.';
304
+ this.sessions = new Map();
305
+ this.sessionTTL = 30 * 60 * 1000;
306
+ this.#pruneTimer = setInterval(() => this.pruneExpiredSessions(), this.sessionTTL);
307
+ }
308
+
309
+ #pruneTimer = null;
310
+
311
+ pruneExpiredSessions() {
312
+ const now = Date.now();
313
+ for (const [id, session] of this.sessions) {
314
+ if (now - session.createdAt > this.sessionTTL) {
315
+ this.sessions.delete(id);
316
+ }
317
+ }
318
+ }
319
+
320
+ getApp(appId) {
321
+ return String(appId || '').trim() === WEATHER_APP.id ? { ...WEATHER_APP } : null;
322
+ }
323
+
324
+ getToolAppId(toolName) {
325
+ const normalized = String(toolName || '').trim();
326
+ return WEATHER_TOOL_DEFINITIONS.some((tool) => tool.name === normalized)
327
+ ? WEATHER_APP.id
328
+ : null;
329
+ }
330
+
331
+ getEnvStatus() {
332
+ return {
333
+ configured: true,
334
+ missing: [],
335
+ summary: 'Weather is ready for account connections.',
336
+ };
337
+ }
338
+
339
+ buildSnapshot(connectionRows) {
340
+ const env = this.getEnvStatus();
341
+ const accounts = (Array.isArray(connectionRows) ? connectionRows : [])
342
+ .slice()
343
+ .sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')))
344
+ .map((row) => normalizeConnectionAccount(row, env));
345
+ const connectedAccounts = accounts.filter((account) => account.connected);
346
+ const latestConnectedAt = connectedAccounts
347
+ .map((account) => account.lastConnectedAt)
348
+ .filter(Boolean)
349
+ .sort()
350
+ .reverse()[0] || null;
351
+
352
+ const appSnapshot = {
353
+ id: WEATHER_APP.id,
354
+ label: WEATHER_APP.label,
355
+ description: WEATHER_APP.description,
356
+ accounts,
357
+ connection: {
358
+ status: connectedAccounts.length > 0 ? 'connected' : 'not_connected',
359
+ connected: connectedAccounts.length > 0,
360
+ accountCount: connectedAccounts.length,
361
+ accountEmail: connectedAccounts.length === 1 ? connectedAccounts[0].accountEmail : null,
362
+ lastConnectedAt: latestConnectedAt,
363
+ },
364
+ availableToolCount: connectedAccounts.length > 0 ? WEATHER_TOOL_DEFINITIONS.length : 0,
365
+ };
366
+
367
+ return {
368
+ id: this.key,
369
+ label: this.label,
370
+ description: this.description,
371
+ icon: this.icon,
372
+ apps: [appSnapshot],
373
+ env,
374
+ connection: {
375
+ status: appSnapshot.connection.status,
376
+ connected: appSnapshot.connection.connected,
377
+ accountCount: appSnapshot.connection.accountCount,
378
+ appCount: appSnapshot.connection.connected ? 1 : 0,
379
+ accountEmail: appSnapshot.connection.accountEmail,
380
+ lastConnectedAt: appSnapshot.connection.lastConnectedAt,
381
+ },
382
+ availableToolCount: appSnapshot.availableToolCount,
383
+ connectPrompt: this.connectPrompt,
384
+ };
385
+ }
386
+
387
+ summarizeForModel(snapshot) {
388
+ if (!snapshot?.connection?.connected) {
389
+ return 'Weather: available but not connected yet. Ask the user to connect Weather in Official Integrations.';
390
+ }
391
+ return 'Weather: connected with keyless Open-Meteo tools for geocoding, current conditions, and hourly forecast.';
392
+ }
393
+
394
+ getToolDefinitions({ connectedAppIds } = {}) {
395
+ const appIds = new Set(Array.isArray(connectedAppIds) ? connectedAppIds : []);
396
+ if (!appIds.has(WEATHER_APP.id)) {
397
+ return [];
398
+ }
399
+ return WEATHER_TOOL_DEFINITIONS.map(sanitizeToolDefinition);
400
+ }
401
+
402
+ supportsTool(toolName) {
403
+ return WEATHER_TOOL_DEFINITIONS.some((tool) => tool.name === String(toolName || '').trim());
404
+ }
405
+
406
+ async beginConnection({ userId, agentId, appKey }) {
407
+ if (!this.getApp(appKey)) {
408
+ throw new Error(`Unknown ${this.label} app: ${appKey || 'missing app key'}`);
409
+ }
410
+
411
+ const accountEmail = 'public@open-meteo';
412
+ db.prepare(
413
+ `INSERT INTO integration_connections (
414
+ user_id,
415
+ agent_id,
416
+ provider_key,
417
+ app_key,
418
+ status,
419
+ account_email,
420
+ scopes_json,
421
+ credentials_json,
422
+ metadata_json,
423
+ last_connected_at,
424
+ updated_at
425
+ ) VALUES (?, ?, ?, ?, 'connected', ?, ?, ?, ?, datetime('now'), datetime('now'))
426
+ ON CONFLICT(user_id, agent_id, provider_key, app_key, account_email) DO UPDATE SET
427
+ status = 'connected',
428
+ scopes_json = excluded.scopes_json,
429
+ credentials_json = excluded.credentials_json,
430
+ metadata_json = excluded.metadata_json,
431
+ last_connected_at = excluded.last_connected_at,
432
+ updated_at = excluded.updated_at`,
433
+ ).run(
434
+ userId,
435
+ agentId,
436
+ this.key,
437
+ WEATHER_APP.id,
438
+ accountEmail,
439
+ JSON.stringify(['open-meteo:public']),
440
+ JSON.stringify({}),
441
+ JSON.stringify({ source: 'open-meteo', mode: 'public' }),
442
+ );
443
+
444
+ const connection = db.prepare(
445
+ `SELECT id FROM integration_connections
446
+ WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND app_key = ? AND account_email = ?`,
447
+ ).get(userId, agentId, this.key, WEATHER_APP.id, accountEmail);
448
+
449
+ const sessionId = crypto.randomBytes(18).toString('hex');
450
+ this.sessions.set(sessionId, {
451
+ id: sessionId,
452
+ userId,
453
+ agentId,
454
+ appKey: WEATHER_APP.id,
455
+ status: 'connected',
456
+ connectionId: connection?.id || null,
457
+ accountEmail,
458
+ createdAt: Date.now(),
459
+ });
460
+
461
+ return {
462
+ provider: this.key,
463
+ appId: WEATHER_APP.id,
464
+ status: 'interactive_connect',
465
+ sessionId,
466
+ url: `/api/integrations/${this.key}/connect/${sessionId}`,
467
+ };
468
+ }
469
+
470
+ getConnectionSession(userId, providerKey, sessionId, agentId = null) {
471
+ if (providerKey !== this.key) {
472
+ return null;
473
+ }
474
+ const session = this.sessions.get(String(sessionId || '').trim());
475
+ if (!session) {
476
+ return null;
477
+ }
478
+ if (session.userId !== userId || String(session.agentId || '') !== String(agentId || '')) {
479
+ return null;
480
+ }
481
+ return {
482
+ id: session.id,
483
+ provider: this.key,
484
+ appId: session.appKey,
485
+ status: session.status,
486
+ connectionId: session.connectionId,
487
+ accountEmail: session.accountEmail,
488
+ error: null,
489
+ qr: null,
490
+ };
491
+ }
492
+
493
+ async disconnect(sessionId = null) {
494
+ if (sessionId) {
495
+ this.sessions.delete(String(sessionId));
496
+ }
497
+ return null;
498
+ }
499
+
500
+ shutdown() {
501
+ if (this.#pruneTimer) {
502
+ clearInterval(this.#pruneTimer);
503
+ this.#pruneTimer = null;
504
+ }
505
+ }
506
+
507
+ async executeTool(toolName, args, connection) {
508
+ switch (toolName) {
509
+ case 'weather_search_locations': {
510
+ const query = String(args.query || '').trim();
511
+ if (!query) {
512
+ throw new Error('query is required.');
513
+ }
514
+ const limit = Math.max(1, Math.min(Number(args.limit) || 5, 10));
515
+ const results = await geocodeLocation(query, limit);
516
+ return {
517
+ result: {
518
+ query,
519
+ count: results.length,
520
+ results,
521
+ },
522
+ };
523
+ }
524
+ case 'weather_get_current': {
525
+ const location = await resolveLocation(args, connection);
526
+ const forecast = await fetchForecastForLocation(location, 1);
527
+ return {
528
+ result: {
529
+ location: forecast.location,
530
+ current: forecast.current,
531
+ },
532
+ };
533
+ }
534
+ case 'weather_get_forecast': {
535
+ const location = await resolveLocation(args, connection);
536
+ const forecastHours = Math.max(1, Math.min(Number(args.forecast_hours) || 24, 72));
537
+ const forecast = await fetchForecastForLocation(location, forecastHours);
538
+ return {
539
+ result: {
540
+ location: forecast.location,
541
+ current: forecast.current,
542
+ hourly: forecast.hourly,
543
+ horizonHours: forecastHours,
544
+ },
545
+ };
546
+ }
547
+ default:
548
+ return null;
549
+ }
550
+ }
551
+ }
552
+
553
+ function createWeatherProvider() {
554
+ return new WeatherProvider();
555
+ }
556
+
557
+ module.exports = {
558
+ createWeatherProvider,
559
+ };
@@ -545,7 +545,7 @@ class WhatsAppPersonalProvider extends EventEmitter {
545
545
  chatCount: client.chats.size,
546
546
  cachedMessageChats: client.messages.size,
547
547
  syncNote:
548
- 'Read tools only expose chats and messages synchronized through this personal integration session.',
548
+ 'This account is linked and connected. Read tools expose only chats and messages currently available in this session cache.',
549
549
  },
550
550
  };
551
551
  case 'whatsapp_personal_list_chats': {
@@ -557,10 +557,14 @@ class WhatsAppPersonalProvider extends EventEmitter {
557
557
  .slice(0, limit);
558
558
  return {
559
559
  result: {
560
+ account: client.accountEmail || connection.account_email || null,
561
+ connected: client.status === 'connected',
560
562
  chats,
561
563
  count: chats.length,
562
564
  syncNote:
563
- 'Only chats synchronized after linking this personal integration are available here.',
565
+ chats.length > 0
566
+ ? 'Chats shown here come from the current personal WhatsApp cache.'
567
+ : 'The personal WhatsApp account is connected, but no chats are cached yet. The cache populates as history/events are synchronized.',
564
568
  },
565
569
  };
566
570
  }
@@ -24,6 +24,7 @@ const { RuntimeManager } = require('./runtime/manager');
24
24
  const { BrowserExtensionRegistry } = require('./browser/extension/registry');
25
25
  const { DesktopCompanionRegistry } = require('./desktop/registry');
26
26
  const { DesktopProvider } = require('./desktop/provider');
27
+ const { ScreenRecorder } = require('./desktop/screenRecorder');
27
28
  const { assertRuntimeValidation, getRuntimeValidation } = require('./runtime/validation');
28
29
  const {
29
30
  getErrorMessage,
@@ -428,6 +429,17 @@ function createWidgetService(app) {
428
429
  return widgetService;
429
430
  }
430
431
 
432
+ function createScreenRecorder(app) {
433
+ const screenRecorder = registerLocal(
434
+ app,
435
+ 'screenRecorder',
436
+ new ScreenRecorder(),
437
+ );
438
+ screenRecorder.start();
439
+ logServiceReady('Screen recorder started');
440
+ return screenRecorder;
441
+ }
442
+
431
443
  function restoreMessagingConnections(messagingManager) {
432
444
  void runBackgroundTask('[Messaging] Restore error:', () =>
433
445
  messagingManager.restoreConnections(),
@@ -513,6 +525,7 @@ async function startServices(app, io) {
513
525
  const messagingManager = createMessagingManager(app, io, agentEngine);
514
526
  const recordingManager = createRecordingManager(app, io);
515
527
  createWidgetService(app);
528
+ createScreenRecorder(app);
516
529
 
517
530
  restoreMessagingConnections(messagingManager);
518
531
  restoreMcpClients(mcpClient);
@@ -639,6 +652,15 @@ async function stopServices(app) {
639
652
  }
640
653
  }
641
654
 
655
+ if (app.locals.screenRecorder) {
656
+ try {
657
+ app.locals.screenRecorder.stop();
658
+ logServiceReady('Screen recorder stopped');
659
+ } catch (err) {
660
+ console.error('[ScreenRecorder] Stop error:', getErrorMessage(err));
661
+ }
662
+ }
663
+
642
664
  if (app.locals.browserExtensionRegistry) {
643
665
  try {
644
666
  app.locals.browserExtensionRegistry.closeAll();
@@ -12,6 +12,11 @@ const {
12
12
  const { getMemoryStorageDecision } = require('./policy');
13
13
  const { AGENT_DATA_DIR } = require('../../../runtime/paths');
14
14
  const { isMainAgent, resolveAgentId } = require('../agents/manager');
15
+ const {
16
+ decryptLocalValue,
17
+ encryptLocalValue,
18
+ isLocalEncryptedValue,
19
+ } = require('../../utils/local_secrets');
15
20
 
16
21
  async function getActiveProvider(userId, agentId = null) {
17
22
  try {
@@ -516,11 +521,43 @@ class MemoryManager {
516
521
  return {};
517
522
  }
518
523
  }
519
- try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return {}; }
524
+ try {
525
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
526
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
527
+ return {};
528
+ }
529
+
530
+ let shouldMigrate = false;
531
+ const normalized = {};
532
+
533
+ for (const [service, rawValue] of Object.entries(parsed)) {
534
+ const value = String(rawValue || '');
535
+ if (!value) {
536
+ normalized[service] = '';
537
+ continue;
538
+ }
539
+
540
+ if (!isLocalEncryptedValue(value)) shouldMigrate = true;
541
+ normalized[service] = decryptLocalValue(value);
542
+ }
543
+
544
+ if (shouldMigrate) {
545
+ this.writeApiKeys(normalized, userId);
546
+ }
547
+
548
+ return normalized;
549
+ } catch {
550
+ return {};
551
+ }
520
552
  }
521
553
 
522
554
  writeApiKeys(keys, userId = null) {
523
- fs.writeFileSync(this._userApiKeysPath(userId), JSON.stringify(keys, null, 2), 'utf-8');
555
+ const encrypted = {};
556
+ for (const [service, rawValue] of Object.entries(keys || {})) {
557
+ const value = String(rawValue || '');
558
+ encrypted[service] = value ? encryptLocalValue(value) : '';
559
+ }
560
+ fs.writeFileSync(this._userApiKeysPath(userId), JSON.stringify(encrypted, null, 2), 'utf-8');
524
561
  }
525
562
 
526
563
  setApiKey(service, key, userId = null) {