neoagent 1.6.0 → 2.0.0

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 (62) hide show
  1. package/README.md +18 -4
  2. package/docs/configuration.md +2 -2
  3. package/docs/skills.md +1 -1
  4. package/lib/manager.js +64 -2
  5. package/package.json +9 -2
  6. package/server/config/origins.js +34 -0
  7. package/server/db/database.js +0 -13
  8. package/server/http/errors.js +17 -0
  9. package/server/http/middleware.js +81 -0
  10. package/server/http/routes.js +45 -0
  11. package/server/http/socket.js +23 -0
  12. package/server/http/static.js +50 -0
  13. package/server/index.js +50 -188
  14. package/server/public/.last_build_id +1 -0
  15. package/server/public/assets/AssetManifest.bin +1 -0
  16. package/server/public/assets/AssetManifest.bin.json +1 -0
  17. package/server/public/assets/AssetManifest.json +1 -0
  18. package/server/public/assets/FontManifest.json +1 -0
  19. package/server/public/assets/NOTICES +33454 -0
  20. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  21. package/server/public/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf +0 -0
  22. package/server/public/assets/shaders/ink_sparkle.frag +126 -0
  23. package/server/public/assets/web/icons/Icon-192.png +0 -0
  24. package/server/public/canvaskit/canvaskit.js +192 -0
  25. package/server/public/canvaskit/canvaskit.js.symbols +12142 -0
  26. package/server/public/canvaskit/canvaskit.wasm +0 -0
  27. package/server/public/canvaskit/chromium/canvaskit.js +192 -0
  28. package/server/public/canvaskit/chromium/canvaskit.js.symbols +11106 -0
  29. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  30. package/server/public/canvaskit/skwasm.js +140 -0
  31. package/server/public/canvaskit/skwasm.js.symbols +12164 -0
  32. package/server/public/canvaskit/skwasm.wasm +0 -0
  33. package/server/public/canvaskit/skwasm_heavy.js +140 -0
  34. package/server/public/canvaskit/skwasm_heavy.js.symbols +13766 -0
  35. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  36. package/server/public/favicon.png +0 -0
  37. package/server/public/flutter.js +32 -0
  38. package/server/public/flutter_bootstrap.js +43 -0
  39. package/server/public/flutter_service_worker.js +208 -0
  40. package/server/public/icons/Icon-192.png +0 -0
  41. package/server/public/icons/Icon-512.png +0 -0
  42. package/server/public/icons/Icon-maskable-192.png +0 -0
  43. package/server/public/icons/Icon-maskable-512.png +0 -0
  44. package/server/public/index.html +38 -0
  45. package/server/public/main.dart.js +103124 -0
  46. package/server/public/manifest.json +35 -0
  47. package/server/public/version.json +1 -0
  48. package/server/services/ai/models.js +2 -8
  49. package/server/services/ai/tools.js +0 -47
  50. package/server/services/browser/controller.js +34 -0
  51. package/server/services/manager.js +49 -118
  52. package/server/services/messaging/automation.js +210 -0
  53. package/server/utils/version.js +37 -0
  54. package/server/public/app.html +0 -682
  55. package/server/public/assets/world-office-dark.png +0 -0
  56. package/server/public/assets/world-office-light.png +0 -0
  57. package/server/public/css/app.css +0 -941
  58. package/server/public/css/styles.css +0 -963
  59. package/server/public/favicon.svg +0 -17
  60. package/server/public/js/app.js +0 -4105
  61. package/server/public/login.html +0 -313
  62. package/server/routes/protocols.js +0 -87
package/README.md CHANGED
@@ -6,12 +6,11 @@
6
6
 
7
7
  [![Node.js](https://img.shields.io/badge/Node.js-18+-5fa04e?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org)
8
8
  [![SQLite](https://img.shields.io/badge/SQLite-WAL-003b57?style=flat-square&logo=sqlite&logoColor=white)](https://sqlite.org)
9
- [![Multi-platform](https://img.shields.io/badge/macOS%20%2F%20Linux-supported-6366f1?style=flat-square&logo=apple&logoColor=white)](#)
9
+ [![Flutter](https://img.shields.io/badge/Flutter-web%20%2B%20android-02569B?style=flat-square&logo=flutter&logoColor=white)](https://flutter.dev)
10
10
  [![License](https://img.shields.io/badge/License-MIT-a855f7?style=flat-square)](LICENSE)
11
- [![Android APK](https://img.shields.io/badge/Android-Download%20APK-3ddc84?style=flat-square&logo=android&logoColor=white)](https://github.com/NeoLabs-Systems/NeoAgent/releases/latest/download/app-debug.apk)
12
11
 
13
- A self-hosted, proactive AI agent with a web UI no cloud dependency, no limits.
14
- Connects to Anthropic, OpenAI, xAI, Google and local Ollama models.
12
+ A self-hosted, proactive AI agent with a Flutter client for web and Android.
13
+ Connects to OpenAI, xAI, Google, and local Ollama with `qwen3.5:4b`.
15
14
  Runs tasks on a schedule, controls a browser, manages files, and talks to you over Telegram, Discord, or WhatsApp.
16
15
 
17
16
  ```bash
@@ -31,6 +30,21 @@ neoagent update
31
30
  neoagent logs
32
31
  ```
33
32
 
33
+ Build the Flutter web client:
34
+ ```bash
35
+ npm run flutter:build:web
36
+ ```
37
+
38
+ The installer and npm package ship the bundled web client from `server/public`, so Flutter is only needed when you want to rebuild the frontend locally.
39
+
40
+ Local development helpers live in `dev/`:
41
+ ```bash
42
+ ./dev/backend.sh
43
+ ./dev/web.sh
44
+ ./dev/stack.sh
45
+ ./dev/test.sh
46
+ ```
47
+
34
48
  ---
35
49
 
36
50
  [⚙️ Configuration](docs/configuration.md) · [🧰 Skills](docs/skills.md) · [🐛 Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
@@ -17,7 +17,7 @@ You can override the runtime root with `NEOAGENT_HOME`.
17
17
 
18
18
  ### AI Providers
19
19
 
20
- At least one API key is required. The active provider and model are configured in the web UI.
20
+ At least one API key is required. The active provider and model are configured in the Flutter app.
21
21
 
22
22
  | Variable | Provider |
23
23
  |---|---|
@@ -34,7 +34,7 @@ At least one API key is required. The active provider and model are configured i
34
34
  |---|---|
35
35
  | `TELNYX_WEBHOOK_TOKEN` | Telnyx webhook signature verification |
36
36
 
37
- Telegram, Discord, and WhatsApp tokens are stored in the database via the web UI Settings page — not in `.env`.
37
+ Telegram, Discord, and WhatsApp tokens are stored in the database via the Flutter app settings page — not in `.env`.
38
38
 
39
39
  ## Runtime data paths
40
40
 
package/docs/skills.md CHANGED
@@ -46,4 +46,4 @@ The agent reads all `.md` files in the skills directory on each conversation tur
46
46
 
47
47
  ## MCP tools
48
48
 
49
- External tools are connected via the [Model Context Protocol](https://modelcontextprotocol.io). Configure MCP servers in the web UI under **Settings → MCP**. Connected tools appear alongside built-in skills automatically.
49
+ External tools are connected via the [Model Context Protocol](https://modelcontextprotocol.io). Configure MCP servers in the Flutter app under **Models / Settings → MCP**. Connected tools appear alongside built-in skills automatically.
package/lib/manager.js CHANGED
@@ -21,6 +21,8 @@ const SERVICE_LABEL = 'com.neoagent';
21
21
  const PLIST_SRC = path.join(APP_DIR, 'com.neoagent.plist');
22
22
  const PLIST_DST = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.neoagent.plist');
23
23
  const SYSTEMD_UNIT = path.join(os.homedir(), '.config', 'systemd', 'user', 'neoagent.service');
24
+ const FLUTTER_APP_DIR = path.join(APP_DIR, 'flutter_app');
25
+ const WEB_CLIENT_DIR = path.join(APP_DIR, 'server', 'public');
24
26
 
25
27
  const COLORS = process.stdout.isTTY
26
28
  ? {
@@ -137,6 +139,14 @@ function runQuiet(cmd, args, options = {}) {
137
139
  return spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', cwd: APP_DIR, ...options });
138
140
  }
139
141
 
142
+ function withInstallEnv(extraEnv = {}) {
143
+ return {
144
+ ...process.env,
145
+ PUPPETEER_SKIP_DOWNLOAD: process.env.PUPPETEER_SKIP_DOWNLOAD || 'true',
146
+ ...extraEnv
147
+ };
148
+ }
149
+
140
150
  function commandExists(cmd) {
141
151
  const res = runQuiet('bash', ['-lc', `command -v ${cmd}`]);
142
152
  return res.status === 0;
@@ -259,10 +269,53 @@ async function cmdSetup() {
259
269
 
260
270
  function installDependencies() {
261
271
  heading('Dependencies');
262
- runOrThrow('npm', ['install', '--omit=dev', '--no-audit', '--no-fund']);
272
+ runOrThrow('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
273
+ env: withInstallEnv()
274
+ });
263
275
  logOk('Dependencies installed');
264
276
  }
265
277
 
278
+ function hasBundledWebClient() {
279
+ return fs.existsSync(path.join(WEB_CLIENT_DIR, 'index.html'));
280
+ }
281
+
282
+ function buildBundledWebClientIfPossible({ required = false } = {}) {
283
+ heading('Web Client');
284
+
285
+ if (!fs.existsSync(FLUTTER_APP_DIR)) {
286
+ if (hasBundledWebClient()) {
287
+ logOk('Using bundled Flutter web client');
288
+ return false;
289
+ }
290
+ if (required) {
291
+ throw new Error(`Missing Flutter app sources at ${FLUTTER_APP_DIR}`);
292
+ }
293
+ logWarn('Flutter app sources not found; keeping existing bundled web client');
294
+ return false;
295
+ }
296
+
297
+ if (!commandExists('flutter')) {
298
+ if (hasBundledWebClient()) {
299
+ logWarn('Flutter SDK not found; using bundled web client');
300
+ return false;
301
+ }
302
+ throw new Error('Flutter SDK is required to build the web client because no bundled client was found.');
303
+ }
304
+
305
+ runOrThrow('flutter', [
306
+ 'build',
307
+ 'web',
308
+ '--output',
309
+ '../server/public',
310
+ `--dart-define=NEOAGENT_BACKEND_URL=${process.env.NEOAGENT_BACKEND_URL || ''}`
311
+ ], {
312
+ cwd: FLUTTER_APP_DIR,
313
+ env: process.env
314
+ });
315
+ logOk('Bundled Flutter web client updated');
316
+ return true;
317
+ }
318
+
266
319
  function installMacService() {
267
320
  ensureLogDir();
268
321
  fs.mkdirSync(path.dirname(PLIST_DST), { recursive: true });
@@ -325,6 +378,7 @@ async function cmdInstall() {
325
378
  }
326
379
 
327
380
  installDependencies();
381
+ buildBundledWebClientIfPossible({ required: true });
328
382
 
329
383
  const platform = detectPlatform();
330
384
  if (platform === 'macos' && commandExists('launchctl')) {
@@ -481,15 +535,19 @@ function cmdUpdate() {
481
535
  if (current.status === 0 && next.status === 0 && current.stdout.trim() !== next.stdout.trim()) {
482
536
  logOk(`Updated ${current.stdout.trim()} -> ${next.stdout.trim()}`);
483
537
  installDependencies();
538
+ buildBundledWebClientIfPossible();
484
539
  } else {
485
540
  logOk('Already up to date');
541
+ buildBundledWebClientIfPossible();
486
542
  }
487
543
  } else {
488
544
  logWarn('No git repo detected; attempting npm global update.');
489
545
  if (commandExists('npm')) {
490
546
  try {
491
547
  backupRuntimeData();
492
- runOrThrow('npm', ['install', '-g', 'neoagent@latest']);
548
+ runOrThrow('npm', ['install', '-g', 'neoagent@latest'], {
549
+ env: withInstallEnv()
550
+ });
493
551
  logOk('npm global update completed');
494
552
  } catch {
495
553
  logWarn('npm global update failed. Run: npm install -g neoagent@latest');
@@ -499,6 +557,10 @@ function cmdUpdate() {
499
557
  }
500
558
  }
501
559
 
560
+ if (!hasBundledWebClient()) {
561
+ throw new Error('No bundled Flutter web client found after update.');
562
+ }
563
+
502
564
  cmdRestart();
503
565
  }
504
566
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "1.6.0",
3
+ "version": "2.0.0",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -21,6 +21,13 @@
21
21
  "scripts": {
22
22
  "start": "node server/index.js",
23
23
  "dev": "node --watch server/index.js",
24
+ "dev:backend": "./dev/backend.sh",
25
+ "dev:web": "./dev/web.sh",
26
+ "dev:stack": "./dev/stack.sh",
27
+ "dev:build": "./dev/build.sh",
28
+ "dev:test": "./dev/test.sh",
29
+ "flutter:run:web": "cd flutter_app && flutter run -d chrome",
30
+ "flutter:build:web": "cd flutter_app && flutter build web --output ../server/public --dart-define=NEOAGENT_BACKEND_URL=${NEOAGENT_BACKEND_URL:-}",
24
31
  "manage": "node bin/neoagent.js",
25
32
  "test": "node --test",
26
33
  "benchmark:tokens": "node scripts/benchmark-token-cost.js",
@@ -54,7 +61,7 @@
54
61
  "node-pty": "^1.0.0",
55
62
  "node-telegram-bot-api": "^0.67.0",
56
63
  "openai": "^4.85.4",
57
- "puppeteer": "^24.4.0",
64
+ "puppeteer-core": "^24.40.0",
58
65
  "puppeteer-extra": "^3.3.6",
59
66
  "puppeteer-extra-plugin-stealth": "^2.11.2",
60
67
  "socket.io": "^4.8.1",
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const configuredOrigins = (process.env.ALLOWED_ORIGINS || '')
4
+ .split(',')
5
+ .map((origin) => origin.trim())
6
+ .filter(Boolean);
7
+
8
+ function isLoopbackOrigin(origin) {
9
+ try {
10
+ const parsed = new URL(origin);
11
+ return ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname);
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function isAllowedOrigin(origin) {
18
+ if (!origin) return true;
19
+ if (configuredOrigins.includes(origin)) return true;
20
+ if (isLoopbackOrigin(origin)) return true;
21
+ return false;
22
+ }
23
+
24
+ function validateOrigin(origin, callback) {
25
+ if (isAllowedOrigin(origin)) return callback(null, true);
26
+ return callback(new Error(`Origin not allowed: ${origin || 'unknown'}`));
27
+ }
28
+
29
+ module.exports = {
30
+ configuredOrigins,
31
+ isAllowedOrigin,
32
+ isLoopbackOrigin,
33
+ validateOrigin
34
+ };
@@ -214,19 +214,6 @@ db.exec(`
214
214
 
215
215
  CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id, archived, updated_at DESC);
216
216
  CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(user_id, category, archived);
217
- CREATE TABLE IF NOT EXISTS protocols (
218
- id INTEGER PRIMARY KEY AUTOINCREMENT,
219
- user_id INTEGER NOT NULL,
220
- name TEXT UNIQUE NOT NULL,
221
- description TEXT,
222
- content TEXT NOT NULL,
223
- created_at TEXT DEFAULT (datetime('now')),
224
- updated_at TEXT DEFAULT (datetime('now')),
225
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
226
- );
227
-
228
- CREATE INDEX IF NOT EXISTS idx_protocols_user ON protocols(user_id);
229
-
230
217
  CREATE INDEX IF NOT EXISTS idx_core_memory_user ON core_memory(user_id, key);
231
218
 
232
219
  CREATE TABLE IF NOT EXISTS health_sync_runs (
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const { sanitizeError } = require('../utils/security');
4
+
5
+ function registerErrorHandler(app) {
6
+ app.use((err, req, res, next) => {
7
+ console.error('[Unhandled error]', err);
8
+ const status = err.status || err.statusCode || 500;
9
+ const message = sanitizeError(err);
10
+ if (req.path.startsWith('/api/')) {
11
+ return res.status(status).json({ error: message });
12
+ }
13
+ return res.status(status).send('Something went wrong.');
14
+ });
15
+ }
16
+
17
+ module.exports = { registerErrorHandler };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const session = require('express-session');
4
+ const SQLiteStore = require('connect-sqlite3')(session);
5
+ const helmet = require('helmet');
6
+ const cors = require('cors');
7
+ const { DATA_DIR } = require('../../runtime/paths');
8
+
9
+ function buildHelmetOptions({ secureCookies }) {
10
+ const wsConnectSrc = secureCookies ? ['wss:'] : ['ws:', 'wss:'];
11
+
12
+ return {
13
+ strictTransportSecurity: false,
14
+ crossOriginOpenerPolicy: false,
15
+ originAgentCluster: false,
16
+ contentSecurityPolicy: {
17
+ directives: {
18
+ defaultSrc: ["'self'"],
19
+ scriptSrc: [
20
+ "'self'",
21
+ "'unsafe-inline'",
22
+ "'unsafe-eval'",
23
+ 'blob:',
24
+ 'https://cdn.jsdelivr.net'
25
+ ],
26
+ scriptSrcAttr: ["'unsafe-inline'"],
27
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
28
+ imgSrc: ["'self'", 'data:', 'blob:', 'https://api.qrserver.com'],
29
+ connectSrc: [
30
+ "'self'",
31
+ 'https://fonts.googleapis.com',
32
+ 'https://fonts.gstatic.com',
33
+ ...wsConnectSrc
34
+ ],
35
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
36
+ workerSrc: ["'self'", 'blob:'],
37
+ formAction: ["'self'"],
38
+ frameAncestors: ["'self'"],
39
+ upgradeInsecureRequests: null
40
+ }
41
+ }
42
+ };
43
+ }
44
+
45
+ function createSessionMiddleware({ secureCookies }) {
46
+ return session({
47
+ store: new SQLiteStore({ db: 'sessions.db', dir: DATA_DIR }),
48
+ secret: process.env.SESSION_SECRET || 'neoagent-dev-secret-change-me',
49
+ name: 'neoagent.sid',
50
+ resave: false,
51
+ saveUninitialized: false,
52
+ cookie: {
53
+ maxAge: 7 * 24 * 60 * 60 * 1000,
54
+ httpOnly: true,
55
+ sameSite: 'lax',
56
+ secure: secureCookies
57
+ }
58
+ });
59
+ }
60
+
61
+ function applyHttpMiddleware(app, { secureCookies, sessionMiddleware, validateOrigin }) {
62
+ if (secureCookies) {
63
+ app.set('trust proxy', 1);
64
+ }
65
+
66
+ app.use(helmet(buildHelmetOptions({ secureCookies })));
67
+ app.use(
68
+ cors({
69
+ origin: validateOrigin,
70
+ credentials: true
71
+ })
72
+ );
73
+ app.use(require('express').json({ limit: '10mb' }));
74
+ app.use(require('express').urlencoded({ extended: true }));
75
+ app.use(sessionMiddleware);
76
+ }
77
+
78
+ module.exports = {
79
+ applyHttpMiddleware,
80
+ createSessionMiddleware
81
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const { setupTelnyxWebhook } = require('../routes/telnyx');
5
+ const { getVersionInfo } = require('../utils/version');
6
+
7
+ const routeRegistry = [
8
+ { basePath: null, modulePath: '../routes/auth' },
9
+ { basePath: '/api/settings', modulePath: '../routes/settings' },
10
+ { basePath: '/api/agents', modulePath: '../routes/agents' },
11
+ { basePath: '/api/messaging', modulePath: '../routes/messaging' },
12
+ { basePath: '/api/mcp', modulePath: '../routes/mcp' },
13
+ { basePath: '/api/skills', modulePath: '../routes/skills' },
14
+ { basePath: '/api/store', modulePath: '../routes/store' },
15
+ { basePath: '/api/memory', modulePath: '../routes/memory' },
16
+ { basePath: '/api/scheduler', modulePath: '../routes/scheduler' },
17
+ { basePath: '/api/browser', modulePath: '../routes/browser' },
18
+ { basePath: '/api/mobile/health', modulePath: '../routes/mobile-health' }
19
+ ];
20
+
21
+ function registerApiRoutes(app) {
22
+ for (const route of routeRegistry) {
23
+ const handler = require(route.modulePath);
24
+ if (route.basePath) {
25
+ app.use(route.basePath, handler);
26
+ } else {
27
+ app.use(handler);
28
+ }
29
+ }
30
+
31
+ setupTelnyxWebhook(app);
32
+
33
+ app.get('/api/health', requireAuth, (req, res) => {
34
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
35
+ });
36
+
37
+ app.get('/api/version', requireAuth, (req, res) => {
38
+ res.json(getVersionInfo());
39
+ });
40
+ }
41
+
42
+ module.exports = {
43
+ registerApiRoutes,
44
+ routeRegistry
45
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { Server: SocketIO } = require('socket.io');
4
+
5
+ function createSocketServer(httpServer, { validateOrigin }) {
6
+ return new SocketIO(httpServer, {
7
+ cors: {
8
+ origin: validateOrigin,
9
+ credentials: true
10
+ }
11
+ });
12
+ }
13
+
14
+ function bindSocketSessions(io, sessionMiddleware) {
15
+ io.use((socket, next) => {
16
+ sessionMiddleware(socket.request, {}, next);
17
+ });
18
+ }
19
+
20
+ module.exports = {
21
+ bindSocketSessions,
22
+ createSocketServer
23
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const express = require('express');
6
+ const { DATA_DIR } = require('../../runtime/paths');
7
+ const { requireAuth } = require('../middleware/auth');
8
+
9
+ const FLUTTER_WEB_DIR = path.join(__dirname, '..', 'public');
10
+
11
+ function registerStaticRoutes(app) {
12
+ app.use(
13
+ '/telnyx-audio',
14
+ express.static(path.join(DATA_DIR, 'telnyx-audio'), {
15
+ index: false,
16
+ setHeaders: (res, filePath) => {
17
+ if (!filePath.match(/\.(mp3|wav|ogg|aac|m4a)$/i)) {
18
+ res.status(403).end();
19
+ }
20
+ }
21
+ })
22
+ );
23
+
24
+ app.use(
25
+ '/screenshots',
26
+ requireAuth,
27
+ express.static(path.join(DATA_DIR, 'screenshots'))
28
+ );
29
+
30
+ app.use(express.static(FLUTTER_WEB_DIR, { index: false }));
31
+ app.get(/^\/(?!api|screenshots|telnyx-audio).*/, serveFlutterApp);
32
+ }
33
+
34
+ function serveFlutterApp(req, res) {
35
+ const entry = path.join(FLUTTER_WEB_DIR, 'index.html');
36
+ if (!fs.existsSync(entry)) {
37
+ return res
38
+ .status(503)
39
+ .send(
40
+ 'Flutter web build not found. Run "npm run flutter:build:web" to generate the bundled client.'
41
+ );
42
+ }
43
+ return res.sendFile(entry);
44
+ }
45
+
46
+ module.exports = {
47
+ FLUTTER_WEB_DIR,
48
+ registerStaticRoutes,
49
+ serveFlutterApp
50
+ };