neoagent 2.3.1-beta.71 → 2.3.1-beta.73

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.
@@ -134,6 +134,8 @@ Runtime profile and backend selection are stored in user settings, not normally
134
134
 
135
135
  Production policy can require the VM backend. In that case, set a strong `NEOAGENT_VM_GUEST_TOKEN` of at least 32 characters and avoid placeholder values.
136
136
 
137
+ The VM backend requires QEMU on the host machine. It runs an x86_64 Ubuntu guest so the Android emulator can run in the same isolated runtime as the browser. On macOS, install QEMU with `brew install qemu`. On Ubuntu or Debian, install `qemu-system` and `qemu-utils` from apt before starting NeoAgent.
138
+
137
139
  The app exposes two browser backend choices: VM and Chrome extension. VM uses the local isolated runtime. Chrome extension uses the paired extension connection on the remote machine instead of the server-local browser. To install only the extension on a remote machine, open NeoAgent, download `/api/browser-extension/download`, unzip it, load the folder through `chrome://extensions` with Developer mode enabled, then pair after logging in to NeoAgent. Unpacked Chrome extensions cannot replace themselves automatically; use the extension popup's update check to compare against the server bundle, then download and reload the latest ZIP when needed.
138
140
 
139
141
  ## Secrets Guidance
@@ -5,10 +5,27 @@ NeoAgent installs as a Node CLI and runs a self-hosted server with a bundled Flu
5
5
  ## Requirements
6
6
 
7
7
  - Node.js 20 or newer.
8
+ - QEMU for VM-backed browser and Android runs.
8
9
  - A reachable server URL if you want OAuth callbacks, mobile access, or messaging webhooks.
9
10
  - At least one hosted AI provider API key, unless you only use local Ollama.
10
11
  - Android Studio or a Flutter Android toolchain if you build the Android client yourself.
11
12
 
13
+ ### QEMU Installation
14
+
15
+ NeoAgent uses a per-user x86_64 VM for browser and Android execution. Install QEMU before starting the service:
16
+
17
+ ```bash
18
+ # macOS
19
+ brew install qemu
20
+
21
+ # Ubuntu / Debian
22
+ sudo apt-get update
23
+ sudo apt-get install -y qemu-system qemu-utils
24
+ ```
25
+
26
+ The first VM boot also downloads the Ubuntu base image and seeds the guest runtime automatically.
27
+ That guest bootstrap installs the browser and Android runtime dependencies it needs, including Java and the emulator support packages.
28
+
12
29
  ## Install
13
30
 
