neoagent 2.4.1-beta.28 → 2.4.1-beta.29
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/flutter_app/lib/main_controller.dart +102 -0
- package/flutter_app/lib/main_devices.dart +305 -1
- package/flutter_app/lib/main_integrations.dart +254 -7
- package/flutter_app/lib/src/backend_client.dart +35 -0
- package/package.json +1 -1
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +76175 -75500
- package/server/routes/workspace.js +86 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/workspace/manager.js +116 -0
|
@@ -189,8 +189,8 @@ class OfficialIntegrationsTab extends StatelessWidget {
|
|
|
189
189
|
? item.env.summary
|
|
190
190
|
: item.hasExpiredAccounts
|
|
191
191
|
? item.id == 'google_workspace'
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
? 'One or more accounts expired. Reconnect to restore access. If this keeps happening, your Google Cloud OAuth app may be in Testing mode — publish it to Production in Google Cloud Console to get long-lived tokens.'
|
|
193
|
+
: 'One or more accounts expired. Reconnect the affected account to restore tool access.'
|
|
194
194
|
: !item.supportsMultipleAccounts && item.isConnected
|
|
195
195
|
? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
|
|
196
196
|
: item.isConnected
|
|
@@ -228,12 +228,255 @@ void _openOfficialIntegrationSetupDialog(
|
|
|
228
228
|
String providerId,
|
|
229
229
|
) {
|
|
230
230
|
switch (providerId) {
|
|
231
|
+
case 'home_assistant':
|
|
232
|
+
_showHomeAssistantSetupDialog(context, controller);
|
|
233
|
+
return;
|
|
231
234
|
case 'trello':
|
|
232
235
|
_showTrelloSetupDialog(context, controller);
|
|
233
236
|
return;
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
Future<void> _showHomeAssistantSetupDialog(
|
|
241
|
+
BuildContext context,
|
|
242
|
+
NeoAgentController controller,
|
|
243
|
+
) async {
|
|
244
|
+
Map<String, dynamic> existing;
|
|
245
|
+
try {
|
|
246
|
+
existing = await controller.getOfficialIntegrationConfig('home_assistant');
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (context.mounted) {
|
|
249
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
250
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
|
|
257
|
+
final hasToken = existing['hasToken'] == true;
|
|
258
|
+
final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
|
|
259
|
+
final hasConnectedAccount =
|
|
260
|
+
existing['hasConnectedAccount'] == true || accountCount > 0;
|
|
261
|
+
var formError = '';
|
|
262
|
+
var saving = false;
|
|
263
|
+
|
|
264
|
+
final baseUrlController = TextEditingController(text: savedBaseUrl);
|
|
265
|
+
final tokenController = TextEditingController();
|
|
266
|
+
|
|
267
|
+
if (!context.mounted) return;
|
|
268
|
+
await showDialog<void>(
|
|
269
|
+
context: context,
|
|
270
|
+
barrierDismissible: false,
|
|
271
|
+
builder: (dialogContext) {
|
|
272
|
+
return StatefulBuilder(
|
|
273
|
+
builder: (dialogContext, setState) {
|
|
274
|
+
return AlertDialog(
|
|
275
|
+
title: const Text('Home Assistant Setup'),
|
|
276
|
+
content: SizedBox(
|
|
277
|
+
width: 520,
|
|
278
|
+
child: Column(
|
|
279
|
+
mainAxisSize: MainAxisSize.min,
|
|
280
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
281
|
+
children: <Widget>[
|
|
282
|
+
Text(
|
|
283
|
+
'Connect a public HTTPS Home Assistant endpoint with a Long-Lived Access Token. Local, loopback, and private network addresses are blocked by the server.',
|
|
284
|
+
style: TextStyle(color: _textSecondary),
|
|
285
|
+
),
|
|
286
|
+
const SizedBox(height: 16),
|
|
287
|
+
_IntegrationSetupStatusItem(
|
|
288
|
+
label: 'Endpoint',
|
|
289
|
+
status: savedBaseUrl.trim().isNotEmpty
|
|
290
|
+
? 'Configured'
|
|
291
|
+
: 'Not configured',
|
|
292
|
+
isConnected: savedBaseUrl.trim().isNotEmpty,
|
|
293
|
+
),
|
|
294
|
+
const SizedBox(height: 12),
|
|
295
|
+
_IntegrationSetupStatusItem(
|
|
296
|
+
label: 'Connected Instance',
|
|
297
|
+
status: hasConnectedAccount
|
|
298
|
+
? '$accountCount ${accountCount == 1 ? 'instance' : 'instances'} connected'
|
|
299
|
+
: 'Not connected',
|
|
300
|
+
isConnected: hasConnectedAccount,
|
|
301
|
+
),
|
|
302
|
+
const SizedBox(height: 12),
|
|
303
|
+
TextField(
|
|
304
|
+
controller: baseUrlController,
|
|
305
|
+
onChanged: (_) => setState(() {}),
|
|
306
|
+
keyboardType: TextInputType.url,
|
|
307
|
+
decoration: const InputDecoration(
|
|
308
|
+
labelText: 'Home Assistant URL',
|
|
309
|
+
hintText: 'https://your-instance.example.com',
|
|
310
|
+
border: OutlineInputBorder(),
|
|
311
|
+
),
|
|
312
|
+
),
|
|
313
|
+
const SizedBox(height: 12),
|
|
314
|
+
TextField(
|
|
315
|
+
controller: tokenController,
|
|
316
|
+
onChanged: (_) => setState(() {}),
|
|
317
|
+
obscureText: true,
|
|
318
|
+
decoration: InputDecoration(
|
|
319
|
+
labelText: hasToken
|
|
320
|
+
? 'Paste replacement Long-Lived Access Token'
|
|
321
|
+
: 'Long-Lived Access Token',
|
|
322
|
+
border: const OutlineInputBorder(),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
if (hasToken) ...<Widget>[
|
|
326
|
+
const SizedBox(height: 8),
|
|
327
|
+
Text(
|
|
328
|
+
'Leave the token empty to keep the currently stored token.',
|
|
329
|
+
style: TextStyle(color: _textSecondary, fontSize: 12),
|
|
330
|
+
),
|
|
331
|
+
],
|
|
332
|
+
if (formError.isNotEmpty) ...<Widget>[
|
|
333
|
+
const SizedBox(height: 12),
|
|
334
|
+
Container(
|
|
335
|
+
padding: const EdgeInsets.all(8),
|
|
336
|
+
decoration: BoxDecoration(
|
|
337
|
+
color: _danger.withValues(alpha: 0.1),
|
|
338
|
+
borderRadius: BorderRadius.circular(4),
|
|
339
|
+
border: Border.all(
|
|
340
|
+
color: _danger.withValues(alpha: 0.3),
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
child: Text(
|
|
344
|
+
formError,
|
|
345
|
+
style: TextStyle(color: _danger, fontSize: 12),
|
|
346
|
+
),
|
|
347
|
+
),
|
|
348
|
+
],
|
|
349
|
+
],
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
actions: <Widget>[
|
|
353
|
+
if (savedBaseUrl.trim().isNotEmpty || hasToken)
|
|
354
|
+
TextButton(
|
|
355
|
+
onPressed: saving
|
|
356
|
+
? null
|
|
357
|
+
: () async {
|
|
358
|
+
final shouldClear =
|
|
359
|
+
await showDialog<bool>(
|
|
360
|
+
context: dialogContext,
|
|
361
|
+
builder: (context) {
|
|
362
|
+
return AlertDialog(
|
|
363
|
+
title: const Text(
|
|
364
|
+
'Disconnect Home Assistant?',
|
|
365
|
+
),
|
|
366
|
+
content: const Text(
|
|
367
|
+
'This removes the Home Assistant setup and connected instance for this agent.',
|
|
368
|
+
),
|
|
369
|
+
actions: [
|
|
370
|
+
TextButton(
|
|
371
|
+
onPressed: () =>
|
|
372
|
+
Navigator.of(context).pop(false),
|
|
373
|
+
child: const Text('Cancel'),
|
|
374
|
+
),
|
|
375
|
+
FilledButton(
|
|
376
|
+
onPressed: () =>
|
|
377
|
+
Navigator.of(context).pop(true),
|
|
378
|
+
child: const Text('Disconnect'),
|
|
379
|
+
),
|
|
380
|
+
],
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
) ??
|
|
384
|
+
false;
|
|
385
|
+
if (!shouldClear) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
setState(() {
|
|
389
|
+
formError = '';
|
|
390
|
+
saving = true;
|
|
391
|
+
});
|
|
392
|
+
try {
|
|
393
|
+
await controller.clearOfficialIntegrationConfig(
|
|
394
|
+
'home_assistant',
|
|
395
|
+
);
|
|
396
|
+
if (dialogContext.mounted) {
|
|
397
|
+
Navigator.of(dialogContext).pop();
|
|
398
|
+
}
|
|
399
|
+
} catch (_) {
|
|
400
|
+
setState(() {
|
|
401
|
+
formError =
|
|
402
|
+
controller.errorMessage ??
|
|
403
|
+
'Could not disconnect Home Assistant.';
|
|
404
|
+
saving = false;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
child: const Text('Disconnect'),
|
|
409
|
+
),
|
|
410
|
+
TextButton(
|
|
411
|
+
onPressed: saving
|
|
412
|
+
? null
|
|
413
|
+
: () => Navigator.of(dialogContext).pop(),
|
|
414
|
+
child: const Text('Close'),
|
|
415
|
+
),
|
|
416
|
+
FilledButton(
|
|
417
|
+
onPressed: saving
|
|
418
|
+
? null
|
|
419
|
+
: () async {
|
|
420
|
+
setState(() {
|
|
421
|
+
formError = '';
|
|
422
|
+
saving = true;
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
final baseUrl = baseUrlController.text.trim();
|
|
426
|
+
final token = tokenController.text.trim();
|
|
427
|
+
if (baseUrl.isEmpty) {
|
|
428
|
+
setState(() {
|
|
429
|
+
formError = 'Home Assistant URL is required.';
|
|
430
|
+
saving = false;
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (token.isEmpty && !hasToken) {
|
|
435
|
+
setState(() {
|
|
436
|
+
formError =
|
|
437
|
+
'Home Assistant Long-Lived Access Token is required.';
|
|
438
|
+
saving = false;
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await controller.saveOfficialIntegrationConfig(
|
|
443
|
+
'home_assistant',
|
|
444
|
+
config: <String, dynamic>{
|
|
445
|
+
'baseUrl': baseUrl,
|
|
446
|
+
if (token.isNotEmpty) 'token': token,
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
if (dialogContext.mounted) {
|
|
450
|
+
Navigator.of(dialogContext).pop();
|
|
451
|
+
}
|
|
452
|
+
} catch (_) {
|
|
453
|
+
setState(() {
|
|
454
|
+
formError =
|
|
455
|
+
controller.errorMessage ??
|
|
456
|
+
'Could not save Home Assistant setup.';
|
|
457
|
+
saving = false;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
child: Text(
|
|
462
|
+
saving
|
|
463
|
+
? 'Saving...'
|
|
464
|
+
: hasConnectedAccount
|
|
465
|
+
? 'Update Instance'
|
|
466
|
+
: 'Connect Instance',
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
],
|
|
470
|
+
);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
baseUrlController.dispose();
|
|
477
|
+
tokenController.dispose();
|
|
478
|
+
}
|
|
479
|
+
|
|
237
480
|
Future<void> _showTrelloSetupDialog(
|
|
238
481
|
BuildContext context,
|
|
239
482
|
NeoAgentController controller,
|
|
@@ -283,13 +526,13 @@ Future<void> _showTrelloSetupDialog(
|
|
|
283
526
|
style: TextStyle(color: _textSecondary),
|
|
284
527
|
),
|
|
285
528
|
const SizedBox(height: 16),
|
|
286
|
-
|
|
529
|
+
_IntegrationSetupStatusItem(
|
|
287
530
|
label: 'API Key',
|
|
288
531
|
status: apiKeyConfigured ? 'Configured' : 'Not configured',
|
|
289
532
|
isConnected: apiKeyConfigured,
|
|
290
533
|
),
|
|
291
534
|
const SizedBox(height: 12),
|
|
292
|
-
|
|
535
|
+
_IntegrationSetupStatusItem(
|
|
293
536
|
label: 'Connected Account',
|
|
294
537
|
status: hasConnectedAccount
|
|
295
538
|
? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
|
|
@@ -337,7 +580,9 @@ Future<void> _showTrelloSetupDialog(
|
|
|
337
580
|
decoration: BoxDecoration(
|
|
338
581
|
color: _danger.withValues(alpha: 0.1),
|
|
339
582
|
borderRadius: BorderRadius.circular(4),
|
|
340
|
-
border: Border.all(
|
|
583
|
+
border: Border.all(
|
|
584
|
+
color: _danger.withValues(alpha: 0.3),
|
|
585
|
+
),
|
|
341
586
|
),
|
|
342
587
|
child: Text(
|
|
343
588
|
formError,
|
|
@@ -532,8 +777,8 @@ Future<void> _showTrelloSetupDialog(
|
|
|
532
777
|
tokenInputController.dispose();
|
|
533
778
|
}
|
|
534
779
|
|
|
535
|
-
class
|
|
536
|
-
const
|
|
780
|
+
class _IntegrationSetupStatusItem extends StatelessWidget {
|
|
781
|
+
const _IntegrationSetupStatusItem({
|
|
537
782
|
required this.label,
|
|
538
783
|
required this.status,
|
|
539
784
|
required this.isConnected,
|
|
@@ -886,11 +1131,13 @@ class _OfficialIntegrationIcon extends StatelessWidget {
|
|
|
886
1131
|
Widget build(BuildContext context) {
|
|
887
1132
|
final color = switch (item.icon) {
|
|
888
1133
|
'google' => const Color(0xFF4285F4),
|
|
1134
|
+
'home_assistant' => const Color(0xFF41BDF5),
|
|
889
1135
|
'trello' => const Color(0xFF0C66E4),
|
|
890
1136
|
_ => _accent,
|
|
891
1137
|
};
|
|
892
1138
|
final label = switch (item.icon) {
|
|
893
1139
|
'google' => 'G',
|
|
1140
|
+
'home_assistant' => 'H',
|
|
894
1141
|
'trello' => 'T',
|
|
895
1142
|
_ => item.label.isNotEmpty ? item.label[0] : '?',
|
|
896
1143
|
};
|
|
@@ -695,6 +695,41 @@ class BackendClient {
|
|
|
695
695
|
return getMap(baseUrl, '/api/desktop/devices');
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
+
Future<Map<String, dynamic>> fetchWorkspaceDirectory(
|
|
699
|
+
String baseUrl, {
|
|
700
|
+
String path = '.',
|
|
701
|
+
}) async {
|
|
702
|
+
return getMap(
|
|
703
|
+
baseUrl,
|
|
704
|
+
'/api/workspace/files?path=${Uri.encodeQueryComponent(path)}',
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
Future<Map<String, dynamic>> fetchWorkspaceFile(
|
|
709
|
+
String baseUrl, {
|
|
710
|
+
required String path,
|
|
711
|
+
}) async {
|
|
712
|
+
return getMap(
|
|
713
|
+
baseUrl,
|
|
714
|
+
'/api/workspace/files/content?path=${Uri.encodeQueryComponent(path)}',
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
Future<Map<String, dynamic>> saveWorkspaceFile(
|
|
719
|
+
String baseUrl, {
|
|
720
|
+
required String path,
|
|
721
|
+
required String content,
|
|
722
|
+
}) async {
|
|
723
|
+
return putMap(baseUrl, '/api/workspace/files/content', <String, dynamic>{
|
|
724
|
+
'path': path,
|
|
725
|
+
'content': content,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
String workspaceDownloadPath(String path) {
|
|
730
|
+
return '/api/workspace/files/download?path=${Uri.encodeQueryComponent(path)}';
|
|
731
|
+
}
|
|
732
|
+
|
|
698
733
|
Future<Map<String, dynamic>> selectDesktopDevice(
|
|
699
734
|
String baseUrl, {
|
|
700
735
|
required String deviceId,
|
package/package.json
CHANGED
package/server/http/routes.js
CHANGED
|
@@ -18,6 +18,7 @@ const routeRegistry = [
|
|
|
18
18
|
{ basePath: '/api/skills', modulePath: '../routes/skills' },
|
|
19
19
|
{ basePath: '/api/store', modulePath: '../routes/store' },
|
|
20
20
|
{ basePath: '/api/artifacts', modulePath: '../routes/artifacts' },
|
|
21
|
+
{ basePath: '/api/workspace', modulePath: '../routes/workspace' },
|
|
21
22
|
{ basePath: '/api/memory', modulePath: '../routes/memory' },
|
|
22
23
|
{ basePath: '/api/tasks', modulePath: '../routes/tasks' },
|
|
23
24
|
{ basePath: '/api/widgets', modulePath: '../routes/widgets' },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7ba231f310e272fc741c3f39d24d2198
|
|
Binary file
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"c416acfeb8126e097f758c664aaa3da929e27d
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "515105998" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|