neoagent 2.0.1 → 2.0.2

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 (38) hide show
  1. package/.env.example +8 -0
  2. package/docs/configuration.md +3 -0
  3. package/package.json +1 -1
  4. package/server/db/database.js +75 -0
  5. package/server/http/middleware.js +26 -2
  6. package/server/http/routes.js +1 -0
  7. package/server/index.js +97 -6
  8. package/server/public/.last_build_id +1 -1
  9. package/server/public/assets/NOTICES +24298 -26578
  10. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  11. package/server/public/assets/shaders/ink_sparkle.frag +1 -0
  12. package/server/public/assets/shaders/stretch_effect.frag +64 -0
  13. package/server/public/assets/web/icons/Icon-192.png +0 -0
  14. package/server/public/canvaskit/canvaskit.js +91 -90
  15. package/server/public/canvaskit/canvaskit.js.symbols +11577 -11578
  16. package/server/public/canvaskit/canvaskit.wasm +0 -0
  17. package/server/public/canvaskit/chromium/canvaskit.js +92 -91
  18. package/server/public/canvaskit/chromium/canvaskit.js.symbols +10382 -10395
  19. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  20. package/server/public/canvaskit/skwasm.js +95 -89
  21. package/server/public/canvaskit/skwasm.js.symbols +12814 -12146
  22. package/server/public/canvaskit/skwasm.wasm +0 -0
  23. package/server/public/canvaskit/skwasm_heavy.js +95 -89
  24. package/server/public/canvaskit/skwasm_heavy.js.symbols +14435 -13747
  25. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  26. package/server/public/canvaskit/wimp.js +136 -0
  27. package/server/public/canvaskit/wimp.js.symbols +11313 -0
  28. package/server/public/canvaskit/wimp.wasm +0 -0
  29. package/server/public/favicon.svg +12 -0
  30. package/server/public/flutter.js +3 -4
  31. package/server/public/flutter_bootstrap.js +5 -6
  32. package/server/public/flutter_service_worker.js +22 -199
  33. package/server/public/index.html +1 -0
  34. package/server/public/main.dart.js +73857 -65543
  35. package/server/routes/recordings.js +113 -0
  36. package/server/services/manager.js +9 -0
  37. package/server/services/recordings/deepgram.js +53 -0
  38. package/server/services/recordings/manager.js +715 -0
package/.env.example CHANGED
@@ -31,3 +31,11 @@ GOOGLE_AI_KEY=your-google-ai-key-here
31
31
  # • Native web_search tool (search the web without driving the browser)
32
32
  # Get your key at: https://api.search.brave.com/
33
33
  BRAVE_SEARCH_API_KEY=your-brave-search-api-key-here
34
+
35
+ # Deepgram API key — used for:
36
+ # • Recordings transcription (Deepgram Nova-3 multilingual via /api/recordings)
37
+ # Without this key: recordings can still be captured and stored, but transcription will fail until retried with a valid key.
38
+ # Get your key at: https://console.deepgram.com/
39
+ DEEPGRAM_API_KEY=your-deepgram-api-key-here
40
+ DEEPGRAM_MODEL=nova-3
41
+ DEEPGRAM_LANGUAGE=multi
@@ -26,6 +26,9 @@ At least one API key is required. The active provider and model are configured i
26
26
  | `XAI_API_KEY` | Grok (xAI) |
27
27
  | `GOOGLE_AI_KEY` | Gemini (Google) |
28
28
  | `BRAVE_SEARCH_API_KEY` | Brave Search API for the native `web_search` tool |
29
+ | `DEEPGRAM_API_KEY` | Recordings transcription with Deepgram Nova-3 multilingual |
30
+ | `DEEPGRAM_MODEL` | Deepgram speech model override (defaults to `nova-3`) |
31
+ | `DEEPGRAM_LANGUAGE` | Deepgram language override (defaults to `multi`) |
29
32
  | `OLLAMA_URL` | Local Ollama (`http://localhost:11434`) |
30
33
 
31
34
  ### Messaging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -256,6 +256,78 @@ db.exec(`
256
256
  CREATE INDEX IF NOT EXISTS idx_health_sync_runs_user ON health_sync_runs(user_id, created_at DESC);
257
257
  CREATE INDEX IF NOT EXISTS idx_health_metric_samples_user ON health_metric_samples(user_id, metric_type, updated_at DESC);
258
258
  CREATE INDEX IF NOT EXISTS idx_health_metric_samples_time ON health_metric_samples(user_id, start_time DESC, end_time DESC);
259
+
260
+ CREATE TABLE IF NOT EXISTS recording_sessions (
261
+ id TEXT PRIMARY KEY,
262
+ user_id INTEGER NOT NULL,
263
+ title TEXT,
264
+ platform TEXT DEFAULT 'unknown',
265
+ status TEXT DEFAULT 'recording',
266
+ transcript_text TEXT,
267
+ transcript_language TEXT,
268
+ transcript_model TEXT,
269
+ started_at TEXT DEFAULT (datetime('now')),
270
+ ended_at TEXT,
271
+ duration_ms INTEGER DEFAULT 0,
272
+ last_error TEXT,
273
+ metadata_json TEXT DEFAULT '{}',
274
+ created_at TEXT DEFAULT (datetime('now')),
275
+ updated_at TEXT DEFAULT (datetime('now')),
276
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
277
+ );
278
+
279
+ CREATE TABLE IF NOT EXISTS recording_sources (
280
+ id TEXT PRIMARY KEY,
281
+ session_id TEXT NOT NULL,
282
+ source_key TEXT NOT NULL,
283
+ source_kind TEXT NOT NULL,
284
+ media_kind TEXT NOT NULL,
285
+ mime_type TEXT,
286
+ status TEXT DEFAULT 'recording',
287
+ chunk_count INTEGER DEFAULT 0,
288
+ bytes_received INTEGER DEFAULT 0,
289
+ duration_ms INTEGER DEFAULT 0,
290
+ metadata_json TEXT DEFAULT '{}',
291
+ created_at TEXT DEFAULT (datetime('now')),
292
+ updated_at TEXT DEFAULT (datetime('now')),
293
+ FOREIGN KEY (session_id) REFERENCES recording_sessions(id) ON DELETE CASCADE,
294
+ UNIQUE(session_id, source_key)
295
+ );
296
+
297
+ CREATE TABLE IF NOT EXISTS recording_chunks (
298
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
299
+ source_id TEXT NOT NULL,
300
+ sequence_index INTEGER NOT NULL,
301
+ start_ms INTEGER DEFAULT 0,
302
+ end_ms INTEGER DEFAULT 0,
303
+ byte_count INTEGER DEFAULT 0,
304
+ mime_type TEXT,
305
+ file_path TEXT NOT NULL,
306
+ created_at TEXT DEFAULT (datetime('now')),
307
+ FOREIGN KEY (source_id) REFERENCES recording_sources(id) ON DELETE CASCADE,
308
+ UNIQUE(source_id, sequence_index)
309
+ );
310
+
311
+ CREATE TABLE IF NOT EXISTS recording_transcript_segments (
312
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
313
+ session_id TEXT NOT NULL,
314
+ source_id TEXT,
315
+ source_key TEXT,
316
+ speaker TEXT,
317
+ text TEXT NOT NULL,
318
+ start_ms INTEGER DEFAULT 0,
319
+ end_ms INTEGER DEFAULT 0,
320
+ confidence REAL,
321
+ words_json TEXT,
322
+ created_at TEXT DEFAULT (datetime('now')),
323
+ FOREIGN KEY (session_id) REFERENCES recording_sessions(id) ON DELETE CASCADE,
324
+ FOREIGN KEY (source_id) REFERENCES recording_sources(id) ON DELETE CASCADE
325
+ );
326
+
327
+ CREATE INDEX IF NOT EXISTS idx_recording_sessions_user ON recording_sessions(user_id, created_at DESC);
328
+ CREATE INDEX IF NOT EXISTS idx_recording_sources_session ON recording_sources(session_id, source_key);
329
+ CREATE INDEX IF NOT EXISTS idx_recording_chunks_source ON recording_chunks(source_id, sequence_index);
330
+ CREATE INDEX IF NOT EXISTS idx_recording_segments_session ON recording_transcript_segments(session_id, start_ms, created_at);
259
331
  `);
260
332
 
261
333
  try {
@@ -320,6 +392,9 @@ for (const col of [
320
392
  "ALTER TABLE conversations ADD COLUMN summary TEXT",
321
393
  "ALTER TABLE conversations ADD COLUMN summary_message_count INTEGER DEFAULT 0",
322
394
  "ALTER TABLE conversations ADD COLUMN last_summary TEXT",
395
+ "ALTER TABLE recording_sessions ADD COLUMN transcript_language TEXT",
396
+ "ALTER TABLE recording_sessions ADD COLUMN transcript_model TEXT",
397
+ "ALTER TABLE recording_sessions ADD COLUMN duration_ms INTEGER DEFAULT 0",
323
398
  ]) {
324
399
  try { db.exec(col); } catch { /* column already exists */ }
325
400
  }
@@ -61,6 +61,14 @@ function createSessionMiddleware({ secureCookies }) {
61
61
  }
62
62
 
63
63
  function applyHttpMiddleware(app, { secureCookies, sessionMiddleware, validateOrigin }) {
64
+ const rawRecordingChunkBody = require('express').raw({ limit: '50mb', type: '*/*' });
65
+ const jsonBody = require('express').json({ limit: '10mb' });
66
+ const urlencodedBody = require('express').urlencoded({ extended: true });
67
+ const isRecordingChunkPath = (value = '') => {
68
+ const path = `${value}`.split('?')[0];
69
+ return /^\/api\/recordings\/[^/]+\/chunks$/i.test(path);
70
+ };
71
+
64
72
  if (secureCookies) {
65
73
  app.set('trust proxy', 1);
66
74
  }
@@ -72,8 +80,24 @@ function applyHttpMiddleware(app, { secureCookies, sessionMiddleware, validateOr
72
80
  credentials: true
73
81
  })
74
82
  );
75
- app.use(require('express').json({ limit: '10mb' }));
76
- app.use(require('express').urlencoded({ extended: true }));
83
+ app.use((req, res, next) => {
84
+ if (isRecordingChunkPath(req.originalUrl || req.url || req.path)) {
85
+ return rawRecordingChunkBody(req, res, next);
86
+ }
87
+ return next();
88
+ });
89
+ app.use((req, res, next) => {
90
+ if (isRecordingChunkPath(req.originalUrl || req.url || req.path)) {
91
+ return next();
92
+ }
93
+ return jsonBody(req, res, next);
94
+ });
95
+ app.use((req, res, next) => {
96
+ if (isRecordingChunkPath(req.originalUrl || req.url || req.path)) {
97
+ return next();
98
+ }
99
+ return urlencodedBody(req, res, next);
100
+ });
77
101
  app.use(sessionMiddleware);
78
102
  }
79
103
 
@@ -15,6 +15,7 @@ const routeRegistry = [
15
15
  { basePath: '/api/memory', modulePath: '../routes/memory' },
16
16
  { basePath: '/api/scheduler', modulePath: '../routes/scheduler' },
17
17
  { basePath: '/api/browser', modulePath: '../routes/browser' },
18
+ { basePath: '/api/recordings', modulePath: '../routes/recordings' },
18
19
  { basePath: '/api/mobile/health', modulePath: '../routes/mobile-health' }
19
20
  ];
20
21
 
package/server/index.js CHANGED
@@ -39,6 +39,7 @@ const app = express();
39
39
  const httpServer = createServer(app);
40
40
  const io = createSocketServer(httpServer, { validateOrigin });
41
41
  const sessionMiddleware = createSessionMiddleware({ secureCookies: SECURE_COOKIES });
42
+ const activeSockets = new Set();
42
43
 
43
44
  setupConsoleInterceptor(io);
44
45
  applyHttpMiddleware(app, {
@@ -53,6 +54,98 @@ registerErrorHandler(app);
53
54
 
54
55
  let shuttingDown = false;
55
56
 
57
+ httpServer.on('connection', (socket) => {
58
+ activeSockets.add(socket);
59
+ socket.on('close', () => activeSockets.delete(socket));
60
+ });
61
+
62
+ function closeSocketServer(ioServer, timeoutMs = 5000) {
63
+ return new Promise((resolve) => {
64
+ if (!ioServer) {
65
+ resolve();
66
+ return;
67
+ }
68
+
69
+ let finished = false;
70
+
71
+ const finish = () => {
72
+ if (finished) return;
73
+ finished = true;
74
+ clearTimeout(forceTimer);
75
+ resolve();
76
+ };
77
+
78
+ const forceTimer = setTimeout(() => {
79
+ try {
80
+ ioServer.disconnectSockets(true);
81
+ } catch (err) {
82
+ console.error('[Shutdown] Socket.IO force disconnect error:', err.message);
83
+ }
84
+ console.warn('[Shutdown] Socket.IO close timed out; forcing client disconnects.');
85
+ finish();
86
+ }, timeoutMs);
87
+
88
+ forceTimer.unref?.();
89
+
90
+ try {
91
+ ioServer.disconnectSockets(true);
92
+ ioServer.close(() => finish());
93
+ } catch (err) {
94
+ console.error('[Shutdown] Socket.IO close error:', err.message);
95
+ finish();
96
+ }
97
+ });
98
+ }
99
+
100
+ function closeHttpServer(server, sockets, timeoutMs = 5000) {
101
+ return new Promise((resolve) => {
102
+ let finished = false;
103
+
104
+ const finish = () => {
105
+ if (finished) return;
106
+ finished = true;
107
+ clearTimeout(forceTimer);
108
+ resolve();
109
+ };
110
+
111
+ const forceTimer = setTimeout(() => {
112
+ if (typeof server.closeAllConnections === 'function') {
113
+ server.closeAllConnections();
114
+ }
115
+
116
+ for (const socket of sockets) {
117
+ socket.destroy();
118
+ }
119
+
120
+ if (sockets.size > 0) {
121
+ console.warn(`[Shutdown] Forced ${sockets.size} open socket(s) closed.`);
122
+ }
123
+
124
+ finish();
125
+ }, timeoutMs);
126
+
127
+ forceTimer.unref?.();
128
+
129
+ try {
130
+ server.close((err) => {
131
+ if (err && err.code !== 'ERR_SERVER_NOT_RUNNING') {
132
+ console.error('[Shutdown] HTTP server close error:', err.message);
133
+ }
134
+ finish();
135
+ });
136
+
137
+ if (typeof server.closeIdleConnections === 'function') {
138
+ server.closeIdleConnections();
139
+ }
140
+ } catch (err) {
141
+ if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
142
+ console.error('[Shutdown] HTTP server close threw:', err.message);
143
+ }
144
+ finish();
145
+ }
146
+ });
147
+ }
148
+
56
149
  async function shutdown() {
57
150
  if (shuttingDown) return;
58
151
  shuttingDown = true;
@@ -60,12 +153,10 @@ async function shutdown() {
60
153
  console.log('Shutting down...');
61
154
 
62
155
  await stopServices(app);
63
-
64
- try {
65
- await new Promise((resolve) => httpServer.close(resolve));
66
- } catch {
67
- // ignore close races during shutdown
68
- }
156
+ await Promise.allSettled([
157
+ closeSocketServer(io),
158
+ closeHttpServer(httpServer, activeSockets),
159
+ ]);
69
160
 
70
161
  db.close();
71
162
  process.exit(0);
@@ -1 +1 @@
1
- fe64fce67ac7f05d4575c0fd7302c732
1
+ e38b9444952230adccfb989891baa8ec