14
31
  ```bash
@@ -167,6 +167,7 @@ class AccountSettingsPanel extends StatefulWidget {
167
167
 
168
168
  class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
169
169
  late AccountSettingsTab _selectedTab;
170
+ late final TextEditingController _displayNameController;
170
171
  late final TextEditingController _emailController;
171
172
  late final TextEditingController _emailPasswordController;
172
173
  late final TextEditingController _setupPasswordController;
@@ -178,6 +179,8 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
178
179
  late final TextEditingController _confirmNewPasswordController;
179
180
  Map<String, dynamic>? _pendingSetup;
180
181
  List<String> _recoveryCodes = const <String>[];
182
+ String? _displayNameSuccessMessage;
183
+ String? _displayNameInlineError;
181
184
  String? _emailSuccessMessage;
182
185
  String? _emailInlineError;
183
186
  String? _passwordSuccessMessage;
@@ -187,6 +190,9 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
187
190
  void initState() {
188
191
  super.initState();
189
192
  _selectedTab = widget.initialTab ?? AccountSettingsTab.account;
193
+ _displayNameController = TextEditingController(
194
+ text: widget.controller.user?['display_name']?.toString() ?? '',
195
+ );
190
196
  _emailController = TextEditingController(
191
197
  text: widget.controller.user?['email']?.toString() ?? '',
192
198
  );
@@ -208,6 +214,11 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
208
214
  oldWidget.initialTab != widget.initialTab) {
209
215
  _selectedTab = widget.initialTab!;
210
216
  }
217
+ final displayName =
218
+ widget.controller.user?['display_name']?.toString() ?? '';
219
+ if (_displayNameController.text.isEmpty && displayName.isNotEmpty) {
220
+ _displayNameController.text = displayName;
221
+ }
211
222
  final email = widget.controller.user?['email']?.toString() ?? '';
212
223
  if (_emailController.text.isEmpty && email.isNotEmpty) {
213
224
  _emailController.text = email;
@@ -216,6 +227,7 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
216
227
 
217
228
  @override
218
229
  void dispose() {
230
+ _displayNameController.dispose();
219
231
  _emailController.dispose();
220
232
  _emailPasswordController.dispose();
221
233
  _setupPasswordController.dispose();
@@ -409,6 +421,56 @@ class _AccountSettingsPanelState extends State<AccountSettingsPanel> {
409
421
  const SizedBox(height: 12),
410
422
  _MetaPill(label: username, icon: Icons.person_outline),
411
423
  const SizedBox(height: 18),
424
+ TextField(
425
+ controller: _displayNameController,
426
+ decoration: const InputDecoration(
427
+ labelText: 'Display name',
428
+ helperText: 'Shown in the sidebar. Leave blank to use your username.',
429
+ ),
430
+ ),
431
+ if (_displayNameInlineError != null) ...<Widget>[
432
+ const SizedBox(height: 10),
433
+ _InlineError(message: _displayNameInlineError!),
434
+ ],
435
+ if (_displayNameSuccessMessage != null) ...<Widget>[
436
+ const SizedBox(height: 10),
437
+ _InlineSuccess(message: _displayNameSuccessMessage!),
438
+ ],
439
+ const SizedBox(height: 14),
440
+ FilledButton.icon(
441
+ onPressed: controller.isSavingAccountSettings
442
+ ? null
443
+ : () async {
444
+ setState(() {
445
+ _displayNameInlineError = null;
446
+ _displayNameSuccessMessage = null;
447
+ });
448
+ final trimmed = _displayNameController.text.trim();
449
+ if (trimmed.length > 64) {
450
+ setState(() {
451
+ _displayNameInlineError =
452
+ 'Display name must be 64 characters or fewer.';
453
+ });
454
+ return;
455
+ }
456
+ final saved = await controller.updateAccountDisplayName(
457
+ displayName: trimmed,
458
+ );
459
+ if (saved && mounted) {
460
+ setState(() {
461
+ _displayNameSuccessMessage = 'Display name saved.';
462
+ });
463
+ }
464
+ },
465
+ icon: controller.isSavingAccountSettings
466
+ ? const SizedBox.square(
467
+ dimension: 16,
468
+ child: CircularProgressIndicator(strokeWidth: 2),
469
+ )
470
+ : Icon(Icons.save_outlined),
471
+ label: Text('Save name'),
472
+ ),
473
+ const SizedBox(height: 22),
412
474
  Text('Current email: $currentEmail'),
413
475
  const SizedBox(height: 16),
414
476
  TextField(
@@ -4510,7 +4510,7 @@ class NeoAgentController extends ChangeNotifier {
4510
4510
  notifyListeners();
4511
4511
 
4512
4512
  final payload = <String, dynamic>{
4513
- 'headless_browser': headlessBrowser,
4513
+ 'headless_browser': true,
4514
4514
  'browser_backend': browserBackend,
4515
4515
  'smarter_model_selector': smarterSelector,
4516
4516
  'enabled_models': enabledModels,
@@ -4645,6 +4645,29 @@ class NeoAgentController extends ChangeNotifier {
4645
4645
  }
4646
4646
  }
4647
4647
 
4648
+ Future<bool> updateAccountDisplayName({
4649
+ required String displayName,
4650
+ }) async {
4651
+ isSavingAccountSettings = true;
4652
+ errorMessage = null;
4653
+ notifyListeners();
4654
+ try {
4655
+ _applyAccountResponse(
4656
+ await _backendClient.updateAccountDisplayName(
4657
+ baseUrl: backendUrl,
4658
+ displayName: displayName,
4659
+ ),
4660
+ );
4661
+ return true;
4662
+ } catch (error) {
4663
+ errorMessage = _friendlyErrorMessage(error);
4664
+ return false;
4665
+ } finally {
4666
+ isSavingAccountSettings = false;
4667
+ notifyListeners();
4668
+ }
4669
+ }
4670
+
4648
4671
  Future<void> linkAccountProvider(String provider) async {
4649
4672
  isSavingAccountSettings = true;
4650
4673
  errorMessage = null;
@@ -5957,9 +5980,7 @@ class NeoAgentController extends ChangeNotifier {
5957
5980
  details.length <= 800;
5958
5981
  }
5959
5982
 
5960
- bool get headlessBrowser =>
5961
- settings['headless_browser'] != false &&
5962
- settings['headless_browser'] != 'false';
5983
+ bool get headlessBrowser => true;
5963
5984
 
5964
5985
  String get browserBackend =>
5965
5986
  settings['browser_backend']?.toString().trim().toLowerCase() ?? 'vm';
@@ -6099,8 +6120,11 @@ class NeoAgentController extends ChangeNotifier {
6099
6120
 
6100
6121
  DateTime? get liveVoiceCaptureStartedAt => _liveVoiceCaptureStartedAt;
6101
6122
 
6102
- String get accountLabel =>
6103
- user?['username']?.toString() ?? username.ifEmpty('NeoAgent User');
6123
+ String get accountLabel {
6124
+ final displayName = user?['display_name']?.toString().trim() ?? '';
6125
+ if (displayName.isNotEmpty) return displayName;
6126
+ return user?['username']?.toString() ?? username.ifEmpty('NeoAgent User');
6127
+ }
6104
6128
 
6105
6129
  String get modelIndicator {
6106
6130
  if (defaultChatModel != 'auto') {
@@ -456,7 +456,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
456
456
  onPressed: controller.isSavingSettings
457
457
  ? null
458
458
  : () => controller.saveSettings(
459
- headlessBrowser: _headlessBrowser,
459
+ headlessBrowser: true,
460
460
  browserBackend: _browserBackend == 'extension'
461
461
  ? 'extension'
462
462
  : 'vm',
@@ -593,14 +593,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
593
593
  ),
594
594
  ),
595
595
  const SizedBox(height: 12),
596
- _SettingToggle(
597
- title: 'Run browser headless',
598
- subtitle:
599
- 'Keep browser automation off-screen when visible windows are not needed.',
600
- value: _headlessBrowser,
601
- onChanged: (value) => setState(() => _headlessBrowser = value),
602
- ),
603
- const SizedBox(height: 12),
596
+
604
597
  DropdownButtonFormField<String>(
605
598
  initialValue: _browserBackend,
606
599
  decoration: const InputDecoration(
@@ -10,7 +10,7 @@ class BackendClient {
10
10
  BackendClient({AppHttpClient? httpClient})
11
11
  : _httpClient = httpClient ?? createAppHttpClient();
12
12
 
13
- static const Duration _requestTimeout = Duration(seconds: 20);
13
+ static const Duration _requestTimeout = Duration(seconds: 60);
14
14
 
15
15
  final AppHttpClient _httpClient;
16
16
 
@@ -246,6 +246,15 @@ class BackendClient {
246
246
  });
247
247
  }
248
248
 
249
+ Future<Map<String, dynamic>> updateAccountDisplayName({
250
+ required String baseUrl,
251
+ required String displayName,
252
+ }) async {
253
+ return putMap(baseUrl, '/api/account/display-name', <String, dynamic>{
254
+ 'displayName': displayName,
255
+ });
256
+ }
257
+
249
258
  Future<Map<String, dynamic>> beginTwoFactorSetup({
250
259
  required String baseUrl,
251
260
  required String currentPassword,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.71",
3
+ "version": "2.3.1-beta.73",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
package/runtime/paths.js CHANGED
@@ -135,7 +135,7 @@ function generateSecret(bytes = 32) {
135
135
  return crypto.randomBytes(bytes).toString('hex');
136
136
  }
137
137
 
138
- function getDefaultVmBaseImageUrl(arch = process.arch) {
138
+ function getDefaultVmBaseImageUrl(arch = 'x64') {
139
139
  return arch === 'arm64' ? DEFAULT_VM_BASE_IMAGE_URLS.arm64 : DEFAULT_VM_BASE_IMAGE_URLS.x64;
140
140
  }
141
141
 
@@ -173,8 +173,9 @@ function ensureSecureRuntimeEnv({ envFile = ENV_FILE, env = process.env, logger
173
173
  }
174
174
 
175
175
  let vmBaseImageUrl = String(env.NEOAGENT_VM_BASE_IMAGE_URL || parsed.get('NEOAGENT_VM_BASE_IMAGE_URL') || '').trim();
176
- if (!vmBaseImageUrl) {
177
- vmBaseImageUrl = getDefaultVmBaseImageUrl();
176
+ const preferredVmBaseImageUrl = getDefaultVmBaseImageUrl();
177
+ if (!vmBaseImageUrl || /arm64|aarch64/i.test(vmBaseImageUrl)) {
178
+ vmBaseImageUrl = preferredVmBaseImageUrl;
178
179
  env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
179
180
  upsertEnvValue(envFile, 'NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
180
181
  changes.push('NEOAGENT_VM_BASE_IMAGE_URL');
@@ -1589,6 +1589,19 @@ function migrateUsersOnboarding() {
1589
1589
  }
1590
1590
  migrateUsersOnboarding();
1591
1591
 
1592
+ function migrateUsersDisplayName() {
1593
+ try {
1594
+ const columns = db.pragma('table_info(users)');
1595
+ const hasDisplayNameCol = columns.some((c) => c.name === 'display_name');
1596
+ if (!hasDisplayNameCol) {
1597
+ db.exec('ALTER TABLE users ADD COLUMN display_name TEXT');
1598
+ }
1599
+ } catch (err) {
1600
+ console.warn('Could not add display_name column:', err.message);
1601
+ }
1602
+ }
1603
+ migrateUsersDisplayName();
1604
+
1592
1605
  try {
1593
1606
  db.exec(`
1594
1607
  INSERT OR REPLACE INTO conversation_history_fts(rowid, content, role, user_id, agent_id, agent_run_id)
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "neoagent-guest-agent",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services (uses puppeteer-core with playwright-chromium browser binaries)",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.21.2",
11
+ "node-pty": "^1.0.0",
12
+ "playwright-chromium": "^1.59.1",
13
+ "proper-lockfile": "^4.1.2",
14
+ "puppeteer-core": "^24.40.0"
15
+ }
16
+ }
@@ -10,7 +10,19 @@ const { AndroidController } = require('./services/android/controller');
10
10
  const { RUNTIME_HOME } = require('../runtime/paths');
11
11
 
12
12
  const PORT = Number(process.env.NEOAGENT_GUEST_AGENT_PORT || 8421);
13
- const AUTH_TOKEN = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
13
+ function resolveGuestToken() {
14
+ const raw = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
15
+ if (raw) return raw;
16
+ const b64 = String(process.env.NEOAGENT_VM_GUEST_TOKEN_B64 || '').trim();
17
+ if (!b64) return '';
18
+ try {
19
+ return Buffer.from(b64, 'base64').toString('utf8').trim();
20
+ } catch {
21
+ return '';
22
+ }
23
+ }
24
+
25
+ const AUTH_TOKEN = resolveGuestToken();
14
26
  const FILE_ROOT = path.join(RUNTIME_HOME, 'guest-agent-files');
