neoagent 2.4.0 → 2.4.1-beta.6

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 (30) hide show
  1. package/LICENSE +619 -21
  2. package/README.md +1 -1
  3. package/extensions/chrome-browser/icons/icon128.png +0 -0
  4. package/extensions/chrome-browser/icons/icon16.png +0 -0
  5. package/extensions/chrome-browser/icons/icon48.png +0 -0
  6. package/extensions/chrome-browser/icons/logo.svg +12 -0
  7. package/extensions/chrome-browser/manifest.json +11 -1
  8. package/extensions/chrome-browser/popup.css +5 -0
  9. package/extensions/chrome-browser/popup.html +2 -4
  10. package/extensions/chrome-browser/popup.js +1 -5
  11. package/flutter_app/lib/main_controller.dart +16 -0
  12. package/flutter_app/lib/main_devices.dart +70 -1
  13. package/flutter_app/lib/main_settings.dart +215 -31
  14. package/flutter_app/lib/src/backend_client.dart +12 -0
  15. package/flutter_app/lib/src/desktop_companion_actions.dart +72 -0
  16. package/flutter_app/lib/src/desktop_companion_io.dart +9 -4
  17. package/flutter_app/macos/Runner/AppDelegate.swift +12 -1
  18. package/package.json +2 -2
  19. package/server/guest_agent.js +12 -1
  20. package/server/http/routes.js +190 -0
  21. package/server/public/.last_build_id +1 -1
  22. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  23. package/server/public/flutter_bootstrap.js +1 -1
  24. package/server/public/main.dart.js +51613 -51262
  25. package/server/services/ai/tools.js +39 -28
  26. package/server/services/desktop/protocol.js +1 -0
  27. package/server/services/desktop/provider.js +11 -0
  28. package/server/services/desktop/registry.js +51 -10
  29. package/server/services/runtime/docker-vm-manager.js +26 -3
  30. package/server/services/runtime/manager.js +25 -2
@@ -52,7 +52,8 @@ class DesktopCompanionManager extends ChangeNotifier {
52
52
 
53
53
  Future<void> bootstrap(SharedPreferences prefs) async {
54
54
  _enabled = prefs.getBool(desktopCompanionEnabledPrefsKey) ?? false;
55
- _paused = prefs.getBool(desktopCompanionPausedPrefsKey) ?? false;
55
+ // Always start unpaused — paused state must not carry over across restarts.
56
+ _paused = false;
56
57
  _label =
57
58
  prefs.getString(desktopCompanionLabelPrefsKey)?.trim() ??
58
59
  _defaultLabel();
@@ -116,7 +117,6 @@ class DesktopCompanionManager extends ChangeNotifier {
116
117
 
117
118
  Future<void> setPaused(bool value, SharedPreferences prefs) async {
118
119
  _paused = value;
119
- await prefs.setBool(desktopCompanionPausedPrefsKey, value);
120
120
  notifyListeners();
121
121
  if (_connected) {
122
122
  await _sendEvent('statusChanged', <String, Object?>{'paused': value});
@@ -387,10 +387,15 @@ class DesktopCompanionManager extends ChangeNotifier {
387
387
  case 'pauseControl':
388
388
  final paused = payload['paused'] != false;
389
389
  _paused = paused;
390
- final prefs = await SharedPreferences.getInstance();
391
- await prefs.setBool(desktopCompanionPausedPrefsKey, paused);
392
390
  notifyListeners();
393
391
  return <String, Object?>{'success': true, 'paused': _paused};
392
+ case 'executeCommand':
393
+ return _actions.executeShellCommand(
394
+ command: payload['command']?.toString() ?? '',
395
+ cwd: payload['cwd']?.toString(),
396
+ timeoutMs: (payload['timeout'] as num?)?.toInt(),
397
+ stdinInput: payload['stdin_input']?.toString(),
398
+ );
394
399
  case 'ping':
395
400
  return <String, Object?>{'pong': true};
396
401
  default:
@@ -316,7 +316,18 @@ final class DesktopCompanionNativePlugin: NSObject {
316
316
  }
317
317
 
318
318
  private func isAccessibilityTrusted() -> Bool {
319
- AXIsProcessTrusted()
319
+ if AXIsProcessTrusted() { return true }
320
+ // AXIsProcessTrusted() may cache false on macOS 14+ after a System Settings grant.
321
+ // Probe with a live AX read: .apiDisabled is the error returned when the process
322
+ // lacks accessibility permission (AXError has no .notTrusted case in the macOS SDK).
323
+ let sysElement = AXUIElementCreateSystemWide()
324
+ var value: CFTypeRef?
325
+ let status = AXUIElementCopyAttributeValue(
326
+ sysElement,
327
+ kAXFocusedApplicationAttribute as CFString,
328
+ &value
329
+ )
330
+ return status != .apiDisabled
320
331
  }
321
332
 
322
333
  private func resolveDisplayId(_ raw: String?) -> CGDirectDisplayID {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.0",
3
+ "version": "2.4.1-beta.6",
4
4
  "description": "Proactive personal AI agent with no limits",
5
- "license": "MIT",
5
+ "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
7
7
  "engines": {
8
8
  "node": ">=20"
@@ -62,7 +62,18 @@ function isInsideAllowedRoots(targetPath) {
62
62
  }
63
63
 
64
64
  function requireToken(req, res, next) {
65
- next();
65
+ if (!AUTH_TOKEN) {
66
+ // Token not configured in this environment — allow but unauthenticated.
67
+ // Pass NEOAGENT_VM_GUEST_TOKEN to the container to enforce auth.
68
+ return next();
69
+ }
70
+ const header = String(req.headers?.authorization || '').trim();
71
+ const prefix = 'Bearer ';
72
+ const provided = header.startsWith(prefix) ? header.slice(prefix.length).trim() : '';
73
+ if (!provided || provided !== AUTH_TOKEN) {
74
+ return res.status(401).json({ error: 'Unauthorized.' });
75
+ }
76
+ return next();
66
77
  }
67
78
 
68
79
  function sanitizeError(err) {
@@ -67,6 +67,196 @@ function registerApiRoutes(app) {
67
67
  });
68
68
  });
69
69
 
70
+ app.get('/api/system/health-check', requireAuth, async (req, res) => {
71
+ const userId = req.session?.userId;
72
+ const runtimeManager = req.app?.locals?.runtimeManager;
73
+ const desktopRegistry = req.app?.locals?.desktopCompanionRegistry;
74
+ const extensionRegistry = req.app?.locals?.browserExtensionRegistry;
75
+ const results = [];
76
+
77
+ // 1. Backend connectivity — trivially true if we got here.
78
+ results.push({ id: 'backend', label: 'Backend server', passed: true, detail: 'Reachable' });
79
+
80
+ // 2. Cloud VM runtime availability.
81
+ const runtimeValidation = getRuntimeValidation(runtimeManager);
82
+ const runtimeReady = Boolean(runtimeValidation?.ready);
83
+ results.push({
84
+ id: 'vm_runtime',
85
+ label: 'Cloud VM runtime',
86
+ passed: runtimeReady,
87
+ detail: runtimeReady ? 'Available' : String(runtimeValidation?.issues?.[0] || 'Not configured'),
88
+ });
89
+
90
+ // 3. Cloud VM CLI execution — actually run a command.
91
+ if (runtimeManager && typeof runtimeManager.executeCommand === 'function') {
92
+ try {
93
+ const cmdResult = await runtimeManager.executeCommand(userId, 'echo "health_check_ok"', { timeout: 15000 });
94
+ const exitOk = cmdResult?.exitCode === 0;
95
+ const outputOk = String(cmdResult?.stdout || '').includes('health_check_ok');
96
+ results.push({
97
+ id: 'vm_cli',
98
+ label: 'Cloud VM — command execution',
99
+ passed: exitOk && outputOk,
100
+ detail: exitOk && outputOk
101
+ ? 'Commands running'
102
+ : `Exit ${cmdResult?.exitCode ?? '?'}: ${String(cmdResult?.stderr || cmdResult?.stdout || '').slice(0, 120)}`,
103
+ });
104
+ } catch (err) {
105
+ results.push({ id: 'vm_cli', label: 'Cloud VM — command execution', passed: false, detail: String(err?.message || err).slice(0, 120) });
106
+ }
107
+ } else {
108
+ results.push({ id: 'vm_cli', label: 'Cloud VM — command execution', passed: false, detail: 'VM runtime unavailable' });
109
+ }
110
+
111
+ // 4. Desktop companion (macOS app / remote device) connectivity + permissions.
112
+ if (desktopRegistry) {
113
+ try {
114
+ const desktopStatus = desktopRegistry.getStatus(userId);
115
+ const connected = Boolean(desktopStatus?.connected);
116
+ results.push({
117
+ id: 'desktop_connected',
118
+ label: 'Desktop companion',
119
+ passed: connected,
120
+ detail: connected
121
+ ? `${desktopStatus.onlineCount} device${desktopStatus.onlineCount !== 1 ? 's' : ''} connected`
122
+ : 'No device connected — open the desktop app',
123
+ });
124
+
125
+ if (connected && Array.isArray(desktopStatus?.devices)) {
126
+ const onlineDevice = desktopStatus.devices.find((d) => d.online && !d.revokedAt);
127
+ const perms = onlineDevice?.permissions || {};
128
+ const screenOk = Boolean(perms.screenCapture || perms.screen_capture);
129
+ const inputOk = Boolean(perms.accessibility || perms.inputControl || perms.input_control);
130
+ results.push({
131
+ id: 'desktop_screen',
132
+ label: 'Desktop — screen capture',
133
+ passed: screenOk,
134
+ detail: screenOk ? 'Granted' : 'Not granted — open System Settings › Privacy › Screen Recording',
135
+ });
136
+ results.push({
137
+ id: 'desktop_input',
138
+ label: 'Desktop — input control',
139
+ passed: inputOk,
140
+ detail: inputOk ? 'Granted' : 'Not granted — open System Settings › Privacy › Accessibility',
141
+ });
142
+ }
143
+ } catch (err) {
144
+ results.push({ id: 'desktop_connected', label: 'Desktop companion', passed: false, detail: String(err?.message || err).slice(0, 120) });
145
+ }
146
+ }
147
+
148
+ // 5. Chrome extension connectivity.
149
+ if (extensionRegistry) {
150
+ try {
151
+ const extStatus = extensionRegistry.getStatus(userId);
152
+ const extConnected = Boolean(extStatus?.connected);
153
+ results.push({
154
+ id: 'chrome_extension',
155
+ label: 'Chrome extension',
156
+ passed: extConnected,
157
+ detail: extConnected ? 'Connected' : 'Not connected — install the NeoAgent extension in Chrome',
158
+ });
159
+ } catch (err) {
160
+ results.push({ id: 'chrome_extension', label: 'Chrome extension', passed: false, detail: String(err?.message || err).slice(0, 120) });
161
+ }
162
+ }
163
+
164
+ const allPassed = results.every((r) => r.passed);
165
+ res.json({ passed: allPassed, results });
166
+ });
167
+
168
+ // Targeted runtime self-tests — one check per endpoint so the UI can embed
169
+ // results inline next to the relevant settings control.
170
+
171
+ app.get('/api/system/test/cli', requireAuth, async (req, res) => {
172
+ const userId = req.session?.userId;
173
+ const runtimeManager = req.app?.locals?.runtimeManager;
174
+ if (!runtimeManager || typeof runtimeManager.executeCommand !== 'function') {
175
+ return res.json({ passed: false, backendUsed: 'vm', detail: 'Runtime not configured on this server.' });
176
+ }
177
+ // Note: executeCommand always routes through the VM backend regardless of
178
+ // the cli_backend setting — desktop CLI routing is not yet implemented.
179
+ try {
180
+ const result = await runtimeManager.executeCommand(userId, 'echo "cli_test_ok"', { timeout: 15000 });
181
+ const exitOk = result?.exitCode === 0;
182
+ const outputOk = String(result?.stdout || '').includes('cli_test_ok');
183
+ return res.json({
184
+ passed: exitOk && outputOk,
185
+ backendUsed: 'vm',
186
+ detail: exitOk && outputOk
187
+ ? 'Command executed successfully'
188
+ : `Exit ${result?.exitCode ?? '?'}: ${String(result?.stderr || result?.stdout || '').slice(0, 120)}`,
189
+ });
190
+ } catch (err) {
191
+ return res.json({ passed: false, backendUsed: 'vm', detail: String(err?.message || err).slice(0, 120) });
192
+ }
193
+ });
194
+
195
+ app.get('/api/system/test/extension', requireAuth, (req, res) => {
196
+ const userId = req.session?.userId;
197
+ const extensionRegistry = req.app?.locals?.browserExtensionRegistry;
198
+ if (!extensionRegistry) {
199
+ return res.json({ passed: false, detail: 'Extension registry not available on this server.' });
200
+ }
201
+ try {
202
+ const status = extensionRegistry.getStatus(userId);
203
+ const connected = Boolean(status?.connected);
204
+ return res.json({
205
+ passed: connected,
206
+ detail: connected ? 'Extension is connected and live' : 'Extension is not connected',
207
+ tokenId: status?.activeTokenId || null,
208
+ meta: status?.connectedMeta || null,
209
+ });
210
+ } catch (err) {
211
+ return res.json({ passed: false, detail: String(err?.message || err).slice(0, 120) });
212
+ }
213
+ });
214
+
215
+ app.get('/api/system/test/desktop', requireAuth, (req, res) => {
216
+ const userId = req.session?.userId;
217
+ const desktopRegistry = req.app?.locals?.desktopCompanionRegistry;
218
+ if (!desktopRegistry) {
219
+ return res.json({ passed: false, detail: 'Desktop registry not available on this server.' });
220
+ }
221
+ try {
222
+ const status = desktopRegistry.getStatus(userId);
223
+ const connected = Boolean(status?.connected);
224
+ const devices = Array.isArray(status?.devices)
225
+ ? status.devices.filter((d) => d.online && !d.revokedAt)
226
+ : [];
227
+ const selected = status?.selectedDeviceId || null;
228
+ const activeDevice = selected
229
+ ? devices.find((d) => d.deviceId === selected)
230
+ : devices.length === 1 ? devices[0] : null;
231
+ const perms = activeDevice?.permissions || {};
232
+ const screenOk = Boolean(perms.screenCapture || perms.screen_capture);
233
+ const inputOk = Boolean(perms.accessibility || perms.inputControl || perms.input_control);
234
+ return res.json({
235
+ passed: connected,
236
+ connected,
237
+ onlineCount: devices.length,
238
+ selectedDeviceId: selected,
239
+ activeDevice: activeDevice ? {
240
+ deviceId: activeDevice.deviceId,
241
+ label: activeDevice.label || activeDevice.hostname || activeDevice.deviceId,
242
+ platform: activeDevice.platform || null,
243
+ paused: activeDevice.paused || false,
244
+ permissions: { screenCapture: screenOk, inputControl: inputOk },
245
+ } : null,
246
+ multipleOnline: devices.length > 1 && !activeDevice,
247
+ detail: !connected
248
+ ? 'No device connected'
249
+ : devices.length > 1 && !activeDevice
250
+ ? `${devices.length} devices online — select one in Desktop settings`
251
+ : activeDevice?.paused
252
+ ? `${activeDevice.label || 'Device'} is paused`
253
+ : `${activeDevice?.label || 'Device'} connected`,
254
+ });
255
+ } catch (err) {
256
+ return res.json({ passed: false, detail: String(err?.message || err).slice(0, 120) });
257
+ }
258
+ });
259
+
70
260
  app.get('/api/version', requireAuth, (req, res) => {
71
261
  res.json(getVersionInfo());
72
262
  });
@@ -1 +1 @@
1
- 18f90bb9a4d3bcea22e501f688e05b77
1
+ 9647cb1542fc26ae38d596b749b42002
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"4c525dac5ebe5971c5708ef73558ed8edcf4a3
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "1600220817" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "14317349" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });