neoagent 3.0.1-beta.2 → 3.0.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 (94) hide show
  1. package/.env.example +19 -0
  2. package/README.md +20 -3
  3. package/docs/benchmarking.md +102 -0
  4. package/docs/billing.md +224 -0
  5. package/docs/configuration.md +22 -0
  6. package/docs/getting-started.md +10 -8
  7. package/docs/index.md +1 -0
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +4 -1
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +316 -0
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1594 -224
  14. package/flutter_app/lib/main_controller.dart +263 -26
  15. package/flutter_app/lib/main_devices.dart +1 -1
  16. package/flutter_app/lib/main_install.dart +1147 -0
  17. package/flutter_app/lib/main_navigation.dart +12 -0
  18. package/flutter_app/lib/main_operations.dart +134 -9
  19. package/flutter_app/lib/main_settings.dart +148 -52
  20. package/flutter_app/lib/main_shared.dart +1 -0
  21. package/flutter_app/lib/src/backend_client.dart +86 -1
  22. package/landing/index.html +2 -2
  23. package/lib/manager.js +184 -66
  24. package/lib/schema_migrations.js +84 -0
  25. package/package.json +10 -4
  26. package/server/admin/access.js +12 -7
  27. package/server/admin/admin.css +78 -0
  28. package/server/admin/admin.js +511 -23
  29. package/server/admin/billing.js +415 -0
  30. package/server/admin/index.html +120 -13
  31. package/server/admin/users.js +15 -15
  32. package/server/db/database.js +13 -21
  33. package/server/db/sessions_db.js +8 -0
  34. package/server/http/middleware.js +4 -4
  35. package/server/http/routes.js +9 -0
  36. package/server/http/static.js +4 -2
  37. package/server/middleware/requireBilling.js +12 -0
  38. package/server/public/.last_build_id +1 -1
  39. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  40. package/server/public/flutter_bootstrap.js +1 -1
  41. package/server/public/main.dart.js +89347 -85449
  42. package/server/routes/account.js +53 -0
  43. package/server/routes/admin.js +515 -63
  44. package/server/routes/agents.js +203 -21
  45. package/server/routes/billing.js +127 -0
  46. package/server/routes/billing_webhook.js +43 -0
  47. package/server/routes/memory.js +1 -4
  48. package/server/routes/settings.js +1 -2
  49. package/server/routes/skills.js +1 -1
  50. package/server/routes/triggers.js +2 -2
  51. package/server/services/account/erasure.js +263 -0
  52. package/server/services/account/sessions.js +1 -9
  53. package/server/services/ai/loop/agent_engine_core.js +42 -0
  54. package/server/services/ai/loop/blank_recovery.js +36 -0
  55. package/server/services/ai/loop/completion_judge.js +42 -0
  56. package/server/services/ai/loop/conversation_loop.js +248 -34
  57. package/server/services/ai/loop/progress_monitor.js +6 -6
  58. package/server/services/ai/loopPolicy.js +37 -44
  59. package/server/services/ai/messagingFallback.js +25 -1
  60. package/server/services/ai/models.js +18 -0
  61. package/server/services/ai/preModelCompaction.js +25 -5
  62. package/server/services/ai/rate_limits.js +28 -4
  63. package/server/services/ai/systemPrompt.js +23 -5
  64. package/server/services/ai/taskAnalysis.js +2 -0
  65. package/server/services/ai/tools.js +231 -20
  66. package/server/services/android/controller.js +6 -2
  67. package/server/services/billing/billing_email.js +106 -0
  68. package/server/services/billing/config.js +17 -0
  69. package/server/services/billing/plans.js +121 -0
  70. package/server/services/billing/stripe_client.js +21 -0
  71. package/server/services/billing/subscriptions.js +462 -0
  72. package/server/services/billing/trial_guard.js +111 -0
  73. package/server/services/browser/contentExtractor.js +629 -0
  74. package/server/services/browser/controller.js +4 -8
  75. package/server/services/desktop/gateway.js +7 -4
  76. package/server/services/integrations/google/calendar.js +30 -2
  77. package/server/services/integrations/secrets.js +7 -4
  78. package/server/services/manager.js +11 -0
  79. package/server/services/messaging/automation.js +1 -1
  80. package/server/services/messaging/telnyx.js +12 -11
  81. package/server/services/messaging/whatsapp.js +1 -1
  82. package/server/services/recordings/manager.js +13 -7
  83. package/server/services/runtime/backends/local-vm.js +40 -22
  84. package/server/services/runtime/docker-vm-manager.js +157 -266
  85. package/server/services/runtime/guest_bootstrap.js +17 -5
  86. package/server/services/runtime/guest_image.js +182 -0
  87. package/server/services/runtime/manager.js +0 -1
  88. package/server/services/runtime/validation.js +3 -8
  89. package/server/services/tasks/runtime.js +103 -15
  90. package/server/services/tasks/schedule_utils.js +5 -5
  91. package/server/services/voice/runtimeManager.js +19 -10
  92. package/server/services/wearable/gateway.js +8 -5
  93. package/server/services/websocket.js +26 -8
  94. package/server/services/workspace/manager.js +37 -3
@@ -910,7 +910,7 @@ p { margin: 0; text-wrap: pretty; }
910
910
  <div class="wrap">
911
911
  <div class="hero-grid">
912
912
  <div class="hero-copy">
913
- <div class="pill"><span class="tag">New</span> <b>NeoAgent is now in private beta</b></div>
913
+ <div class="pill"><span class="tag">New</span> <b>NeoAgent is now in beta</b></div>
914
914
  <h1>
915
915
  <span class="h1-line"><span class="w" style="--i:0">One</span> <span class="w" style="--i:1">agent</span> <span class="w" style="--i:2">for</span> <span class="w" style="--i:3">your</span></span>
916
916
  <span class="h1-line"><span class="w" style="--i:4"><em>whole</em></span> <span class="w" style="--i:5"><em>digital</em></span> <span class="w" style="--i:6"><em>life</em></span></span>
@@ -1194,7 +1194,7 @@ p { margin: 0; text-wrap: pretty; }
1194
1194
  <section class="section-tight" id="cta" data-screen-label="cta">
1195
1195
  <div class="wrap">
1196
1196
  <div class="final reveal">
1197
- <span class="eyebrow" style="display:block;margin-bottom:20px;">Private beta · Limited spots</span>
1197
+ <span class="eyebrow" style="display:block;margin-bottom:20px;">Beta · Limited spots</span>
1198
1198
  <h2>The agent that just <em>handles it.</em></h2>
1199
1199
  <p>Be first to put NeoAgent to work across your devices, channels, and life. We'll send your invite as spots open up.</p>
1200
1200
  <div class="hero-actions" style="justify-content:center;">
package/lib/manager.js CHANGED
@@ -1432,81 +1432,43 @@ function startFallback() {
1432
1432
  logOk(`Started detached process (pid ${child.pid})`);
1433
1433
  }
1434
1434
 
1435
- async function ensureQemuInstalled({ required = false } = {}) {
1436
- heading('Ensure QEMU Installed');
1435
+ // The isolated browser/CLI runtime runs each user's guest agent in a Docker
1436
+ // container built from a baked image (browser + dependencies installed at build
1437
+ // time). Docker cannot be auto-installed safely across platforms, so when it is
1438
+ // missing we surface clear guidance; when it is present we pre-build the guest
1439
+ // image so the first browser/CLI action is instant rather than paying a one-time
1440
+ // multi-minute build on demand.
1441
+ async function ensureContainerRuntime({ required = false } = {}) {
1442
+ heading('Ensure Container Runtime');
1437
1443
  const platform = detectPlatform();
1438
-
1439
- const hasSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
1440
- const hasImg = commandExists('qemu-img');
1441
1444
 
1442
- if (hasSystem && hasImg) {
1443
- logOk('QEMU components are already installed');
1444
- return;
1445
- }
1446
-
1447
- logInfo('QEMU components not found. Attempting to install...');
1445
+ const dockerReady = commandExists('docker')
1446
+ && spawnSync('docker', ['info'], { stdio: 'ignore', timeout: 10000 }).status === 0;
1448
1447
 
1449
- try {
1450
- if (platform === 'macos') {
1451
- if (!commandExists('brew')) {
1452
- const message = 'Homebrew is required for automatic QEMU install on macOS. Install Homebrew from https://brew.sh/ and run `neoagent install` again.';
1453
- if (required) throw new Error(message);
1454
- logWarn('Homebrew not found; VM features will stay unavailable until QEMU is installed.');
1455
- rememberInstallAction(message);
1456
- return false;
1457
- }
1458
- logInfo('Running "brew install qemu"...');
1459
- runOrThrow('brew', ['install', 'qemu']);
1460
- } else if (platform === 'linux') {
1461
- const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1462
- if (commandExists('apt-get')) {
1463
- const pkgs = ['qemu-utils'];
1464
- if (isArm) {
1465
- pkgs.push('qemu-system-arm');
1466
- } else {
1467
- pkgs.push('qemu-system-x86');
1468
- }
1469
- logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1470
- runOrThrow('sudo', ['apt-get', 'update']);
1471
- runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1472
- } else if (commandExists('dnf')) {
1473
- logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1474
- runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1475
- } else if (commandExists('yum')) {
1476
- logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1477
- runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1478
- } else {
1479
- const message = 'Unsupported Linux package manager for automatic QEMU install. Install qemu-system and qemu-utils, then run `neoagent install` again.';
1480
- if (required) throw new Error(message);
1481
- logWarn('Could not find apt-get, dnf, or yum for QEMU installation.');
1482
- rememberInstallAction(message);
1483
- return false;
1484
- }
1485
- } else {
1486
- const message = 'Unsupported platform for automatic QEMU installation. Install QEMU manually if you need VM-isolated runtime features.';
1487
- if (required) throw new Error(message);
1488
- logWarn('Skipping QEMU auto-install on this platform.');
1489
- rememberInstallAction(message);
1490
- return false;
1491
- }
1492
- } catch (err) {
1493
- if (required) throw err;
1494
- logWarn(`QEMU install did not complete: ${err.message}`);
1495
- rememberInstallAction('Install QEMU manually or rerun `neoagent install` after your package manager is ready.');
1448
+ if (!dockerReady) {
1449
+ const installHint = platform === 'macos'
1450
+ ? 'Install Docker Desktop from https://www.docker.com/products/docker-desktop/ and start it.'
1451
+ : 'Install Docker Engine (https://docs.docker.com/engine/install/) and ensure the daemon is running.';
1452
+ const message = `Docker is required for the isolated browser/CLI runtime. ${installHint} Then run \`neoagent install\` again.`;
1453
+ if (required) throw new Error(message);
1454
+ logWarn('Docker not detected; the cloud browser and CLI will be unavailable until Docker is installed and running.');
1455
+ rememberInstallAction(message);
1496
1456
  return false;
1497
1457
  }
1498
1458
 
1499
- const verifiedSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
1500
- const verifiedImg = commandExists('qemu-img');
1459
+ logOk('Docker is installed and the daemon is running');
1501
1460
 
1502
- if (verifiedSystem && verifiedImg) {
1503
- logOk('QEMU installed successfully');
1461
+ try {
1462
+ logInfo('Building the guest runtime image (one-time; downloads the browser and dependencies)…');
1463
+ const { GuestImageBuilder } = require('../server/services/runtime/guest_image');
1464
+ await new GuestImageBuilder({ runtimeProfile: 'browser_cli' }).ensure();
1465
+ logOk('Guest runtime image is ready');
1504
1466
  return true;
1505
- } else {
1506
- const message = 'QEMU installation finished but required binaries were not found in PATH.';
1467
+ } catch (err) {
1468
+ const message = `Guest runtime image build did not complete: ${err.message}`;
1507
1469
  if (required) throw new Error(message);
1508
1470
  logWarn(message);
1509
- rememberInstallAction('Ensure qemu-system and qemu-img are available in PATH, then run `neoagent install` again.');
1471
+ rememberInstallAction('Rerun `neoagent install` to retry building the guest runtime image, or it will build automatically on first use.');
1510
1472
  return false;
1511
1473
  }
1512
1474
  }
@@ -1585,7 +1547,7 @@ async function cmdInstall() {
1585
1547
  }
1586
1548
 
1587
1549
  installDependencies();
1588
- await ensureQemuInstalled({ required: false });
1550
+ await ensureContainerRuntime({ required: false });
1589
1551
  ensureYtDlpInstalled();
1590
1552
  buildBundledWebClientIfPossible({ required: true });
1591
1553
 
@@ -2000,6 +1962,152 @@ function cmdVersion() {
2000
1962
  console.log(currentInstalledVersionLabel());
2001
1963
  }
2002
1964
 
1965
+ async function cmdBilling(args = []) {
1966
+ const sub = (args[0] || 'status').toLowerCase();
1967
+
1968
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
1969
+ console.log('\nNeoAgent Billing');
1970
+ console.log('Usage: neoagent billing [subcommand]');
1971
+ console.log('');
1972
+ console.log('Subcommands:');
1973
+ console.log(' neoagent billing Show billing configuration status');
1974
+ console.log(' neoagent billing setup Interactive Stripe setup wizard');
1975
+ console.log(' neoagent billing enable Enable billing and restart');
1976
+ console.log(' neoagent billing disable Disable billing and restart');
1977
+ console.log('');
1978
+ return;
1979
+ }
1980
+
1981
+ if (sub === 'status') {
1982
+ cliBanner('Billing', 'configuration status');
1983
+ heading('Billing Status');
1984
+ const env = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
1985
+ const enabled = ['1', 'true', 'yes'].includes(String(env.NEOAGENT_BILLING_ENABLED || '').toLowerCase());
1986
+ const hasSecret = Boolean(env.STRIPE_SECRET_KEY);
1987
+ const hasPub = Boolean(env.STRIPE_PUBLISHABLE_KEY);
1988
+ const hasWebhook = Boolean(env.STRIPE_WEBHOOK_SECRET);
1989
+ const trialDays = env.BILLING_TRIAL_DAYS || '14';
1990
+
1991
+ cliSection('Billing config');
1992
+ statusLine(enabled, 'enabled', enabled ? 'yes' : 'no', enabled ? '' : 'run: neoagent billing enable');
1993
+ statusLine(hasSecret, 'secret', hasSecret ? maskEnvValue('SECRET', env.STRIPE_SECRET_KEY) : 'not set', hasSecret ? '' : 'run: neoagent billing setup');
1994
+ statusLine(hasPub, 'public', hasPub ? maskEnvValue('KEY', env.STRIPE_PUBLISHABLE_KEY) : 'not set');
1995
+ statusLine(hasWebhook, 'webhook', hasWebhook ? 'configured' : 'not set');
1996
+ console.log('');
1997
+ cliSection('Trial');
1998
+ console.log(` trial days ${trialDays}`);
1999
+
2000
+ const port = loadEnvPort();
2001
+ const publicUrl = env.PUBLIC_URL || `http://localhost:${port}`;
2002
+ console.log('');
2003
+ cliSection('Webhook endpoint');
2004
+ console.log(` ${publicUrl}/api/billing/webhook`);
2005
+ console.log('');
2006
+
2007
+ if (!hasSecret || !hasPub) {
2008
+ logWarn('Stripe keys not configured. Run `neoagent billing setup` to get started.');
2009
+ } else if (!enabled) {
2010
+ logInfo('Billing is configured but not enabled. Run `neoagent billing enable` to activate.');
2011
+ } else {
2012
+ logOk('Billing is active. Manage plans in Admin › Billing.');
2013
+ }
2014
+ return;
2015
+ }
2016
+
2017
+ if (sub === 'enable') {
2018
+ heading('Enable Billing');
2019
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'true');
2020
+ logOk('Set NEOAGENT_BILLING_ENABLED=true');
2021
+ logInfo('Restarting NeoAgent to apply changes...');
2022
+ cmdRestart();
2023
+ return;
2024
+ }
2025
+
2026
+ if (sub === 'disable') {
2027
+ heading('Disable Billing');
2028
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'false');
2029
+ logOk('Set NEOAGENT_BILLING_ENABLED=false');
2030
+ logInfo('Restarting NeoAgent to apply changes...');
2031
+ cmdRestart();
2032
+ return;
2033
+ }
2034
+
2035
+ if (sub === 'setup') {
2036
+ cliBanner('Billing Setup', 'Stripe configuration');
2037
+ heading('Billing Setup');
2038
+ ensureRuntimeDirs();
2039
+
2040
+ const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
2041
+ const port = loadEnvPort();
2042
+ const publicUrl = current.PUBLIC_URL || `http://localhost:${port}`;
2043
+
2044
+ logInfo('Press Enter to keep the current value shown in brackets.');
2045
+ logInfo(`Webhook endpoint: ${publicUrl}/api/billing/webhook`);
2046
+ console.log('');
2047
+
2048
+ heading('Stripe API keys');
2049
+ logInfo('Find these in your Stripe dashboard: https://dashboard.stripe.com/apikeys');
2050
+ const secretKey = await askSecret('Stripe secret key (sk_live_... or sk_test_...)', current.STRIPE_SECRET_KEY || '');
2051
+ const publishableKey = await askSecret('Stripe publishable key (pk_live_... or pk_test_...)', current.STRIPE_PUBLISHABLE_KEY || '');
2052
+
2053
+ if (secretKey && !secretKey.startsWith('sk_')) {
2054
+ logWarn('Secret key does not start with "sk_" — double-check the value.');
2055
+ }
2056
+ if (publishableKey && !publishableKey.startsWith('pk_')) {
2057
+ logWarn('Publishable key does not start with "pk_" — double-check the value.');
2058
+ }
2059
+
2060
+ heading('Webhook');
2061
+ logInfo(`Register this endpoint in Stripe: ${publicUrl}/api/billing/webhook`);
2062
+ logInfo('Required events: customer.subscription.*, invoice.payment_succeeded, invoice.payment_failed');
2063
+ const webhookSecret = await askSecret('Stripe webhook signing secret (whsec_...)', current.STRIPE_WEBHOOK_SECRET || '');
2064
+
2065
+ if (webhookSecret && !webhookSecret.startsWith('whsec_')) {
2066
+ logWarn('Webhook secret does not start with "whsec_" — double-check the value.');
2067
+ }
2068
+
2069
+ heading('Trial');
2070
+ const trialDaysRaw = await ask('Free trial length in days (0 to disable)', current.BILLING_TRIAL_DAYS || '14');
2071
+ const trialDays = Number(trialDaysRaw);
2072
+ if (!Number.isInteger(trialDays) || trialDays < 0) {
2073
+ throw new Error(`Invalid trial length "${trialDaysRaw}". Must be a non-negative integer.`);
2074
+ }
2075
+
2076
+ if (secretKey) upsertEnvValue('STRIPE_SECRET_KEY', secretKey);
2077
+ if (publishableKey) upsertEnvValue('STRIPE_PUBLISHABLE_KEY', publishableKey);
2078
+ if (webhookSecret) upsertEnvValue('STRIPE_WEBHOOK_SECRET', webhookSecret);
2079
+ upsertEnvValue('BILLING_TRIAL_DAYS', String(trialDays));
2080
+
2081
+ logOk('Stripe credentials saved to .env');
2082
+
2083
+ const isAlreadyEnabled = ['1', 'true', 'yes'].includes(String(current.NEOAGENT_BILLING_ENABLED || '').toLowerCase());
2084
+ let enableNow = isAlreadyEnabled;
2085
+ if (!isAlreadyEnabled) {
2086
+ const enableAnswer = await ask('Enable billing now? (y/N)', 'N');
2087
+ enableNow = enableAnswer.toLowerCase() === 'y' || enableAnswer.toLowerCase() === 'yes';
2088
+ }
2089
+
2090
+ if (enableNow) {
2091
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'true');
2092
+ logOk('Billing enabled');
2093
+ logInfo('Restarting NeoAgent to apply credentials...');
2094
+ cmdRestart();
2095
+ } else {
2096
+ logInfo('Billing is not yet enabled. Run `neoagent billing enable` when ready.');
2097
+ }
2098
+
2099
+ console.log('');
2100
+ heading('Next steps');
2101
+ logInfo('1. Verify the webhook endpoint is reachable from Stripe');
2102
+ logInfo('2. Create subscription plans in Admin › Billing › Plans');
2103
+ logInfo('3. Add a free plan (price = 0) for users without a paid subscription');
2104
+ logInfo(' See: neoagent billing status');
2105
+ return;
2106
+ }
2107
+
2108
+ throw new Error(`Unknown billing subcommand: ${sub}. Run "neoagent billing --help" for usage.`);
2109
+ }
2110
+
2003
2111
  function printHelp() {
2004
2112
  const c = COLORS;
2005
2113
  const W = 38;
@@ -2047,6 +2155,13 @@ function printHelp() {
2047
2155
  row('admin', 'Show admin dashboard URL and credentials');
2048
2156
  console.log('');
2049
2157
 
2158
+ cliSection('Billing');
2159
+ row('billing', 'Show billing configuration status');
2160
+ row('billing setup', 'Interactive Stripe setup wizard');
2161
+ row('billing enable', 'Enable billing and restart');
2162
+ row('billing disable', 'Disable billing and restart');
2163
+ console.log('');
2164
+
2050
2165
  cliSection('Maintenance');
2051
2166
  row('migrate', 'Migrate from another agent installation');
2052
2167
  row('migrate dry-run', 'Preview what would be migrated');
@@ -2106,6 +2221,9 @@ async function runCLI(argv) {
2106
2221
  case 'login':
2107
2222
  await cmdLogin(argv.slice(1));
2108
2223
  break;
2224
+ case 'billing':
2225
+ await cmdBilling(argv.slice(1));
2226
+ break;
2109
2227
  case 'admin': {
2110
2228
  cliBanner('Admin Dashboard', 'credentials');
2111
2229
  const adminCreds = readAdminCredentials();
@@ -458,6 +458,88 @@ function migrateToolPoliciesAllowAlways(db) {
458
458
  `);
459
459
  }
460
460
 
461
+ function migrateBilling(db) {
462
+ db.exec(`
463
+ CREATE TABLE IF NOT EXISTS billing_plans (
464
+ id TEXT PRIMARY KEY,
465
+ name TEXT NOT NULL,
466
+ description TEXT DEFAULT '',
467
+ price_cents INTEGER NOT NULL DEFAULT 0,
468
+ currency TEXT NOT NULL DEFAULT 'usd',
469
+ interval TEXT DEFAULT 'month',
470
+ stripe_price_id TEXT,
471
+ token_limit_4h INTEGER,
472
+ token_limit_weekly INTEGER,
473
+ allowed_models_json TEXT DEFAULT '[]',
474
+ features_json TEXT DEFAULT '[]',
475
+ is_active INTEGER DEFAULT 1,
476
+ sort_order INTEGER DEFAULT 0,
477
+ created_at TEXT DEFAULT (datetime('now')),
478
+ updated_at TEXT DEFAULT (datetime('now'))
479
+ );
480
+
481
+ CREATE TABLE IF NOT EXISTS billing_customers (
482
+ user_id INTEGER PRIMARY KEY,
483
+ stripe_customer_id TEXT UNIQUE,
484
+ created_at TEXT DEFAULT (datetime('now')),
485
+ updated_at TEXT DEFAULT (datetime('now')),
486
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
487
+ );
488
+
489
+ CREATE TABLE IF NOT EXISTS user_subscriptions (
490
+ id TEXT PRIMARY KEY,
491
+ user_id INTEGER NOT NULL,
492
+ plan_id TEXT NOT NULL,
493
+ stripe_subscription_id TEXT UNIQUE,
494
+ status TEXT NOT NULL,
495
+ trial_ends_at TEXT,
496
+ current_period_start TEXT,
497
+ current_period_end TEXT,
498
+ cancel_at_period_end INTEGER DEFAULT 0,
499
+ canceled_at TEXT,
500
+ created_at TEXT DEFAULT (datetime('now')),
501
+ updated_at TEXT DEFAULT (datetime('now')),
502
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
503
+ FOREIGN KEY (plan_id) REFERENCES billing_plans(id)
504
+ );
505
+
506
+ CREATE TABLE IF NOT EXISTS subscription_events (
507
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
508
+ user_id INTEGER NOT NULL,
509
+ subscription_id TEXT,
510
+ event_type TEXT NOT NULL,
511
+ stripe_event_id TEXT UNIQUE,
512
+ payload_json TEXT DEFAULT '{}',
513
+ created_at TEXT DEFAULT (datetime('now')),
514
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
515
+ );
516
+
517
+ CREATE TABLE IF NOT EXISTS trial_fingerprints (
518
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
519
+ fingerprint_hash TEXT NOT NULL,
520
+ fingerprint_type TEXT NOT NULL,
521
+ user_id INTEGER,
522
+ used_at TEXT DEFAULT (datetime('now')),
523
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
524
+ );
525
+
526
+ CREATE INDEX IF NOT EXISTS idx_billing_customers_stripe
527
+ ON billing_customers(stripe_customer_id);
528
+
529
+ CREATE INDEX IF NOT EXISTS idx_user_subscriptions_user
530
+ ON user_subscriptions(user_id, status, updated_at DESC);
531
+
532
+ CREATE INDEX IF NOT EXISTS idx_user_subscriptions_stripe
533
+ ON user_subscriptions(stripe_subscription_id);
534
+
535
+ CREATE INDEX IF NOT EXISTS idx_subscription_events_stripe
536
+ ON subscription_events(stripe_event_id);
537
+
538
+ CREATE INDEX IF NOT EXISTS idx_trial_fingerprints_hash
539
+ ON trial_fingerprints(fingerprint_hash, fingerprint_type);
540
+ `);
541
+ }
542
+
461
543
  function runSchemaMigrations(db) {
462
544
  migrateMemoryEmbeddingIndex(db);
463
545
  migrateMemoryProvenance(db);
@@ -469,6 +551,7 @@ function runSchemaMigrations(db) {
469
551
  migrateToolPermissions(db);
470
552
  migrateApprovalPersistence(db);
471
553
  migrateToolPoliciesAllowAlways(db);
554
+ migrateBilling(db);
472
555
  }
473
556
 
474
557
  module.exports = {
@@ -482,5 +565,6 @@ module.exports = {
482
565
  migrateToolPermissions,
483
566
  migrateApprovalPersistence,
484
567
  migrateToolPoliciesAllowAlways,
568
+ migrateBilling,
485
569
  runSchemaMigrations,
486
570
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.0.1-beta.2",
3
+ "version": "3.0.1-beta.21",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -51,10 +51,14 @@
51
51
  "flutter:test": "cd flutter_app && flutter test ../test/flutter/unit ../test/flutter/widget",
52
52
  "flutter:test:unit": "cd flutter_app && flutter test ../test/flutter/unit",
53
53
  "flutter:test:widget": "cd flutter_app && flutter test ../test/flutter/widget",
54
+ "benchmark:setup": "node benchmark/cli.js setup",
55
+ "benchmark:run": "node benchmark/cli.js run",
56
+ "benchmark:report": "node benchmark/cli.js report",
54
57
  "benchmark:tokens": "node scripts/benchmark-token-cost.js",
55
58
  "benchmark:memory": "node scripts/benchmark-memory.js",
56
59
  "benchmark:memorybench:install": "node scripts/install-memorybench-provider.js",
57
- "release": "npx semantic-release"
60
+ "release": "npx semantic-release",
61
+ "audit:ci": "npm audit --omit=dev --audit-level=high"
58
62
  },
59
63
  "repository": {
60
64
  "type": "git",
@@ -99,11 +103,12 @@
99
103
  "remotion": "^4.0.459",
100
104
  "sharp": "^0.34.5",
101
105
  "socket.io": "^4.8.1",
106
+ "stripe": "^22.2.1",
102
107
  "telegraf": "^4.16.3",
103
108
  "telnyx": "^5.51.0",
104
109
  "tesseract.js": "^7.0.0",
105
110
  "uuid": "^11.1.0",
106
- "ws": "^8.19.0"
111
+ "ws": "^8.21.0"
107
112
  },
108
113
  "overrides": {
109
114
  "axios": "^1.15.2",
@@ -113,7 +118,8 @@
113
118
  "protobufjs": "^7.5.5",
114
119
  "serialize-javascript": "^7.0.5",
115
120
  "undici": "^6.24.0",
116
- "webpackbar": "^7.0.0"
121
+ "webpackbar": "^7.0.0",
122
+ "ws": "^8.21.0"
117
123
  },
118
124
  "devDependencies": {
119
125
  "@docusaurus/core": "3.10.0",
@@ -113,12 +113,12 @@ async function setSignup(enabled) {
113
113
  }
114
114
 
115
115
  async function rotateApiKey(btn) {
116
- if (!confirm(
117
- 'Rotate admin API key?\n\n' +
118
- 'The current key will stop working immediately.\n' +
119
- 'Any scripts using it must be updated.\n\n' +
120
- 'The new key will be shown once — copy it before leaving.'
121
- )) return;
116
+ if (!await showConfirmModal({
117
+ title: 'Rotate admin API key?',
118
+ body: 'The current key will stop working immediately. Any scripts using it must be updated.<br><br>The new key will be shown once — copy it before leaving.',
119
+ confirmLabel: 'Rotate Key',
120
+ confirmClass: 'btn-danger',
121
+ })) return;
122
122
  btn.disabled = true;
123
123
  btn.textContent = 'Rotating…';
124
124
  try {
@@ -140,7 +140,12 @@ async function rotateApiKey(btn) {
140
140
  }
141
141
 
142
142
  async function revokeApiKey(btn) {
143
- if (!confirm('Revoke admin API key?\n\nAll programmatic access using this key will stop immediately.')) return;
143
+ if (!await showConfirmModal({
144
+ title: 'Revoke admin API key?',
145
+ body: 'All programmatic access using this key will stop immediately.',
146
+ confirmLabel: 'Revoke Key',
147
+ confirmClass: 'btn-danger',
148
+ })) return;
144
149
  btn.disabled = true;
145
150
  btn.textContent = 'Revoking…';
146
151
  _revealedKey = null;
@@ -318,3 +318,81 @@ button:disabled,
318
318
  border: 1px solid rgba(116, 192, 124, 0.25);
319
319
  color: var(--success);
320
320
  }
321
+
322
+ /* ── Form helpers (used in dynamic modals) ───────────────────────────────── */
323
+ .form-label {
324
+ display: block;
325
+ font-size: 11px;
326
+ font-weight: 600;
327
+ letter-spacing: 0.04em;
328
+ color: var(--text-muted);
329
+ text-transform: uppercase;
330
+ margin-bottom: 6px;
331
+ }
332
+
333
+ .form-label small {
334
+ font-size: 10px;
335
+ font-weight: 400;
336
+ text-transform: none;
337
+ letter-spacing: 0;
338
+ opacity: 0.75;
339
+ }
340
+
341
+ .form-input {
342
+ width: 100%;
343
+ background: var(--bg-input);
344
+ border: 1px solid var(--border-light);
345
+ border-radius: var(--radius-sm);
346
+ padding: 9px 12px;
347
+ color: var(--text);
348
+ font-family: var(--font-sans);
349
+ font-size: 13px;
350
+ outline: none;
351
+ transition: border-color 0.15s;
352
+ -webkit-appearance: none;
353
+ }
354
+
355
+ .form-input:focus {
356
+ border-color: var(--accent);
357
+ box-shadow: 0 0 0 3px var(--accent-muted);
358
+ }
359
+
360
+ .form-input:disabled {
361
+ opacity: 0.5;
362
+ cursor: default;
363
+ }
364
+
365
+ /* ── Small button variant ────────────────────────────────────────────────── */
366
+ .btn-sm {
367
+ padding: 5px 10px;
368
+ font-size: 12px;
369
+ }
370
+
371
+ /* ── Generic data table (billing, etc.) ──────────────────────────────────── */
372
+ .data-table {
373
+ width: 100%;
374
+ border-collapse: collapse;
375
+ font-size: 13px;
376
+ }
377
+
378
+ .data-table th {
379
+ text-align: left;
380
+ font-size: 10px;
381
+ font-weight: 600;
382
+ text-transform: uppercase;
383
+ letter-spacing: 0.07em;
384
+ color: var(--text-muted);
385
+ padding: 6px 10px;
386
+ border-bottom: 1px solid var(--border);
387
+ white-space: nowrap;
388
+ }
389
+
390
+ .data-table td {
391
+ padding: 10px 10px;
392
+ border-bottom: 1px solid var(--border);
393
+ color: var(--text-secondary);
394
+ vertical-align: middle;
395
+ }
396
+
397
+ .data-table tr:last-child td { border-bottom: none; }
398
+ .data-table tr:hover td { background: var(--row-hover); }