15
27
  const MAX_APK_STREAM_BYTES = Number(process.env.NEOAGENT_GUEST_MAX_APK_STREAM_BYTES || 512 * 1024 * 1024);
16
28
 
package/server/index.js CHANGED
@@ -210,8 +210,17 @@ function closeHttpServer(server, sockets, timeoutMs = 5000) {
210
210
  });
211
211
  }
212
212
 
213
+ function normalizeShutdownExitCode(value) {
214
+ const code = Number(value);
215
+ if (Number.isFinite(code)) {
216
+ return code;
217
+ }
218
+ return 1;
219
+ }
220
+
213
221
  async function shutdown(exitCode = 0) {
214
- shutdownExitCode = Math.max(shutdownExitCode, exitCode);
222
+ const normalizedExitCode = normalizeShutdownExitCode(exitCode);
223
+ shutdownExitCode = Math.max(shutdownExitCode, normalizedExitCode);
215
224
  if (shuttingDown) return;
216
225
  shuttingDown = true;
217
226
 
@@ -224,7 +233,7 @@ async function shutdown(exitCode = 0) {
224
233
  ]);
225
234
 
226
235
  db.close();
227
- process.exit(shutdownExitCode);
236
+ process.exit(normalizeShutdownExitCode(shutdownExitCode));
228
237
  }
229
238
 
230
239
  httpServer.listen(PORT, () => {
@@ -1 +1 @@
1
- e75c29bba45480b61dc03138f9b32797
1
+ af7040916c02919dd1df9c5d1b46d6a6
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "4246583462" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2814524395" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });