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
@@ -31,9 +31,9 @@ async function saveDefaultRateLimits(btn) {
31
31
  headers: { 'Content-Type': 'application/json' },
32
32
  body: JSON.stringify({ rate_limit_4h: parse(v4h), rate_limit_weekly: parse(vw) }),
33
33
  });
34
- if (!res.ok) { const b = await res.json().catch(() => ({})); alert(b.error || 'Failed'); }
34
+ if (!res.ok) { const b = await res.json().catch(() => ({})); showToast(b.error || 'Failed', 'error'); }
35
35
  else { btn.textContent = 'Saved!'; setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2000); return; }
36
- } catch (_) { alert('Network error'); }
36
+ } catch (_) { showToast('Network error', 'error'); }
37
37
  btn.disabled = false;
38
38
  btn.textContent = orig;
39
39
  }
@@ -132,7 +132,7 @@ function renderUsersTable(el, users) {
132
132
  }
133
133
 
134
134
  async function forceLogout(id, username, btn) {
135
- if (!confirm(`Force logout ${username}?\n\nThis will revoke all active sessions. They can log back in.`)) return;
135
+ if (!await showConfirmModal({ title: `Force logout @${esc(username)}?`, body: 'This will revoke all active sessions. They can log back in.', confirmLabel: 'Force Logout', confirmClass: 'btn-danger' })) return;
136
136
  btn.disabled = true;
137
137
  try {
138
138
  const res = await api(`/admin/api/users/${id}/sessions`, { method: 'DELETE' });
@@ -141,22 +141,22 @@ async function forceLogout(id, username, btn) {
141
141
  setTimeout(loadUsers, 800);
142
142
  } else {
143
143
  const b = await res.json().catch(() => ({}));
144
- alert(b.error || 'Failed');
144
+ showToast(b.error || 'Failed', 'error');
145
145
  btn.disabled = false;
146
146
  }
147
147
  } catch (err) {
148
- if (err.message !== 'unauthorized') alert('Network error');
148
+ if (err.message !== 'unauthorized') showToast('Network error', 'error');
149
149
  btn.disabled = false;
150
150
  }
151
151
  }
152
152
 
153
153
  async function deleteUser(id, displayName, btn) {
154
- if (!confirm(
155
- `⚠ GDPR Erasure — permanently delete "${displayName}"?\n\n` +
156
- 'This will delete the account and ALL associated data:\n' +
157
- ' Agent runs, steps, and messages\n• Memories and conversations\n• Integrations and platform connections\n• Artifacts and files on disk\n• All sessions\n\n' +
158
- 'This action CANNOT be undone.'
159
- )) return;
154
+ if (!await showConfirmModal({
155
+ title: `Delete "${esc(displayName)}"?`,
156
+ body: `<strong>GDPR Erasure</strong> — permanently deletes the account and <strong>all</strong> associated data:<br><br>• Agent runs, steps, and messages<br>• Memories and conversations<br>• Integrations and platform connections<br>• Artifacts and files on disk<br>• All sessions<br><br><span style="color:var(--danger)">This action cannot be undone.</span>`,
157
+ confirmLabel: 'Delete User',
158
+ confirmClass: 'btn-danger',
159
+ })) return;
160
160
  btn.disabled = true;
161
161
  btn.textContent = 'Deleting…';
162
162
  try {
@@ -170,12 +170,12 @@ async function deleteUser(id, displayName, btn) {
170
170
  }
171
171
  } else {
172
172
  const b = await res.json().catch(() => ({}));
173
- alert(b.error || 'Failed to delete user');
173
+ showToast(b.error || 'Failed to delete user', 'error');
174
174
  btn.disabled = false;
175
175
  btn.textContent = 'Delete';
176
176
  }
177
177
  } catch (err) {
178
- if (err.message !== 'unauthorized') alert('Network error');
178
+ if (err.message !== 'unauthorized') showToast('Network error', 'error');
179
179
  btn.disabled = false;
180
180
  btn.textContent = 'Delete';
181
181
  }
@@ -266,12 +266,12 @@ async function editRateLimits(id, username) {
266
266
  overlay.remove();
267
267
  await fetchUsers(document.getElementById('user-search')?.value?.trim() || '');
268
268
  } catch (_) {
269
- alert('Failed to save rate limits');
269
+ showToast('Failed to save rate limits', 'error');
270
270
  bSave.disabled = false;
271
271
  bSave.textContent = 'Save';
272
272
  }
273
273
  };
274
274
  } catch (_) {
275
- alert('Failed to fetch rate limits');
275
+ showToast('Failed to fetch rate limits', 'error');
276
276
  }
277
277
  }
@@ -51,13 +51,6 @@ function initializeDatabase(db, dbPath) {
51
51
  return db;
52
52
  }
53
53
 
54
- function sleepSync(ms) {
55
- if (ms <= 0) return;
56
- const buffer = new SharedArrayBuffer(4);
57
- const view = new Int32Array(buffer);
58
- Atomics.wait(view, 0, 0, ms);
59
- }
60
-
61
54
  function isSqliteBusyError(error) {
62
55
  return Boolean(
63
56
  error &&
@@ -69,7 +62,9 @@ function isSqliteBusyError(error) {
69
62
  );
70
63
  }
71
64
 
72
- function runWithBusyRetry(action, { attempts = 5, delayMs = 50, label = 'SQLite operation' } = {}) {
65
+ function runWithBusyRetry(action, { attempts = 3, label = 'SQLite operation' } = {}) {
66
+ // busy_timeout pragma already handles waiting at the SQLite layer;
67
+ // this loop is a last-resort failsafe without adding extra main-thread blocking.
73
68
  let lastError = null;
74
69
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
75
70
  try {
@@ -80,9 +75,8 @@ function runWithBusyRetry(action, { attempts = 5, delayMs = 50, label = 'SQLite
80
75
  throw error;
81
76
  }
82
77
  console.warn(
83
- `[Database] ${label} hit a busy lock on attempt ${attempt}/${attempts}; retrying in ${delayMs}ms.`,
78
+ `[Database] ${label} hit a busy lock on attempt ${attempt}/${attempts}; retrying.`,
84
79
  );
85
- sleepSync(delayMs);
86
80
  }
87
81
  }
88
82
  throw lastError;
@@ -1328,6 +1322,7 @@ for (const col of [
1328
1322
  "ALTER TABLE memory_facts ADD COLUMN invalidated_at TEXT",
1329
1323
  "ALTER TABLE memory_facts ADD COLUMN status TEXT DEFAULT 'active'",
1330
1324
  "ALTER TABLE memory_facts ADD COLUMN supersedes_fact_id TEXT",
1325
+ "ALTER TABLE users ADD COLUMN billing_override_plan_id TEXT",
1331
1326
  ]) {
1332
1327
  try { db.exec(col); } catch { /* column already exists */ }
1333
1328
  }
@@ -1879,7 +1874,7 @@ function migrateIntegrationProviderConfigsTable() {
1879
1874
  }
1880
1875
 
1881
1876
  function migrateIntegrationSecretStorage() {
1882
- try {
1877
+ const migrate = db.transaction(() => {
1883
1878
  const connectionRows = db
1884
1879
  .prepare('SELECT id, credentials_json FROM integration_connections')
1885
1880
  .all();
@@ -1891,11 +1886,7 @@ function migrateIntegrationSecretStorage() {
1891
1886
  if (!current || isEncryptedValue(current)) continue;
1892
1887
  updateConnection.run(encryptValue(current), row.id);
1893
1888
  }
1894
- } catch {
1895
- // Preserve startup even if a row cannot be re-encrypted.
1896
- }
1897
1889
 
1898
- try {
1899
1890
  const stateRows = db
1900
1891
  .prepare('SELECT id, code_verifier FROM integration_oauth_states')
1901
1892
  .all();
@@ -1907,11 +1898,7 @@ function migrateIntegrationSecretStorage() {
1907
1898
  if (!current || isEncryptedValue(current)) continue;
1908
1899
  updateState.run(encryptValue(current), row.id);
1909
1900
  }
1910
- } catch {
1911
- // Preserve startup even if a row cannot be re-encrypted.
1912
- }
1913
1901
 
1914
- try {
1915
1902
  const providerRows = db
1916
1903
  .prepare('SELECT id, config_json FROM integration_provider_configs')
1917
1904
  .all();
@@ -1923,8 +1910,13 @@ function migrateIntegrationSecretStorage() {
1923
1910
  if (!current || isEncryptedValue(current)) continue;
1924
1911
  updateProviderConfig.run(encryptValue(current), row.id);
1925
1912
  }
1926
- } catch {
1927
- // Preserve startup even if a row cannot be re-encrypted.
1913
+ });
1914
+
1915
+ try {
1916
+ migrate();
1917
+ } catch (err) {
1918
+ console.error('[DB] Integration secret migration failed — startup cannot continue with partially migrated secrets:', err.message);
1919
+ throw err;
1928
1920
  }
1929
1921
  }
1930
1922
 
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const Sqlite = require('better-sqlite3');
4
+ const { DATA_DIR } = require('../../runtime/paths');
5
+
6
+ const sessionsDb = new Sqlite(`${DATA_DIR}/sessions.db`);
7
+
8
+ module.exports = sessionsDb;
@@ -1,15 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const session = require('express-session');
4
- const Sqlite = require('better-sqlite3');
5
4
  const SQLiteStore = require('better-sqlite3-session-store')(session);
6
5
  const helmet = require('helmet');
7
6
  const cors = require('cors');
8
- const { DATA_DIR } = require('../../runtime/paths');
9
7
  const { logRequestSummary } = require('../utils/logger');
10
8
  const { getSessionSecret } = require('../services/account/session_secret');
11
9
 
12
- const sessionsDb = new Sqlite(`${DATA_DIR}/sessions.db`);
10
+ const sessionsDb = require('../db/sessions_db');
13
11
  const LEGACY_SESSION_EXPIRE_FALLBACK = 0;
14
12
 
15
13
  function boolEnv(name, fallback = false) {
@@ -89,7 +87,9 @@ function buildHelmetOptions({ secureCookies }) {
89
87
  }
90
88
 
91
89
  return {
92
- strictTransportSecurity: false,
90
+ strictTransportSecurity: secureCookies
91
+ ? { maxAge: Number(process.env.NEOAGENT_HSTS_MAX_AGE ?? 86400), includeSubDomains: false }
92
+ : false,
93
93
  crossOriginOpenerPolicy: false,
94
94
  crossOriginResourcePolicy: { policy: 'same-site' },
95
95
  originAgentCluster: false,
@@ -48,6 +48,15 @@ function registerApiRoutes(app) {
48
48
  }
49
49
  }
50
50
 
51
+ // Billing routes are mounted conditionally — only when billing is enabled.
52
+ // The webhook must be mounted before express.json() consumes the raw body;
53
+ // billing_webhook.js applies express.raw() inline for its own route.
54
+ const { isBillingEnabled } = require('../services/billing/config');
55
+ if (isBillingEnabled()) {
56
+ app.use('/api/billing/webhook', require('../routes/billing_webhook'));
57
+ app.use('/api/billing', require('../routes/billing'));
58
+ }
59
+
51
60
  setupTelnyxWebhook(app);
52
61
 
53
62
  app.get('/api/health', requireAuth, (req, res) => {
@@ -78,8 +78,10 @@ function registerStaticRoutes(app) {
78
78
  app.get(/^\/app(\/.*)?$/, serveFlutterApp);
79
79
 
80
80
  // Landing page at /
81
- app.use(express.static(LANDING_DIR));
82
- app.get('/', (req, res) => res.sendFile(path.join(LANDING_DIR, 'index.html')));
81
+ if (fs.existsSync(path.join(LANDING_DIR, 'index.html'))) {
82
+ app.use(express.static(LANDING_DIR));
83
+ app.get('/', (req, res) => res.sendFile(path.join(LANDING_DIR, 'index.html')));
84
+ }
83
85
  }
84
86
 
85
87
  function serveFlutterApp(req, res) {
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const { isBillingEnabled } = require('../services/billing/config');
4
+
5
+ function requireBilling(req, res, next) {
6
+ if (!isBillingEnabled()) {
7
+ return res.status(404).json({ error: 'Billing is not enabled on this server.' });
8
+ }
9
+ next();
10
+ }
11
+
12
+ module.exports = { requireBilling };
@@ -1 +1 @@
1
- c04ddd17135c19eaa7255284a042b387
1
+ 480214d898daad039c3f7d2d405af709
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "3187756582" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "1637623305" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });