neoagent 2.3.1-beta.0 → 2.3.1-beta.3

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/README.md +2 -0
  2. package/docs/automation.md +1 -1
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +6 -1
  5. package/docs/integrations.md +4 -0
  6. package/package.json +2 -1
  7. package/server/db/database.js +57 -0
  8. package/server/http/routes/screenHistory.js +46 -0
  9. package/server/http/routes/triggers.js +81 -0
  10. package/server/http/routes.js +3 -1
  11. package/server/public/assets/NOTICES +61 -0
  12. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  13. package/server/public/flutter_bootstrap.js +1 -1
  14. package/server/public/main.dart.js +46519 -46294
  15. package/server/routes/integrations.js +11 -8
  16. package/server/services/ai/engine.js +9 -0
  17. package/server/services/ai/systemPrompt.js +1 -1
  18. package/server/services/ai/tools.js +150 -11
  19. package/server/services/browser/controller.js +47 -3
  20. package/server/services/desktop/screenRecorder.js +126 -0
  21. package/server/services/integrations/env.js +5 -0
  22. package/server/services/integrations/registry.js +4 -0
  23. package/server/services/integrations/spotify/provider.js +487 -0
  24. package/server/services/integrations/weather/provider.js +559 -0
  25. package/server/services/integrations/whatsapp/provider.js +6 -2
  26. package/server/services/manager.js +22 -0
  27. package/server/services/messaging/manager.js +29 -7
  28. package/server/services/skills/base_catalog.js +33 -0
  29. package/server/services/tasks/adapters/index.js +2 -0
  30. package/server/services/tasks/adapters/manual.js +12 -0
  31. package/server/services/tasks/adapters/schedule.js +33 -5
  32. package/server/services/tasks/adapters/weather_event.js +84 -0
  33. package/server/services/tasks/integration_runtime.js +85 -0
  34. package/server/services/tasks/runtime.js +2 -2
  35. package/server/services/voice/agentBridge.js +20 -4
  36. package/server/services/voice/message.js +3 -0
  37. package/server/services/voice/runtimeManager.js +136 -1
  38. package/server/services/widgets/service.js +49 -4
@@ -159,6 +159,9 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
159
159
  if (!session) {
160
160
  return res.status(404).send('Connection session not found.');
161
161
  }
162
+ const provider = manager.getProvider(req.params.provider);
163
+ const providerLabel = provider?.label || req.params.provider;
164
+ const appLabel = provider?.getApp?.(session.appKey)?.label || session.appKey || 'account';
162
165
  const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
163
166
  const statusUrl = `/api/integrations/${encodeURIComponent(req.params.provider)}/connect/${encodeURIComponent(req.params.sessionId)}/status?agentId=${encodeURIComponent(agentId)}`;
164
167
  res.send(`
@@ -166,7 +169,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
166
169
  <head>
167
170
  <meta charset="utf-8" />
168
171
  <meta name="viewport" content="width=device-width, initial-scale=1" />
169
- <title>Connect ${escapeHtml(req.params.provider)}</title>
172
+ <title>Connect ${escapeHtml(providerLabel)}</title>
170
173
  <style>
171
174
  body { font-family: ui-sans-serif, system-ui, sans-serif; background: #0b1220; color: #f8fafc; margin: 0; padding: 24px; }
172
175
  .card { max-width: 560px; margin: 0 auto; background: #111827; border: 1px solid #1f2937; border-radius: 20px; padding: 24px; }
@@ -179,11 +182,11 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
179
182
  <body>
180
183
  <div class="card">
181
184
  <div class="pill">Official integration</div>
182
- <h1>Connect WhatsApp</h1>
183
- <p class="muted">This link is isolated from the separate messaging-platform WhatsApp bridge. Scan the QR code with your personal WhatsApp account to finish linking.</p>
185
+ <h1>Connect ${escapeHtml(providerLabel)}</h1>
186
+ <p class="muted">Complete connection for ${escapeHtml(appLabel)}. This window closes automatically when linking is finished.</p>
184
187
  <div id="status" class="muted">Starting connection…</div>
185
- <img id="qr" alt="WhatsApp QR code" style="display:none;" />
186
- <p class="muted">When the link finishes, this window will close automatically. If it does not, you can close it manually.</p>
188
+ <img id="qr" alt="Integration QR code" style="display:none;" />
189
+ <p class="muted">If this flow needs QR scan approval, the code will appear below.</p>
187
190
  </div>
188
191
  <script>
189
192
  const statusEl = document.getElementById('status');
@@ -207,12 +210,12 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
207
210
  const data = await response.json();
208
211
  const status = String(data.status || 'connecting');
209
212
  if (status === 'awaiting_qr' && data.qr) {
210
- statusEl.textContent = 'Scan this QR code with WhatsApp on your phone.';
213
+ statusEl.textContent = 'Scan this QR code to continue linking.';
211
214
  qrEl.src = 'https://api.qrserver.com/v1/create-qr-code/?data=' + encodeURIComponent(data.qr) + '&size=320x320';
212
215
  qrEl.style.display = 'block';
213
216
  } else if (status === 'connected') {
214
217
  qrEl.style.display = 'none';
215
- statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your WhatsApp account') + '. Closing…';
218
+ statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your account') + '. Closing…';
216
219
  notifyOpener({
217
220
  type: 'integration_oauth_success',
218
221
  provider,
@@ -233,7 +236,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
233
236
  });
234
237
  return;
235
238
  } else {
236
- statusEl.textContent = 'Waiting for WhatsApp to generate a QR code…';
239
+ statusEl.textContent = 'Waiting for the integration to finish linking…';
237
240
  }
238
241
  setTimeout(refresh, 1500);
239
242
  }
@@ -686,6 +686,7 @@ class AgentEngine {
686
686
  content,
687
687
  kind,
688
688
  expectsReply = false,
689
+ deferFollowUp = false,
689
690
  } = {}) {
690
691
  const runMeta = this.getRunMeta(runId);
691
692
  if (!runMeta || runMeta.aborted) {
@@ -715,6 +716,9 @@ class AgentEngine {
715
716
  kind: normalizedKind,
716
717
  expectsReply,
717
718
  });
719
+ if (deferFollowUp === true) {
720
+ metadata.defer_follow_up = true;
721
+ }
718
722
  const createdAt = new Date().toISOString();
719
723
 
720
724
  if (triggerSource === 'messaging') {
@@ -739,6 +743,7 @@ class AgentEngine {
739
743
  content: normalizedContent,
740
744
  kind: normalizedKind,
741
745
  expectsReply,
746
+ deferFollowUp,
742
747
  });
743
748
  } else {
744
749
  db.prepare(
@@ -758,6 +763,7 @@ class AgentEngine {
758
763
  content: normalizedContent,
759
764
  kind: normalizedKind,
760
765
  expectsReply: expectsReply === true,
766
+ deferFollowUp: deferFollowUp === true,
761
767
  createdAt,
762
768
  });
763
769
  runMeta.lastInterimMessage = normalizedContent;
@@ -767,6 +773,7 @@ class AgentEngine {
767
773
  content: normalizedContent,
768
774
  kind: normalizedKind,
769
775
  expectsReply: expectsReply === true,
776
+ deferFollowUp: deferFollowUp === true,
770
777
  triggerSource,
771
778
  platform: triggerSource === 'messaging' ? platform : 'web',
772
779
  });
@@ -783,6 +790,7 @@ class AgentEngine {
783
790
  latestInterim: {
784
791
  kind: normalizedKind,
785
792
  expectsReply: expectsReply === true,
793
+ deferFollowUp: deferFollowUp === true,
786
794
  content: normalizedContent,
787
795
  createdAt,
788
796
  },
@@ -795,6 +803,7 @@ class AgentEngine {
795
803
  sent: true,
796
804
  kind: normalizedKind,
797
805
  expectsReply: expectsReply === true,
806
+ deferFollowUp: deferFollowUp === true,
798
807
  content: normalizedContent,
799
808
  terminal: terminalInterim,
800
809
  };
@@ -138,7 +138,7 @@ When drafting on behalf of the user, match their likely voice from available con
138
138
  If the user approves a previously shown draft, send that draft rather than silently rewriting it.
139
139
 
140
140
  TASKS
141
- Use one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
141
+ Use manual triggers for run-on-demand tasks, one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. When calling task tools, prefer one unified trigger section: trigger={ type, config }. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
142
142
  Do not create vague tasks like "check this" when the future run would not know what "this" means. Resolve references into names, links, file paths, IDs, dates, and success criteria before saving the task.
143
143
  For notification tasks, distinguish between notifying the user in their current messaging channel, emailing the user, and contacting someone else. Default reminders should notify the user through the active messaging channel unless the user explicitly asks for email, phone, or a third party.
144
144
  When creating or updating a task, include whether it should notify every time, only on change, only on errors, or only when a condition is met. If unspecified, choose the least noisy useful behavior and say what you chose.
@@ -91,6 +91,103 @@ function compactToolDefinition(tool, options = {}) {
91
91
  return compact;
92
92
  }
93
93
 
94
+ function normalizeScheduleTriggerConfig(inputConfig = {}) {
95
+ if (!inputConfig || typeof inputConfig !== 'object' || Array.isArray(inputConfig)) {
96
+ return inputConfig;
97
+ }
98
+
99
+ const normalized = { ...inputConfig };
100
+ const schedule = (normalized.schedule && typeof normalized.schedule === 'object' && !Array.isArray(normalized.schedule))
101
+ ? normalized.schedule
102
+ : null;
103
+
104
+ const modeCandidate = String(
105
+ normalized.mode
106
+ || normalized.type
107
+ || normalized.scheduleType
108
+ || normalized.schedule_type
109
+ || (normalized.oneTime || normalized.one_time ? 'one_time' : '')
110
+ || ''
111
+ ).trim().toLowerCase();
112
+ if (modeCandidate === 'once') normalized.mode = 'one_time';
113
+ else if (modeCandidate === 'one_time' || modeCandidate === 'recurring') normalized.mode = modeCandidate;
114
+
115
+ const cronCandidate = normalized.cronExpression
116
+ || normalized.cron_expression
117
+ || normalized.cron
118
+ || schedule?.cronExpression
119
+ || schedule?.cron_expression
120
+ || schedule?.cron;
121
+ if (cronCandidate != null) {
122
+ normalized.cronExpression = String(cronCandidate).trim();
123
+ }
124
+
125
+ const runAtCandidate = normalized.runAt
126
+ || normalized.run_at
127
+ || normalized.at
128
+ || normalized.when
129
+ || schedule?.runAt
130
+ || schedule?.run_at
131
+ || schedule?.at
132
+ || schedule?.when;
133
+ if (runAtCandidate != null) {
134
+ normalized.runAt = String(runAtCandidate).trim();
135
+ }
136
+
137
+ if (!normalized.mode) {
138
+ normalized.mode = normalized.runAt ? 'one_time' : 'recurring';
139
+ }
140
+
141
+ delete normalized.schedule;
142
+ return normalized;
143
+ }
144
+
145
+ function normalizeTaskTriggerInput(triggerType, triggerConfig) {
146
+ if (String(triggerType || '').trim() !== 'schedule') {
147
+ return triggerConfig;
148
+ }
149
+ return normalizeScheduleTriggerConfig(triggerConfig || {});
150
+ }
151
+
152
+ function resolveTaskTriggerArgs(args = {}, fallbackTriggerType = null) {
153
+ let triggerType = args.trigger_type !== undefined
154
+ ? String(args.trigger_type || '').trim()
155
+ : String(fallbackTriggerType || '').trim();
156
+ let triggerConfig = args.trigger_config;
157
+ let hasType = args.trigger_type !== undefined;
158
+ let hasConfig = args.trigger_config !== undefined;
159
+
160
+ if (args.trigger && typeof args.trigger === 'object' && !Array.isArray(args.trigger)) {
161
+ const trigger = args.trigger;
162
+ const unifiedType = trigger.type ?? trigger.triggerType ?? trigger.trigger_type;
163
+ const unifiedHasType = unifiedType !== undefined && unifiedType !== null && String(unifiedType).trim().length > 0;
164
+ if (unifiedHasType) {
165
+ triggerType = String(unifiedType).trim();
166
+ hasType = true;
167
+ }
168
+
169
+ const hasUnifiedConfig = Object.prototype.hasOwnProperty.call(trigger, 'config')
170
+ || Object.prototype.hasOwnProperty.call(trigger, 'triggerConfig')
171
+ || Object.prototype.hasOwnProperty.call(trigger, 'trigger_config');
172
+ if (hasUnifiedConfig) {
173
+ triggerConfig = trigger.config ?? trigger.triggerConfig ?? trigger.trigger_config;
174
+ hasConfig = true;
175
+ }
176
+ }
177
+
178
+ const normalizedType = String(triggerType || '').trim() || null;
179
+ const normalizedConfig = hasConfig
180
+ ? normalizeTaskTriggerInput(normalizedType || fallbackTriggerType || 'schedule', triggerConfig)
181
+ : undefined;
182
+
183
+ return {
184
+ triggerType: normalizedType,
185
+ triggerConfig: normalizedConfig,
186
+ hasType,
187
+ hasConfig,
188
+ };
189
+ }
190
+
94
191
  function isProactiveTrigger(triggerSource) {
95
192
  return triggerSource === 'schedule' || triggerSource === 'tasks';
96
193
  }
@@ -876,15 +973,16 @@ function getAvailableTools(app, options = {}) {
876
973
  type: 'object',
877
974
  properties: {
878
975
  name: { type: 'string', description: 'Short descriptive name for the task.' },
879
- trigger_type: { type: 'string', description: 'Trigger type such as schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, or whatsapp_personal_message_received.' },
880
- trigger_config: { type: 'object', description: 'Trigger-specific configuration object.' },
976
+ trigger: { type: 'object', description: 'Unified trigger object. Prefer { type: "manual" | "schedule" | integration_trigger_type, config: {...} }.' },
977
+ trigger_type: { type: 'string', description: 'Trigger type such as manual, schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, weather_event, or whatsapp_personal_message_received.' },
978
+ trigger_config: { type: 'object', description: 'Trigger-specific configuration object. For schedule triggers prefer { mode: "recurring", cronExpression: "m h dom mon dow" } or { mode: "one_time", runAt: ISO datetime }. 5-field cron only (seconds unsupported).' },
881
979
  prompt: { type: 'string', description: 'The instructions the agent will run when the trigger fires.' },
882
980
  enabled: { type: 'boolean', description: 'Whether to activate immediately.' },
883
981
  model: { type: 'string', description: 'Optional model override.' },
884
982
  call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this task fires.' },
885
983
  call_greeting: { type: 'string', description: 'Optional spoken greeting hint for make_call.' }
886
984
  },
887
- required: ['name', 'trigger_type', 'trigger_config', 'prompt']
985
+ required: ['name', 'prompt']
888
986
  }
889
987
  },
890
988
  {
@@ -911,8 +1009,9 @@ function getAvailableTools(app, options = {}) {
911
1009
  properties: {
912
1010
  task_id: { type: 'number', description: 'The numeric ID of the task to update.' },
913
1011
  name: { type: 'string', description: 'New name for the task.' },
914
- trigger_type: { type: 'string', description: 'Updated trigger type.' },
915
- trigger_config: { type: 'object', description: 'Updated trigger-specific configuration.' },
1012
+ trigger: { type: 'object', description: 'Unified trigger object. Use { type, config } to update trigger in one section.' },
1013
+ trigger_type: { type: 'string', description: 'Updated trigger type, e.g. manual, schedule, or integration trigger type.' },
1014
+ trigger_config: { type: 'object', description: 'Updated trigger-specific configuration. For schedule triggers use mode+cronExpression (recurring) or mode+runAt (one_time).' },
916
1015
  prompt: { type: 'string', description: 'Updated task prompt.' },
917
1016
  enabled: { type: 'boolean', description: 'Enable or disable the task.' },
918
1017
  model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override.' },
@@ -1053,6 +1152,17 @@ function getAvailableTools(app, options = {}) {
1053
1152
  required: ['image_path']
1054
1153
  }
1055
1154
  },
1155
+ {
1156
+ name: 'ocr_extract',
1157
+ description: 'Extract raw text from an image locally using Tesseract OCR. This is faster and completely offline compared to analyze_image.',
1158
+ parameters: {
1159
+ type: 'object',
1160
+ properties: {
1161
+ image_path: { type: 'string', description: 'Absolute path to the image file' }
1162
+ },
1163
+ required: ['image_path']
1164
+ }
1165
+ },
1056
1166
  {
1057
1167
  name: 'read_health_data',
1058
1168
  description: 'Read the user\'s synced mobile health data. Omit metric_type for a summary of all available metrics. With metric_type, returns an aggregate summary (total, avg, min, max over all stored data) plus the most recent individual records. Always report the summary figures — avoid listing every raw record.',
@@ -1123,7 +1233,8 @@ function getAvailableTools(app, options = {}) {
1123
1233
  properties: {
1124
1234
  content: { type: 'string', description: 'Natural assistant message derived from the current task state.' },
1125
1235
  kind: { type: 'string', enum: Array.from(INTERIM_KINDS), description: 'ack, progress, question, or blocker' },
1126
- expects_reply: { type: 'boolean', description: 'Set true only when the current run should pause for the user to answer.' }
1236
+ expects_reply: { type: 'boolean', description: 'Set true only when the current run should pause for the user to answer.' },
1237
+ defer_follow_up: { type: 'boolean', description: 'Set true when you choose to deliver the final result later via the user\'s last connected chat target.' }
1127
1238
  },
1128
1239
  required: ['content', 'kind']
1129
1240
  }
@@ -1847,6 +1958,7 @@ async function executeTool(toolName, args, context, engine) {
1847
1958
  }
1848
1959
  const interimContent = typeof args.content === 'string' ? args.content : '';
1849
1960
  const expectsReply = args.expects_reply === true;
1961
+ const deferFollowUp = args.defer_follow_up === true;
1850
1962
  return engine.publishInterimUpdate({
1851
1963
  userId,
1852
1964
  runId,
@@ -1858,6 +1970,7 @@ async function executeTool(toolName, args, context, engine) {
1858
1970
  content: interimContent,
1859
1971
  kind: normalizeInterimKind(args.kind),
1860
1972
  expectsReply,
1973
+ deferFollowUp,
1861
1974
  });
1862
1975
  }
1863
1976
 
@@ -2205,10 +2318,21 @@ async function executeTool(toolName, args, context, engine) {
2205
2318
  const s = taskRuntime();
2206
2319
  if (!s) return { error: 'Task runtime not available' };
2207
2320
  try {
2321
+ const resolvedTrigger = resolveTaskTriggerArgs(args, 'schedule');
2322
+ if (!resolvedTrigger.hasType || !resolvedTrigger.triggerType) {
2323
+ return { error: 'Task trigger type is required (use trigger.type or trigger_type).' };
2324
+ }
2325
+ const normalizedTriggerType = String(resolvedTrigger.triggerType || '').trim();
2326
+ const triggerConfig = (!resolvedTrigger.hasConfig || resolvedTrigger.triggerConfig === undefined)
2327
+ ? (normalizedTriggerType === 'manual' ? {} : undefined)
2328
+ : resolvedTrigger.triggerConfig;
2329
+ if (triggerConfig === undefined) {
2330
+ return { error: 'Task trigger config is required (use trigger.config or trigger_config).' };
2331
+ }
2208
2332
  const task = await s.createTask(userId, {
2209
2333
  name: args.name,
2210
- triggerType: args.trigger_type,
2211
- triggerConfig: args.trigger_config,
2334
+ triggerType: normalizedTriggerType,
2335
+ triggerConfig,
2212
2336
  prompt: args.prompt,
2213
2337
  enabled: args.enabled !== false,
2214
2338
  model: args.model || null,
@@ -2246,12 +2370,13 @@ async function executeTool(toolName, args, context, engine) {
2246
2370
  const s = taskRuntime();
2247
2371
  if (!s) return { error: 'Task runtime not available' };
2248
2372
  try {
2249
- const existing = db.prepare('SELECT agent_id FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
2373
+ const existing = db.prepare('SELECT agent_id, trigger_type FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
2250
2374
  if (!existing || existing.agent_id !== agentId) return { error: 'Task not found for this agent.' };
2251
2375
  const updates = {};
2376
+ const resolvedTrigger = resolveTaskTriggerArgs(args, existing.trigger_type || 'schedule');
2252
2377
  if (args.name !== undefined) updates.name = args.name;
2253
- if (args.trigger_type !== undefined) updates.triggerType = args.trigger_type;
2254
- if (args.trigger_config !== undefined) updates.triggerConfig = args.trigger_config;
2378
+ if (resolvedTrigger.hasType && resolvedTrigger.triggerType) updates.triggerType = resolvedTrigger.triggerType;
2379
+ if (resolvedTrigger.hasConfig && resolvedTrigger.triggerConfig !== undefined) updates.triggerConfig = resolvedTrigger.triggerConfig;
2255
2380
  if (args.prompt !== undefined) updates.prompt = args.prompt;
2256
2381
  if (args.enabled !== undefined) updates.enabled = args.enabled;
2257
2382
  if (args.model !== undefined) updates.model = args.model || null;
@@ -2470,6 +2595,20 @@ async function executeTool(toolName, args, context, engine) {
2470
2595
  }
2471
2596
  }
2472
2597
 
2598
+ case 'ocr_extract': {
2599
+ try {
2600
+ const fs = require('fs');
2601
+ if (!fs.existsSync(args.image_path)) {
2602
+ return { error: 'File not found: ' + args.image_path };
2603
+ }
2604
+ const Tesseract = require('tesseract.js');
2605
+ const result = await Tesseract.recognize(args.image_path, 'eng');
2606
+ return { text: result.data.text, confidence: result.data.confidence };
2607
+ } catch (err) {
2608
+ return { error: err.message };
2609
+ }
2610
+ }
2611
+
2473
2612
  case 'spawn_subagent': {
2474
2613
  try {
2475
2614
  const task = args.context ? `${args.task}\n\nContext: ${args.context}` : args.task;
@@ -6,11 +6,11 @@ const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
6
6
  if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
7
7
 
8
8
  const USER_AGENTS = [
9
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
10
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
9
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
10
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
11
11
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
12
12
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
13
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
13
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
14
14
  ];
15
15
 
16
16
  const VIEWPORTS = [
@@ -148,6 +148,50 @@ class BrowserController {
148
148
  return arr;
149
149
  }
150
150
  });
151
+
152
+ // WebGL Spoofing
153
+ const getParameterProxyHandler = {
154
+ apply: function(target, ctx, args) {
155
+ const param = args[0];
156
+ // UNMASKED_VENDOR_WEBGL
157
+ if (param === 37445) return 'Google Inc. (Apple)';
158
+ // UNMASKED_RENDERER_WEBGL
159
+ if (param === 37446) return 'ANGLE (Apple, Apple M2, OpenGL 4.1)';
160
+ return Reflect.apply(target, ctx, args);
161
+ }
162
+ };
163
+ const getParam = WebGLRenderingContext.prototype.getParameter;
164
+ WebGLRenderingContext.prototype.getParameter = new Proxy(getParam, getParameterProxyHandler);
165
+ if (typeof WebGL2RenderingContext !== 'undefined') {
166
+ const getParam2 = WebGL2RenderingContext.prototype.getParameter;
167
+ WebGL2RenderingContext.prototype.getParameter = new Proxy(getParam2, getParameterProxyHandler);
168
+ }
169
+
170
+ // Canvas Spoofing (slight noise)
171
+ const originalFillText = CanvasRenderingContext2D.prototype.fillText;
172
+ CanvasRenderingContext2D.prototype.fillText = function(...args) {
173
+ if (!this._spoofing_applied) {
174
+ this._spoofing_applied = true;
175
+ const r = Math.random() * 0.0001;
176
+ const g = Math.random() * 0.0001;
177
+ const b = Math.random() * 0.0001;
178
+ this.fillStyle = \`rgba(\${Math.floor(r * 255)}, \${Math.floor(g * 255)}, \${Math.floor(b * 255)}, 0.01)\`;
179
+ originalFillText.call(this, "spoof", 0, 0);
180
+ }
181
+ return originalFillText.apply(this, args);
182
+ };
183
+
184
+ // Media Devices Spoofing
185
+ if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
186
+ const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
187
+ navigator.mediaDevices.enumerateDevices = async () => {
188
+ return [
189
+ { kind: 'audioinput', deviceId: 'default', groupId: 'a', label: 'MacBook Pro Microphone' },
190
+ { kind: 'audiooutput', deviceId: 'default', groupId: 'b', label: 'MacBook Pro Speakers' },
191
+ { kind: 'videoinput', deviceId: 'default', groupId: 'c', label: 'FaceTime HD Camera' }
192
+ ];
193
+ };
194
+ }
151
195
  })();
152
196
  `);
