neoagent 2.4.1-beta.19 → 2.4.1-beta.21

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 (55) hide show
  1. package/README.md +4 -1
  2. package/docs/getting-started.md +9 -3
  3. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  4. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  5. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  6. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  7. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  8. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  9. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  10. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  11. package/flutter_app/lib/features/location/location_service.dart +3 -0
  12. package/flutter_app/lib/main.dart +1 -0
  13. package/flutter_app/lib/main_account_settings.dart +9 -33
  14. package/flutter_app/lib/main_app_shell.dart +237 -197
  15. package/flutter_app/lib/main_controller.dart +0 -25
  16. package/flutter_app/lib/main_devices.dart +2 -0
  17. package/flutter_app/lib/main_models.dart +144 -0
  18. package/flutter_app/lib/main_operations.dart +150 -19
  19. package/flutter_app/lib/main_shared.dart +642 -195
  20. package/flutter_app/lib/main_theme.dart +2 -0
  21. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +3 -1
  22. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  23. package/flutter_app/lib/src/theme/palette.dart +15 -15
  24. package/flutter_app/pubspec.yaml +3 -0
  25. package/flutter_app/web/favicon_light.svg +3 -0
  26. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  27. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  28. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  29. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  30. package/lib/manager.js +282 -81
  31. package/package.json +17 -3
  32. package/server/config/origins.js +3 -1
  33. package/server/db/database.js +73 -0
  34. package/server/public/.last_build_id +1 -1
  35. package/server/public/assets/AssetManifest.bin +1 -1
  36. package/server/public/assets/AssetManifest.bin.json +1 -1
  37. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  38. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  39. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  40. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  41. package/server/public/favicon_light.svg +3 -0
  42. package/server/public/flutter_bootstrap.js +1 -1
  43. package/server/public/icons/Icon-192-light.png +0 -0
  44. package/server/public/icons/Icon-512-light.png +0 -0
  45. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  46. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  47. package/server/public/main.dart.js +68769 -68268
  48. package/server/routes/agent_profiles.js +3 -0
  49. package/server/routes/memory.js +22 -1
  50. package/server/services/account/password_policy.js +6 -1
  51. package/server/services/memory/intelligence.js +181 -0
  52. package/server/services/memory/manager.js +475 -25
  53. package/server/utils/security.js +3 -0
  54. package/server/services/memory/openhuman_uplift.test.js +0 -98
  55. package/server/utils/version.test.js +0 -39
package/lib/manager.js CHANGED
@@ -62,29 +62,86 @@ const COLORS = process.stdout.isTTY
62
62
  green: '\x1b[1;32m',
63
63
  yellow: '\x1b[1;33m',
64
64
  blue: '\x1b[1;34m',
65
+ magenta: '\x1b[1;35m',
65
66
  cyan: '\x1b[1;36m',
66
67
  dim: '\x1b[2m'
67
68
  }
68
- : { reset: '', bold: '', red: '', green: '', yellow: '', blue: '', cyan: '', dim: '' };
69
+ : { reset: '', bold: '', red: '', green: '', yellow: '', blue: '', magenta: '', cyan: '', dim: '' };
70
+
71
+ const CLI_INTERACTIVE = process.stdout.isTTY;
72
+ const installActionItems = [];
69
73
 
70
74
  function logInfo(msg) {
71
- console.log(` ${COLORS.blue}->${COLORS.reset} ${msg}`);
75
+ const mark = CLI_INTERACTIVE ? `${COLORS.blue}◇${COLORS.reset}` : '->';
76
+ console.log(` ${mark} ${msg}`);
72
77
  }
73
78
 
74
79
  function logOk(msg) {
75
- console.log(` ${COLORS.green}ok${COLORS.reset} ${msg}`);
80
+ const mark = CLI_INTERACTIVE ? `${COLORS.green}◆${COLORS.reset}` : 'ok';
81
+ console.log(` ${mark} ${msg}`);
76
82
  }
77
83
 
78
84
  function logWarn(msg) {
79
- console.warn(` ${COLORS.yellow}warn${COLORS.reset} ${msg}`);
85
+ const mark = CLI_INTERACTIVE ? `${COLORS.yellow}▲${COLORS.reset}` : 'warn';
86
+ console.warn(` ${mark} ${msg}`);
80
87
  }
81
88
 
82
89
  function logErr(msg) {
83
- console.error(` ${COLORS.red}err${COLORS.reset} ${msg}`);
90
+ const mark = CLI_INTERACTIVE ? `${COLORS.red}✕${COLORS.reset}` : 'err';
91
+ console.error(` ${mark} ${msg}`);
84
92
  }
85
93
 
86
94
  function heading(text) {
87
- console.log(`\n${COLORS.bold}${text}${COLORS.reset}`);
95
+ if (!CLI_INTERACTIVE) {
96
+ console.log(`\n${text}`);
97
+ return;
98
+ }
99
+ console.log(`\n${COLORS.bold}${COLORS.cyan}${text}${COLORS.reset}`);
100
+ }
101
+
102
+ function cliBanner(title = APP_NAME, subtitle = 'local agent control') {
103
+ if (!CLI_INTERACTIVE) return;
104
+ const c = COLORS;
105
+ const width = 38;
106
+ const stripAnsi = (text) => String(text).replace(/\x1b\[[0-9;]*m/g, '');
107
+ const boxLine = (content) => {
108
+ const padding = Math.max(0, width - stripAnsi(content).length);
109
+ console.log(` ${c.cyan}│${c.reset} ${content}${' '.repeat(padding)} ${c.cyan}│${c.reset}`);
110
+ };
111
+ console.log('');
112
+ console.log(` ${c.cyan}╭────────────────────────────────────────╮${c.reset}`);
113
+ boxLine(`${c.bold}${c.magenta}NeoAgent${c.reset} ${c.dim}arcade ops console${c.reset}`);
114
+ boxLine(`${c.bold}${title}${c.reset} ${c.dim}${subtitle}${c.reset}`);
115
+ console.log(` ${c.cyan}╰────────────────────────────────────────╯${c.reset}`);
116
+ }
117
+
118
+ function cliSection(text) {
119
+ if (CLI_INTERACTIVE) {
120
+ console.log(`${COLORS.dim} ──${COLORS.reset} ${COLORS.bold}${text}${COLORS.reset}`);
121
+ } else {
122
+ console.log(text);
123
+ }
124
+ }
125
+
126
+ function statusLine(ok, label, value, hint = '') {
127
+ const mark = ok ? (CLI_INTERACTIVE ? `${COLORS.green}●${COLORS.reset}` : 'ok') : (CLI_INTERACTIVE ? `${COLORS.yellow}●${COLORS.reset}` : 'warn');
128
+ const padded = String(label).padEnd(9);
129
+ const suffix = hint ? ` ${COLORS.dim}${hint}${COLORS.reset}` : '';
130
+ console.log(` ${mark} ${padded} ${value}${suffix}`);
131
+ }
132
+
133
+ function rememberInstallAction(message) {
134
+ if (!installActionItems.includes(message)) {
135
+ installActionItems.push(message);
136
+ }
137
+ }
138
+
139
+ function printInstallActionItems() {
140
+ if (installActionItems.length === 0) return;
141
+ heading('Post-install actions');
142
+ for (const item of installActionItems) {
143
+ logWarn(item);
144
+ }
88
145
  }
89
146
 
90
147
  function detectPlatform() {
@@ -477,6 +534,43 @@ async function askSecret(question, currentValue = '') {
477
534
  });
478
535
  }
479
536
 
537
+ function defaultEnvLines(current = {}) {
538
+ const defaultVmBaseImageUrl = getDefaultVmBaseImageUrl();
539
+ const port = current.PORT || '3333';
540
+ const publicUrl = current.PUBLIC_URL || '';
541
+ const secureCookies = current.SECURE_COOKIES ||
542
+ (String(publicUrl || '').trim().startsWith('https://') ? 'true' : 'false');
543
+ const trustProxy = current.TRUST_PROXY || secureCookies;
544
+ return [
545
+ 'NODE_ENV=production',
546
+ `PORT=${port}`,
547
+ publicUrl ? `PUBLIC_URL=${publicUrl}` : '',
548
+ `SECURE_COOKIES=${String(secureCookies || '').trim().toLowerCase() === 'true' ? 'true' : 'false'}`,
549
+ `TRUST_PROXY=${String(trustProxy || '').trim().toLowerCase() === 'true' ? 'true' : 'false'}`,
550
+ `SESSION_SECRET=${current.SESSION_SECRET || randomSecret()}`,
551
+ `NEOAGENT_PROFILE=${current.NEOAGENT_PROFILE || 'prod'}`,
552
+ `NEOAGENT_DEPLOYMENT_MODE=${parseDeploymentMode(current.NEOAGENT_DEPLOYMENT_MODE || 'self_hosted')}`,
553
+ `NEOAGENT_RELEASE_CHANNEL=${parseReleaseChannel(current.NEOAGENT_RELEASE_CHANNEL || 'stable') || 'stable'}`,
554
+ `NEOAGENT_VM_BASE_IMAGE_URL=${current.NEOAGENT_VM_BASE_IMAGE_URL || defaultVmBaseImageUrl}`,
555
+ `NEOAGENT_VM_MEMORY_MB=${current.NEOAGENT_VM_MEMORY_MB || '4096'}`,
556
+ `NEOAGENT_VM_CPUS=${current.NEOAGENT_VM_CPUS || '2'}`,
557
+ `NEOAGENT_VM_GUEST_TOKEN=${current.NEOAGENT_VM_GUEST_TOKEN || randomSecret()}`,
558
+ current.XAI_BASE_URL ? `XAI_BASE_URL=${current.XAI_BASE_URL}` : 'XAI_BASE_URL=https://api.x.ai/v1',
559
+ current.OLLAMA_URL ? `OLLAMA_URL=${current.OLLAMA_URL}` : 'OLLAMA_URL=http://localhost:11434',
560
+ current.DEEPGRAM_BASE_URL ? `DEEPGRAM_BASE_URL=${current.DEEPGRAM_BASE_URL}` : 'DEEPGRAM_BASE_URL=https://api.deepgram.com',
561
+ current.DEEPGRAM_MODEL ? `DEEPGRAM_MODEL=${current.DEEPGRAM_MODEL}` : 'DEEPGRAM_MODEL=nova-3',
562
+ current.DEEPGRAM_LANGUAGE ? `DEEPGRAM_LANGUAGE=${current.DEEPGRAM_LANGUAGE}` : 'DEEPGRAM_LANGUAGE=multi',
563
+ ].filter(Boolean);
564
+ }
565
+
566
+ function writeDefaultEnvFile() {
567
+ ensureRuntimeDirs();
568
+ const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
569
+ fs.writeFileSync(ENV_FILE, `${defaultEnvLines(current).join('\n')}\n`, { mode: 0o600 });
570
+ logOk(`Wrote default config to ${ENV_FILE}`);
571
+ rememberInstallAction('Add provider keys with `neoagent setup`, `neoagent env set KEY VALUE`, or the login commands when you are ready.');
572
+ }
573
+
480
574
  async function cmdSetup() {
481
575
  heading('Environment Setup');
482
576
  ensureRuntimeDirs();
@@ -1193,6 +1287,50 @@ function installDependencies() {
1193
1287
  logOk('Dependencies installed');
1194
1288
  }
1195
1289
 
1290
+ function assertSupportedNodeRuntime() {
1291
+ const major = Number(String(process.versions.node || '').split('.')[0]);
1292
+ if (!Number.isInteger(major) || major < 20) {
1293
+ throw new Error(`NeoAgent requires Node.js 20 or newer. Current runtime is ${process.versions.node || 'unknown'}.`);
1294
+ }
1295
+ logOk(`Node.js ${process.versions.node}`);
1296
+ }
1297
+
1298
+ function installPreflight() {
1299
+ heading('Installer preflight');
1300
+ ensureRuntimeDirs();
1301
+ assertSupportedNodeRuntime();
1302
+
1303
+ if (!commandExists('npm')) {
1304
+ throw new Error('npm was not found in PATH. Install Node.js 20+ with npm, then run `neoagent install` again.');
1305
+ }
1306
+ const npmVersion = runQuiet('npm', ['--version']);
1307
+ logOk(`npm ${npmVersion.status === 0 ? npmVersion.stdout.trim() : '(version unknown)'}`);
1308
+
1309
+ const platform = detectPlatform();
1310
+ if (platform === 'other') {
1311
+ rememberInstallAction('Automatic service installation is available on macOS and Linux. This machine will use a detached process fallback.');
1312
+ } else {
1313
+ logOk(`platform ${platform}`);
1314
+ }
1315
+
1316
+ if (platform === 'macos' && !commandExists('launchctl')) {
1317
+ rememberInstallAction('launchctl was not found, so the installer will use a detached process fallback instead of a login service.');
1318
+ }
1319
+ if (platform === 'linux' && !commandExists('systemctl')) {
1320
+ rememberInstallAction('systemctl was not found, so the installer will use a detached process fallback instead of a user service.');
1321
+ }
1322
+
1323
+ const port = loadEnvPort();
1324
+ const portOwner = commandExists('lsof')
1325
+ ? runQuiet('lsof', ['-nP', '-iTCP:' + port, '-sTCP:LISTEN'])
1326
+ : null;
1327
+ if (portOwner && portOwner.status === 0 && portOwner.stdout.trim()) {
1328
+ logInfo(`Port ${port} already has a listener; install will keep existing processes unless service start replaces them.`);
1329
+ } else {
1330
+ logOk(`port ${port} available`);
1331
+ }
1332
+ }
1333
+
1196
1334
  function buildBundledWebClientIfPossible({ required = false } = {}) {
1197
1335
  heading('Web Client');
1198
1336
  return buildWebClient({
@@ -1281,7 +1419,7 @@ function startFallback() {
1281
1419
  logOk(`Started detached process (pid ${child.pid})`);
1282
1420
  }
1283
1421
 
1284
- async function ensureQemuInstalled() {
1422
+ async function ensureQemuInstalled({ required = false } = {}) {
1285
1423
  heading('Ensure QEMU Installed');
1286
1424
  const platform = detectPlatform();
1287
1425
 
@@ -1295,35 +1433,54 @@ async function ensureQemuInstalled() {
1295
1433
 
1296
1434
  logInfo('QEMU components not found. Attempting to install...');
1297
1435
 
1298
- if (platform === 'macos') {
1299
- if (!commandExists('brew')) {
1300
- throw new Error('Homebrew is required to install QEMU on macOS. Please install it first: https://brew.sh/');
1301
- }
1302
- logInfo('Running "brew install qemu"...');
1303
- runOrThrow('brew', ['install', 'qemu']);
1304
- } else if (platform === 'linux') {
1305
- const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1306
- if (commandExists('apt-get')) {
1307
- const pkgs = ['qemu-utils'];
1308
- if (isArm) {
1309
- pkgs.push('qemu-system-arm');
1436
+ try {
1437
+ if (platform === 'macos') {
1438
+ if (!commandExists('brew')) {
1439
+ const message = 'Homebrew is required for automatic QEMU install on macOS. Install Homebrew from https://brew.sh/ and run `neoagent install` again.';
1440
+ if (required) throw new Error(message);
1441
+ logWarn('Homebrew not found; VM features will stay unavailable until QEMU is installed.');
1442
+ rememberInstallAction(message);
1443
+ return false;
1444
+ }
1445
+ logInfo('Running "brew install qemu"...');
1446
+ runOrThrow('brew', ['install', 'qemu']);
1447
+ } else if (platform === 'linux') {
1448
+ const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1449
+ if (commandExists('apt-get')) {
1450
+ const pkgs = ['qemu-utils'];
1451
+ if (isArm) {
1452
+ pkgs.push('qemu-system-arm');
1453
+ } else {
1454
+ pkgs.push('qemu-system-x86');
1455
+ }
1456
+ logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1457
+ runOrThrow('sudo', ['apt-get', 'update']);
1458
+ runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1459
+ } else if (commandExists('dnf')) {
1460
+ logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1461
+ runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1462
+ } else if (commandExists('yum')) {
1463
+ logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1464
+ runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1310
1465
  } else {
1311
- pkgs.push('qemu-system-x86');
1466
+ const message = 'Unsupported Linux package manager for automatic QEMU install. Install qemu-system and qemu-utils, then run `neoagent install` again.';
1467
+ if (required) throw new Error(message);
1468
+ logWarn('Could not find apt-get, dnf, or yum for QEMU installation.');
1469
+ rememberInstallAction(message);
1470
+ return false;
1312
1471
  }
1313
- logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1314
- runOrThrow('sudo', ['apt-get', 'update']);
1315
- runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1316
- } else if (commandExists('dnf')) {
1317
- logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1318
- runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1319
- } else if (commandExists('yum')) {
1320
- logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1321
- runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1322
1472
  } else {
1323
- throw new Error('Unsupported Linux distribution. Please install qemu-system and qemu-utils manually.');
1473
+ const message = 'Unsupported platform for automatic QEMU installation. Install QEMU manually if you need VM-isolated runtime features.';
1474
+ if (required) throw new Error(message);
1475
+ logWarn('Skipping QEMU auto-install on this platform.');
1476
+ rememberInstallAction(message);
1477
+ return false;
1324
1478
  }
1325
- } else {
1326
- throw new Error('Unsupported platform for automatic QEMU installation. Please install QEMU manually.');
1479
+ } catch (err) {
1480
+ if (required) throw err;
1481
+ logWarn(`QEMU install did not complete: ${err.message}`);
1482
+ rememberInstallAction('Install QEMU manually or rerun `neoagent install` after your package manager is ready.');
1483
+ return false;
1327
1484
  }
1328
1485
 
1329
1486
  const verifiedSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
@@ -1331,8 +1488,13 @@ async function ensureQemuInstalled() {
1331
1488
 
1332
1489
  if (verifiedSystem && verifiedImg) {
1333
1490
  logOk('QEMU installed successfully');
1491
+ return true;
1334
1492
  } else {
1335
- throw new Error('QEMU installation failed or components not found in PATH after install.');
1493
+ const message = 'QEMU installation finished but required binaries were not found in PATH.';
1494
+ if (required) throw new Error(message);
1495
+ logWarn(message);
1496
+ rememberInstallAction('Ensure qemu-system and qemu-img are available in PATH, then run `neoagent install` again.');
1497
+ return false;
1336
1498
  }
1337
1499
  }
1338
1500
 
@@ -1341,7 +1503,7 @@ function ensureYtDlpInstalled() {
1341
1503
  if (commandExists('yt-dlp')) {
1342
1504
  const ver = runQuiet('yt-dlp', ['--version']);
1343
1505
  logOk(`yt-dlp ${ver.status === 0 ? ver.stdout.trim() : '(version unknown)'}`);
1344
- return;
1506
+ return true;
1345
1507
  }
1346
1508
 
1347
1509
  logInfo('yt-dlp not found. Attempting to install...');
@@ -1349,16 +1511,19 @@ function ensureYtDlpInstalled() {
1349
1511
 
1350
1512
  if (platform === 'macos') {
1351
1513
  if (!commandExists('brew')) {
1352
- logWarn('Homebrew not found skipping yt-dlp install. Install manually: brew install yt-dlp');
1353
- return;
1514
+ logWarn('Homebrew not found; skipping yt-dlp auto-install.');
1515
+ rememberInstallAction('Install yt-dlp with `brew install yt-dlp` if you need video/audio extraction.');
1516
+ return false;
1354
1517
  }
1355
1518
  try {
1356
1519
  runOrThrow('brew', ['install', 'yt-dlp']);
1357
1520
  logOk('yt-dlp installed via Homebrew');
1521
+ return true;
1358
1522
  } catch {
1359
- logWarn('yt-dlp install failed. Install manually: brew install yt-dlp');
1523
+ logWarn('yt-dlp install failed.');
1524
+ rememberInstallAction('Install yt-dlp with `brew install yt-dlp` if you need video/audio extraction.');
1525
+ return false;
1360
1526
  }
1361
- return;
1362
1527
  }
1363
1528
 
1364
1529
  if (platform === 'linux') {
@@ -1366,7 +1531,7 @@ function ensureYtDlpInstalled() {
1366
1531
  try {
1367
1532
  runOrThrow('pipx', ['install', 'yt-dlp']);
1368
1533
  logOk('yt-dlp installed via pipx');
1369
- return;
1534
+ return true;
1370
1535
  } catch {
1371
1536
  // fall through to pip3
1372
1537
  }
@@ -1375,24 +1540,39 @@ function ensureYtDlpInstalled() {
1375
1540
  try {
1376
1541
  runOrThrow('pip3', ['install', '--user', 'yt-dlp']);
1377
1542
  logOk('yt-dlp installed via pip3');
1378
- return;
1543
+ return true;
1379
1544
  } catch {
1380
1545
  // fall through to warn
1381
1546
  }
1382
1547
  }
1383
- logWarn('Could not install yt-dlp automatically. Install manually: pipx install yt-dlp');
1548
+ logWarn('Could not install yt-dlp automatically.');
1549
+ rememberInstallAction('Install yt-dlp with `pipx install yt-dlp` or your OS package manager if you need video/audio extraction.');
1550
+ return false;
1384
1551
  }
1552
+ rememberInstallAction('Install yt-dlp manually if you need video/audio extraction.');
1553
+ return false;
1385
1554
  }
1386
1555
 
1387
1556
  async function cmdInstall() {
1557
+ installActionItems.length = 0;
1558
+ cliBanner(`Install ${APP_NAME}`, 'guided bootstrap');
1388
1559
  heading(`Install ${APP_NAME}`);
1560
+ installPreflight();
1561
+
1389
1562
  if (!fs.existsSync(ENV_FILE)) {
1390
- logWarn('.env not found; starting setup');
1391
- await cmdSetup();
1563
+ if (process.stdin.isTTY) {
1564
+ logWarn('.env not found; starting guided setup');
1565
+ await cmdSetup();
1566
+ } else {
1567
+ logWarn('.env not found and stdin is not interactive; writing a secure default config');
1568
+ writeDefaultEnvFile();
1569
+ }
1570
+ } else {
1571
+ logOk(`Using config ${ENV_FILE}`);
1392
1572
  }
1393
1573
 
1394
1574
  installDependencies();
1395
- await ensureQemuInstalled();
1575
+ await ensureQemuInstalled({ required: false });
1396
1576
  ensureYtDlpInstalled();
1397
1577
  buildBundledWebClientIfPossible({ required: true });
1398
1578
 
@@ -1407,19 +1587,25 @@ async function cmdInstall() {
1407
1587
 
1408
1588
  const port = loadEnvPort();
1409
1589
  logOk(`Running on http://localhost:${port}`);
1590
+ printInstallActionItems();
1591
+ heading('Ready');
1592
+ logInfo(`Open http://localhost:${port} or run \`neoagent status\` for a health check.`);
1410
1593
  }
1411
1594
 
1412
1595
  function cmdStart() {
1596
+ cliBanner(`Start ${APP_NAME}`, 'boot sequence');
1413
1597
  heading(`Start ${APP_NAME}`);
1414
1598
  const platform = detectPlatform();
1415
1599
 
1416
1600
  if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1601
+ logInfo('Handing launch to launchd');
1417
1602
  installMacService();
1418
1603
  logOk('launchd start requested');
1419
1604
  return;
1420
1605
  }
1421
1606
 
1422
1607
  if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1608
+ logInfo('Handing launch to systemd');
1423
1609
  runOrThrow('systemctl', ['--user', 'start', 'neoagent']);
1424
1610
  runOrThrow('systemctl', ['--user', 'is-active', '--quiet', 'neoagent']);
1425
1611
  logOk('systemd start requested');
@@ -1512,54 +1698,68 @@ function cmdUninstall() {
1512
1698
  }
1513
1699
 
1514
1700
  async function cmdStatus() {
1701
+ cliBanner(`${APP_NAME} Status`, 'systems sweep');
1515
1702
  heading(`${APP_NAME} Status`);
1516
1703
  const port = loadEnvPort();
1517
1704
  const running = await isPortOpen(port);
1518
1705
  const releaseChannel = currentReleaseChannel();
1519
1706
  const platform = detectPlatform();
1520
1707
 
1521
- if (running) {
1522
- logOk(`server http://localhost:${port}`);
1523
- } else {
1524
- logWarn(`server not reachable on port ${port}`);
1525
- }
1708
+ cliSection('Runtime');
1709
+ statusLine(
1710
+ running,
1711
+ 'server',
1712
+ running ? `http://localhost:${port}` : `not reachable on port ${port}`,
1713
+ );
1526
1714
 
1527
1715
  if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1528
1716
  const svcRes = runQuiet('launchctl', ['list', SERVICE_LABEL]);
1529
- if (svcRes.status === 0 && svcRes.stdout.trim()) {
1530
- logOk(`service launchd (${SERVICE_LABEL})`);
1531
- } else {
1532
- logWarn(`service launchd unit not loaded — run: neoagent install`);
1533
- }
1717
+ statusLine(
1718
+ svcRes.status === 0 && Boolean(svcRes.stdout.trim()),
1719
+ 'service',
1720
+ svcRes.status === 0 && svcRes.stdout.trim()
1721
+ ? `launchd (${SERVICE_LABEL})`
1722
+ : 'launchd unit not loaded',
1723
+ svcRes.status === 0 && svcRes.stdout.trim() ? '' : 'run: neoagent install',
1724
+ );
1534
1725
  } else if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1535
1726
  const svcRes = runQuiet('systemctl', ['--user', 'is-active', 'neoagent']);
1536
- if (svcRes.status === 0 && svcRes.stdout.trim() === 'active') {
1537
- logOk('service systemd (neoagent)');
1538
- } else {
1539
- logWarn('service systemd unit not active run: neoagent install');
1540
- }
1727
+ statusLine(
1728
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active',
1729
+ 'service',
1730
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active'
1731
+ ? 'systemd (neoagent)'
1732
+ : 'systemd unit not active',
1733
+ svcRes.status === 0 && svcRes.stdout.trim() === 'active' ? '' : 'run: neoagent install',
1734
+ );
1541
1735
  }
1542
1736
 
1543
- if (fs.existsSync(ENV_FILE)) {
1544
- logOk(`config ${ENV_FILE}`);
1545
- } else {
1546
- logWarn(`config .env not found — run: neoagent setup`);
1547
- }
1737
+ cliSection('Assets');
1738
+ statusLine(
1739
+ fs.existsSync(ENV_FILE),
1740
+ 'config',
1741
+ fs.existsSync(ENV_FILE) ? ENV_FILE : '.env not found',
1742
+ fs.existsSync(ENV_FILE) ? '' : 'run: neoagent setup',
1743
+ );
1548
1744
 
1549
- if (hasBundledWebClient(WEB_CLIENT_DIR)) {
1550
- logOk('web bundled Flutter client present');
1551
- } else {
1552
- logWarn('web no bundled client — run: neoagent rebuild-web');
1553
- }
1745
+ statusLine(
1746
+ hasBundledWebClient(WEB_CLIENT_DIR),
1747
+ 'web',
1748
+ hasBundledWebClient(WEB_CLIENT_DIR)
1749
+ ? 'bundled Flutter client present'
1750
+ : 'no bundled client',
1751
+ hasBundledWebClient(WEB_CLIENT_DIR) ? '' : 'run: neoagent rebuild-web',
1752
+ );
1554
1753
 
1555
1754
  console.log('');
1556
- console.log(` install ${APP_DIR}`);
1557
- console.log(` version ${currentInstalledVersionLabel()}`);
1558
- console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1755
+ cliSection('Build');
1756
+ console.log(` install ${APP_DIR}`);
1757
+ console.log(` version ${currentInstalledVersionLabel()}`);
1758
+ console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1559
1759
 
1560
1760
  const processes = listNeoAgentServerProcesses();
1561
1761
  if (processes.length > 0) {
1562
- console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1762
+ console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1563
1763
  if (processes.length > 1) {
1564
1764
  logWarn(`multiple NeoAgent processes detected (${processes.length})`);
1565
1765
  }
@@ -1729,14 +1929,15 @@ function printHelp() {
1729
1929
 
1730
1930
  function row(cmd, desc) {
1731
1931
  const padded = ` neoagent ${cmd}`.padEnd(W);
1732
- console.log(`${padded}${c.dim}${desc}${c.reset}`);
1932
+ const arrow = CLI_INTERACTIVE ? `${c.cyan}›${c.reset} ` : '';
1933
+ console.log(`${padded}${arrow}${c.dim}${desc}${c.reset}`);
1733
1934
  }
1734
1935
 
1735
- console.log(`\n${c.bold}neoagent${c.reset} — manage your NeoAgent server\n`);
1736
- console.log(`${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1936
+ cliBanner('neoagent', 'command deck');
1937
+ console.log(`\n${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1737
1938
 
1738
- console.log(`${c.bold}Lifecycle${c.reset}`);
1739
- row('install', 'First-time install and service setup');
1939
+ cliSection('Lifecycle');
1940
+ row('install', 'Guided bootstrap, dependencies, config, service');
1740
1941
  row('start', 'Start the server');
1741
1942
  row('stop', 'Stop the server');
1742
1943
  row('restart', 'Stop, then start');
@@ -1745,7 +1946,7 @@ function printHelp() {
1745
1946
  row('uninstall', 'Remove the system service');
1746
1947
  console.log('');
1747
1948
 
1748
- console.log(`${c.bold}Configuration${c.reset}`);
1949
+ cliSection('Configuration');
1749
1950
  row('setup', 'Interactive configuration wizard');
1750
1951
  row('env list', 'List all variables (secrets masked)');
1751
1952
  row('env get KEY', 'Print a single variable');
@@ -1755,7 +1956,7 @@ function printHelp() {
1755
1956
  row('channel stable|beta', 'Switch release channel');
1756
1957
  console.log('');
1757
1958
 
1758
- console.log(`${c.bold}Updates & Auth${c.reset}`);
1959
+ cliSection('Updates & Auth');
1759
1960
  row('update', 'Update to latest on current channel');
1760
1961
  row('update stable|beta', 'Update and switch channel');
1761
1962
  row('login github-copilot','Authenticate GitHub Copilot');
@@ -1764,7 +1965,7 @@ function printHelp() {
1764
1965
  row('login grok-oauth', 'Authenticate Grok (xAI OAuth)');
1765
1966
  console.log('');
1766
1967
 
1767
- console.log(`${c.bold}Maintenance${c.reset}`);
1968
+ cliSection('Maintenance');
1768
1969
  row('migrate', 'Migrate from another agent installation');
1769
1970
  row('migrate dry-run', 'Preview what would be migrated');
1770
1971
  row('rebuild-web', 'Rebuild the bundled Flutter web client');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.19",
3
+ "version": "2.4.1-beta.21",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -38,7 +38,18 @@
38
38
  "flutter:run:web": "cd flutter_app && flutter run -d chrome --dart-define=NEOAGENT_WEB_BUILD_ID=$(node ../scripts/web_build_id.js)",
39
39
  "flutter:build:web": "cd flutter_app && flutter build web --output ../server/public --dart-define=NEOAGENT_BACKEND_URL=${NEOAGENT_BACKEND_URL:-} --dart-define=NEOAGENT_WEB_BUILD_ID=$(node ../scripts/web_build_id.js)",
40
40
  "manage": "node bin/neoagent.js",
41
- "test": "node --test",
41
+ "test": "npm run test:backend",
42
+ "test:unit": "node --test --test-reporter=spec test/backend/unit/*.test.js",
43
+ "test:integration": "node --test --test-reporter=spec test/integration/*.test.js",
44
+ "test:security": "node --test --test-reporter=spec test/security/*.test.js",
45
+ "test:contract": "node --test --test-reporter=spec test/contract/*.test.js",
46
+ "test:e2e": "node --test --test-reporter=spec test/e2e/*.test.js",
47
+ "test:ws": "node --test --test-reporter=spec test/websocket/*.test.js",
48
+ "test:backend": "npm run test:unit && npm run test:integration && npm run test:security && npm run test:contract && npm run test:e2e && npm run test:ws",
49
+ "test:load": "node test/load/auth_load.js",
50
+ "flutter:test": "cd flutter_app && flutter test ../test/flutter/unit ../test/flutter/widget",
51
+ "flutter:test:unit": "cd flutter_app && flutter test ../test/flutter/unit",
52
+ "flutter:test:widget": "cd flutter_app && flutter test ../test/flutter/widget",
42
53
  "benchmark:tokens": "node scripts/benchmark-token-cost.js",
43
54
  "release": "npx semantic-release"
44
55
  },
@@ -104,7 +115,10 @@
104
115
  "devDependencies": {
105
116
  "@docusaurus/core": "3.10.0",
106
117
  "@docusaurus/preset-classic": "3.10.0",
118
+ "autocannon": "^7.15.0",
107
119
  "react": "18.3.1",
108
- "react-dom": "18.3.1"
120
+ "react-dom": "18.3.1",
121
+ "socket.io-client": "^4.8.3",
122
+ "supertest": "^7.2.2"
109
123
  }
110
124
  }
@@ -35,7 +35,9 @@ function isAllowedOrigin(origin, options = {}) {
35
35
 
36
36
  function validateOrigin(origin, callback, options = {}) {
37
37
  if (isAllowedOrigin(origin, options)) return callback(null, true);
38
- return callback(new Error(`Origin not allowed: ${origin || 'unknown'}`));
38
+ const error = new Error(`Origin not allowed: ${origin || 'unknown'}`);
39
+ error.statusCode = 403;
40
+ return callback(error);
39
41
  }
40
42
 
41
43
  module.exports = {