153
197
  }
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs/promises');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { exec } = require('child_process');
7
+ const { promisify } = require('util');
8
+ const tesseract = require('tesseract.js');
9
+ const db = require('../../db/database');
10
+ const { getErrorMessage } = require('../bootstrap_helpers');
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ class ScreenRecorder {
15
+ constructor() {
16
+ this.intervalMs = 10000; // 10 seconds
17
+ this.intervalId = null;
18
+ this.cleanupIntervalId = null;
19
+ this.isRecording = false;
20
+ this.isProcessing = false;
21
+ this.tempFilePath = path.join(os.tmpdir(), `neoagent-screen-${Date.now()}.png`);
22
+ }
23
+
24
+ start() {
25
+ if (process.platform !== 'darwin') {
26
+ console.log('[ScreenRecorder] Not starting: Screen recording is currently macOS only.');
27
+ return;
28
+ }
29
+
30
+ if (this.isRecording) return;
31
+ this.isRecording = true;
32
+
33
+ console.log('[ScreenRecorder] Starting continuous screen recording (10s interval)');
34
+
35
+ // Start the recording loop
36
+ this.intervalId = setInterval(() => this.captureAndProcess(), this.intervalMs);
37
+
38
+ // Run an initial capture
39
+ this.captureAndProcess();
40
+
41
+ // Start daily cleanup of old records (7 days)
42
+ this.cleanupIntervalId = setInterval(() => this.cleanupOldRecords(), 24 * 60 * 60 * 1000);
43
+ this.cleanupOldRecords();
44
+ }
45
+
46
+ stop() {
47
+ this.isRecording = false;
48
+ if (this.intervalId) {
49
+ clearInterval(this.intervalId);
50
+ this.intervalId = null;
51
+ }
52
+ if (this.cleanupIntervalId) {
53
+ clearInterval(this.cleanupIntervalId);
54
+ this.cleanupIntervalId = null;
55
+ }
56
+ console.log('[ScreenRecorder] Stopped continuous screen recording');
57
+ }
58
+
59
+ async captureAndProcess() {
60
+ if (this.isProcessing || !this.isRecording) return;
61
+ this.isProcessing = true;
62
+
63
+ try {
64
+ // Capture screen silently (-x) to file
65
+ await execAsync(`screencapture -x "${this.tempFilePath}"`);
66
+
67
+ // Verify file exists
68
+ await fs.access(this.tempFilePath);
69
+
70
+ // Extract text via local OCR
71
+ const { data } = await tesseract.recognize(this.tempFilePath, 'eng+deu', {
72
+ logger: () => {} // Silence verbose OCR logs
73
+ });
74
+
75
+ const textContent = data.text.trim();
76
+
77
+ // Only store if meaningful text was found
78
+ if (textContent.length > 5) {
79
+ // We need a user ID. For the local desktop agent, usually user 1 or we query the active user.
80
+ const userRow = db.prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
81
+ if (userRow) {
82
+ // Identify the active foreground app via AppleScript
83
+ let appName = 'Unknown';
84
+ try {
85
+ const { stdout } = await execAsync(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`);
86
+ appName = stdout.trim();
87
+ } catch (e) {
88
+ // Ignore AppleScript errors
89
+ }
90
+
91
+ db.prepare(\`
92
+ INSERT INTO screen_history (user_id, app_name, text_content)
93
+ VALUES (?, ?, ?)
94
+ \`).run(userRow.id, appName, textContent);
95
+ }
96
+ }
97
+
98
+ } catch (err) {
99
+ console.error('[ScreenRecorder] Capture/OCR failed:', getErrorMessage(err));
100
+ } finally {
101
+ // Always cleanup the screenshot image immediately
102
+ try {
103
+ await fs.unlink(this.tempFilePath);
104
+ } catch (e) {
105
+ // Ignore unlink errors if file didn't exist
106
+ }
107
+ this.isProcessing = false;
108
+ }
109
+ }
110
+
111
+ cleanupOldRecords() {
112
+ try {
113
+ const result = db.prepare(\`
114
+ DELETE FROM screen_history
115
+ WHERE timestamp < datetime('now', '-7 days')
116
+ \`).run();
117
+ if (result.changes > 0) {
118
+ console.log(\`[ScreenRecorder] Purged \${result.changes} old screen history records.\`);
119
+ }
120
+ } catch (err) {
121
+ console.error('[ScreenRecorder] Cleanup failed:', getErrorMessage(err));
122
+ }
123
+ }
124
+ }
125
+
126
+ module.exports = { ScreenRecorder };
@@ -64,6 +64,10 @@ function resolveHomeAssistantOAuthConfig() {
64
64
  };
65
65
  }
66
66
 
67
+ function resolveSpotifyOAuthConfig() {
68
+ return resolveOAuthConfig('SPOTIFY');
69
+ }
70
+
67
71
  function describeEnvStatus(config, options = {}) {
68
72
  const label = String(options.label || 'This integration').trim() || 'This integration';
69
73
  if (config.configured) {
@@ -90,5 +94,6 @@ module.exports = {
90
94
  resolveOAuthConfig,
91
95
  resolveGoogleOAuthConfig,
92
96
  resolvePublicBaseUrl,
97
+ resolveSpotifyOAuthConfig,
93
98
  resolveSlackOAuthConfig,
94
99
  };
@@ -5,7 +5,9 @@ const { createGoogleWorkspaceProvider } = require('./google/provider');
5
5
  const { createHomeAssistantProvider } = require('./home_assistant/provider');
6
6
  const { createMicrosoftProvider } = require('./microsoft/provider');
7
7
  const { createNotionProvider } = require('./notion/provider');
8
+ const { createSpotifyProvider } = require('./spotify/provider');
8
9
  const { createSlackProvider } = require('./slack/provider');
10
+ const { createWeatherProvider } = require('./weather/provider');
9
11
  const { createWhatsAppPersonalProvider } = require('./whatsapp');
10
12
 
11
13
  function createIntegrationRegistry(options = {}) {
@@ -16,6 +18,8 @@ function createIntegrationRegistry(options = {}) {
16
18
  createSlackProvider(),
17
19
  createFigmaProvider(),
18
20
  createHomeAssistantProvider(),
21
+ createWeatherProvider(),
22
+ createSpotifyProvider(),
19
23
  createWhatsAppPersonalProvider(options),
20
24
  ];
21
25
  const byKey = new Map(providers.map((provider) => [provider.key, provider]));