neoagent 1.4.0 → 1.4.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 (39) hide show
  1. package/.env.example +5 -0
  2. package/com.neoagent.plist +8 -6
  3. package/docs/configuration.md +9 -1
  4. package/docs/skills.md +6 -2
  5. package/lib/manager.js +37 -10
  6. package/package.json +4 -1
  7. package/runtime/paths.js +80 -0
  8. package/server/db/database.js +78 -4
  9. package/server/index.js +5 -5
  10. package/server/public/app.html +124 -49
  11. package/server/public/assets/world-office-dark.png +0 -0
  12. package/server/public/assets/world-office-light.png +0 -0
  13. package/server/public/css/app.css +575 -242
  14. package/server/public/css/styles.css +445 -121
  15. package/server/public/js/app.js +1041 -423
  16. package/server/routes/memory.js +3 -1
  17. package/server/routes/settings.js +42 -6
  18. package/server/routes/skills.js +124 -84
  19. package/server/routes/store.js +102 -1
  20. package/server/services/ai/compaction.js +15 -31
  21. package/server/services/ai/engine.js +224 -202
  22. package/server/services/ai/history.js +188 -0
  23. package/server/services/ai/learning.js +143 -0
  24. package/server/services/ai/providers/google.js +8 -1
  25. package/server/services/ai/settings.js +80 -0
  26. package/server/services/ai/systemPrompt.js +57 -98
  27. package/server/services/ai/toolResult.js +151 -0
  28. package/server/services/ai/toolRunner.js +26 -7
  29. package/server/services/ai/toolSelector.js +140 -0
  30. package/server/services/ai/tools.js +158 -5
  31. package/server/services/browser/controller.js +124 -48
  32. package/server/services/manager.js +26 -3
  33. package/server/services/mcp/client.js +1 -1
  34. package/server/services/memory/embeddings.js +80 -14
  35. package/server/services/memory/manager.js +211 -17
  36. package/server/services/messaging/telnyx.js +3 -2
  37. package/server/services/messaging/whatsapp.js +3 -2
  38. package/server/services/scheduler/cron.js +6 -1
  39. package/server/services/websocket.js +19 -6
@@ -1,36 +1,118 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { DATA_DIR } = require('../../../runtime/paths');
3
4
 
4
- const SCREENSHOTS_DIR = path.join(__dirname, '..', '..', '..', 'data', 'screenshots');
5
+ const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
5
6
  if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
6
7
 
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',
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
+ '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',
14
+ ];
15
+
16
+ const VIEWPORTS = [
17
+ { width: 1280, height: 800 },
18
+ { width: 1366, height: 768 },
19
+ { width: 1440, height: 900 },
20
+ { width: 1920, height: 1080 },
21
+ ];
22
+
23
+ function rand(min, max) {
24
+ return Math.floor(Math.random() * (max - min + 1)) + min;
25
+ }
26
+
27
+ function sleep(ms) {
28
+ return new Promise(r => setTimeout(r, ms));
29
+ }
30
+
7
31
  class BrowserController {
8
32
  constructor(io) {
9
33
  this.io = io;
10
34
  this.browser = null;
11
35
  this.page = null;
12
36
  this.launching = false;
13
- this.headless = true; // can be toggled via setHeadless()
37
+ this.headless = true;
38
+ this._viewport = VIEWPORTS[0];
39
+ this._userAgent = USER_AGENTS[0];
14
40
  }
15
41
 
16
42
  async setHeadless(val) {
17
43
  const wasHeadless = this.headless;
18
44
  this.headless = val !== false && val !== 'false';
19
- // Close browser so it relaunches with new setting next time
20
45
  if (wasHeadless !== this.headless) {
21
46
  await this.close().catch(() => { });
22
47
  }
23
48
  }
24
49
 
25
- // Alias used by graceful shutdown in server.js
26
50
  async closeBrowser() {
27
51
  return this.close();
28
52
  }
29
53
 
54
+ async _applyStealthToPage(page) {
55
+ const ua = this._userAgent;
56
+ const vp = this._viewport;
57
+
58
+ await page.setUserAgent(ua);
59
+ await page.setViewport(vp);
60
+ await page.setExtraHTTPHeaders({
61
+ 'Accept-Language': 'en-US,en;q=0.9',
62
+ });
63
+
64
+ // Inject fingerprint overrides before any page script runs
65
+ await page.evaluateOnNewDocument(`
66
+ (() => {
67
+ // Remove webdriver flag
68
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
69
+
70
+ // Realistic language/platform
71
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
72
+ Object.defineProperty(navigator, 'platform', { get: () => 'MacIntel' });
73
+ Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => ${rand(4, 16)} });
74
+ Object.defineProperty(navigator, 'deviceMemory', { get: () => ${[4, 8, 16][rand(0, 2)]} });
75
+
76
+ // Make it look like a real Chrome install
77
+ window.chrome = {
78
+ app: { isInstalled: false, InstallState: {}, RunningState: {} },
79
+ runtime: {},
80
+ loadTimes: function() {},
81
+ csi: function() {},
82
+ };
83
+
84
+ // Permissions API — bots often show "denied" for notifications
85
+ const origQuery = window.navigator.permissions?.query?.bind(navigator.permissions);
86
+ if (origQuery) {
87
+ navigator.permissions.query = (parameters) =>
88
+ parameters.name === 'notifications'
89
+ ? Promise.resolve({ state: Notification.permission })
90
+ : origQuery(parameters);
91
+ }
92
+
93
+ // Hide automation plugins gap
94
+ Object.defineProperty(navigator, 'plugins', {
95
+ get: () => {
96
+ const arr = [
97
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
98
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
99
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
100
+ ];
101
+ arr.item = i => arr[i];
102
+ arr.namedItem = n => arr.find(p => p.name === n) || null;
103
+ arr.refresh = () => {};
104
+ Object.defineProperty(arr, 'length', { get: () => arr.length });
105
+ return arr;
106
+ }
107
+ });
108
+ })();
109
+ `);
110
+ }
111
+
30
112
  async ensureBrowser() {
31
113
  if (this.browser && this.browser.isConnected()) return;
32
114
  if (this.launching) {
33
- await new Promise(resolve => setTimeout(resolve, 2000));
115
+ await sleep(2000);
34
116
  return;
35
117
  }
36
118
 
@@ -40,29 +122,28 @@ class BrowserController {
40
122
  const StealthPlugin = require('puppeteer-extra-plugin-stealth');
41
123
  puppeteer.use(StealthPlugin());
42
124
 
125
+ this._userAgent = USER_AGENTS[rand(0, USER_AGENTS.length - 1)];
126
+ this._viewport = VIEWPORTS[rand(0, VIEWPORTS.length - 1)];
127
+
43
128
  this.browser = await puppeteer.launch({
44
129
  headless: this.headless ? 'new' : false,
45
130
  args: [
46
131
  '--no-sandbox',
47
132
  '--disable-setuid-sandbox',
48
133
  '--disable-dev-shm-usage',
49
- '--disable-gpu',
50
- '--window-size=1280,800'
134
+ '--disable-blink-features=AutomationControlled',
135
+ '--disable-infobars',
136
+ '--no-first-run',
137
+ '--no-default-browser-check',
138
+ '--lang=en-US,en',
139
+ `--window-size=${this._viewport.width},${this._viewport.height}`,
51
140
  ],
52
- defaultViewport: { width: 1280, height: 800 }
141
+ defaultViewport: this._viewport,
142
+ ignoreDefaultArgs: ['--enable-automation'],
53
143
  });
54
- this.page = await this.browser.newPage();
55
144
 
56
- const userAgents = [
57
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
58
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
59
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0',
60
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
61
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15'
62
- ];
63
- const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
64
- await this.page.setUserAgent(randomUA);
65
- this._currentUserAgent = randomUA;
145
+ this.page = await this.browser.newPage();
146
+ await this._applyStealthToPage(this.page);
66
147
  } finally {
67
148
  this.launching = false;
68
149
  }
@@ -72,9 +153,7 @@ class BrowserController {
72
153
  await this.ensureBrowser();
73
154
  if (!this.page || this.page.isClosed()) {
74
155
  this.page = await this.browser.newPage();
75
- if (this._currentUserAgent) {
76
- await this.page.setUserAgent(this._currentUserAgent);
77
- }
156
+ await this._applyStealthToPage(this.page);
78
157
  }
79
158
  return this.page;
80
159
  }
@@ -113,6 +192,9 @@ class BrowserController {
113
192
  await page.waitForSelector(options.waitFor, { timeout: 10000 }).catch(() => { });
114
193
  }
115
194
 
195
+ // Simulate human reading delay
196
+ await sleep(rand(500, 1500));
197
+
116
198
  const title = await page.title();
117
199
  const currentUrl = page.url();
118
200
 
@@ -125,8 +207,7 @@ class BrowserController {
125
207
  const body = document.body;
126
208
  if (!body) return '';
127
209
  const clone = body.cloneNode(true);
128
- const scripts = clone.querySelectorAll('script, style, noscript');
129
- scripts.forEach(s => s.remove());
210
+ clone.querySelectorAll('script, style, noscript').forEach(s => s.remove());
130
211
  return clone.innerText.slice(0, 10000);
131
212
  });
132
213
 
@@ -152,40 +233,39 @@ class BrowserController {
152
233
  const page = await this.ensurePage();
153
234
 
154
235
  try {
236
+ let target = null;
237
+
155
238
  if (text && !selector) {
156
239
  const elements = await page.$$('a, button, [role="button"], input[type="submit"], [onclick]');
157
- let found = false;
158
240
  for (const el of elements) {
159
241
  const elText = await page.evaluate(e => e.innerText || e.value || e.getAttribute('aria-label') || '', el);
160
242
  if (elText.toLowerCase().includes(text.toLowerCase())) {
161
- await el.click();
162
- found = true;
243
+ target = el;
163
244
  break;
164
245
  }
165
246
  }
166
- if (!found) {
167
- return { error: `No clickable element found with text: ${text}` };
168
- }
247
+ if (!target) return { error: `No clickable element found with text: ${text}` };
169
248
  } else if (selector) {
170
- await page.click(selector);
249
+ target = await page.$(selector);
250
+ if (!target) return { error: `Element not found: ${selector}` };
171
251
  } else {
172
252
  return { error: 'Either selector or text required' };
173
253
  }
174
254
 
175
- await new Promise(r => setTimeout(r, 1000));
255
+ // Human-like: hover first, then click with a hold delay
256
+ await target.hover();
257
+ await sleep(rand(80, 250));
258
+ await target.click({ delay: rand(50, 150) });
176
259
 
177
- let screenshotResult = null;
178
- if (screenshot) {
179
- screenshotResult = await this.takeScreenshot();
180
- }
260
+ await sleep(rand(800, 1800));
181
261
 
182
- const currentUrl = page.url();
183
- const title = await page.title();
262
+ let screenshotResult = null;
263
+ if (screenshot) screenshotResult = await this.takeScreenshot();
184
264
 
185
265
  return {
186
266
  success: true,
187
- url: currentUrl,
188
- title,
267
+ url: page.url(),
268
+ title: await page.title(),
189
269
  screenshotPath: screenshotResult?.screenshotPath || null
190
270
  };
191
271
  } catch (err) {
@@ -202,21 +282,17 @@ class BrowserController {
202
282
  await page.keyboard.press('Backspace');
203
283
  }
204
284
 
205
- // Simulate human typing speeds (between 30ms and 150ms per keystroke)
206
285
  for (const char of text) {
207
- const charDelay = Math.floor(Math.random() * (150 - 30 + 1) + 30);
208
- await page.type(selector, char, { delay: charDelay });
286
+ await page.type(selector, char, { delay: rand(30, 150) });
209
287
  }
210
288
 
211
289
  if (options.pressEnter) {
212
290
  await page.keyboard.press('Enter');
213
- await new Promise(r => setTimeout(r, 1000));
291
+ await sleep(1000);
214
292
  }
215
293
 
216
294
  let screenshotResult = null;
217
- if (options.screenshot !== false) {
218
- screenshotResult = await this.takeScreenshot();
219
- }
295
+ if (options.screenshot !== false) screenshotResult = await this.takeScreenshot();
220
296
 
221
297
  return {
222
298
  success: true,
@@ -268,7 +344,7 @@ class BrowserController {
268
344
  }
269
345
 
270
346
  async screenshot(options = {}) {
271
- return await this.takeScreenshot(options);
347
+ return this.takeScreenshot(options);
272
348
  }
273
349
 
274
350
  async launch(options = {}) {
@@ -5,7 +5,9 @@ const { MemoryManager } = require('./memory/manager');
5
5
  const { MCPClient } = require('./mcp/client');
6
6
  const { BrowserController } = require('./browser/controller');
7
7
  const { AgentEngine } = require('./ai/engine');
8
+ const { LearningManager } = require('./ai/learning');
8
9
  const { MultiStepOrchestrator } = require('./ai/multiStep');
10
+ const { SkillRunner } = require('./ai/toolRunner');
9
11
  const { MessagingManager } = require('./messaging/manager');
10
12
  const { Scheduler } = require('./scheduler/cron');
11
13
  const { setupWebSocket } = require('./websocket');
@@ -29,7 +31,21 @@ async function startServices(app, io) {
29
31
  }
30
32
  app.locals.browserController = browserController;
31
33
 
32
- const agentEngine = new AgentEngine(io, { memoryManager, mcpClient, browserController, messagingManager: null });
34
+ const skillRunner = new SkillRunner();
35
+ await skillRunner.loadSkills();
36
+ app.locals.skillRunner = skillRunner;
37
+
38
+ const learningManager = new LearningManager(skillRunner, io);
39
+ app.locals.learningManager = learningManager;
40
+
41
+ const agentEngine = new AgentEngine(io, {
42
+ memoryManager,
43
+ mcpClient,
44
+ browserController,
45
+ messagingManager: null,
46
+ skillRunner,
47
+ learningManager
48
+ });
33
49
  app.locals.agentEngine = agentEngine;
34
50
 
35
51
  const multiStep = new MultiStepOrchestrator(agentEngine, io);
@@ -162,12 +178,19 @@ async function startServices(app, io) {
162
178
  await processMessage(userId, msg);
163
179
  });
164
180
 
165
- const scheduler = new Scheduler(io, agentEngine);
181
+ const scheduler = new Scheduler(io, agentEngine, app);
166
182
  app.locals.scheduler = scheduler;
167
183
  agentEngine.scheduler = scheduler;
168
184
  scheduler.start();
169
185
 
170
- setupWebSocket(io, { agentEngine, messagingManager, mcpClient, scheduler, memoryManager, app });
186
+ setupWebSocket(io, {
187
+ agentEngine,
188
+ messagingManager,
189
+ mcpClient,
190
+ scheduler,
191
+ memoryManager,
192
+ app
193
+ });
171
194
  app.locals.io = io;
172
195
 
173
196
  console.log('All services initialized');
@@ -12,7 +12,7 @@ class DBAuthProvider {
12
12
  }
13
13
 
14
14
  get redirectUrl() {
15
- const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 8000}`;
15
+ const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3333}`;
16
16
  return `${baseUrl}/api/mcp/oauth/callback`;
17
17
  }
18
18
 
@@ -2,30 +2,80 @@
2
2
 
3
3
  /**
4
4
  * Embedding helpers for the semantic memory system.
5
- * Uses OpenAI text-embedding-3-small (1536 dims) when available.
6
- * Gracefully degrades to keyword search if OPENAI_API_KEY is missing.
5
+ *
6
+ * Provider selection (in priority order):
7
+ * 1. Google (text-embedding-004, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
8
+ * 2. OpenAI (text-embedding-3-small, 1536 dims) — when OPENAI_API_KEY is set
9
+ * 3. Keyword fallback — when no API key is available
7
10
  */
8
11
 
9
12
  const https = require('https');
10
13
 
11
- const EMBEDDING_MODEL = 'text-embedding-3-small';
12
- const EMBED_DIM = 1536;
14
+ const OPENAI_MODEL = 'text-embedding-3-small';
15
+ const OPENAI_DIM = 1536;
16
+ const GOOGLE_MODEL = 'text-embedding-004';
17
+ const GOOGLE_DIM = 768;
13
18
 
14
- /**
15
- * Get an embedding vector for a piece of text.
16
- * Returns a Float32Array of length EMBED_DIM, or null if unavailable.
17
- */
18
- async function getEmbedding(text) {
19
+ // Exported so callers can sanity-check stored vector dimensions if needed
20
+ const EMBED_DIM = OPENAI_DIM;
21
+ const EMBED_DIM_GOOGLE = GOOGLE_DIM;
22
+
23
+ async function getGeminiEmbedding(text) {
24
+ const apiKey = process.env.GOOGLE_AI_KEY;
25
+ if (!apiKey) return null;
26
+ if (!text || !text.trim()) return null;
27
+
28
+ const truncated = text.slice(0, 25000);
29
+
30
+ return new Promise((resolve) => {
31
+ const body = JSON.stringify({
32
+ model: `models/${GOOGLE_MODEL}`,
33
+ content: { parts: [{ text: truncated }] }
34
+ });
35
+
36
+ const path = `/v1beta/models/${GOOGLE_MODEL}:embedContent?key=${apiKey}`;
37
+ const options = {
38
+ hostname: 'generativelanguage.googleapis.com',
39
+ path,
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'Content-Length': Buffer.byteLength(body)
44
+ }
45
+ };
46
+
47
+ const req = https.request(options, (res) => {
48
+ let data = '';
49
+ res.on('data', chunk => { data += chunk; });
50
+ res.on('end', () => {
51
+ try {
52
+ const parsed = JSON.parse(data);
53
+ const vec = parsed.embedding?.values;
54
+ if (!vec) return resolve(null);
55
+ resolve(new Float32Array(vec));
56
+ } catch {
57
+ resolve(null);
58
+ }
59
+ });
60
+ });
61
+
62
+ req.on('error', () => resolve(null));
63
+ req.setTimeout(15000, () => { req.destroy(); resolve(null); });
64
+ req.write(body);
65
+ req.end();
66
+ });
67
+ }
68
+
69
+ async function getOpenAIEmbedding(text) {
19
70
  const apiKey = process.env.OPENAI_API_KEY;
20
71
  if (!apiKey) return null;
21
72
  if (!text || !text.trim()) return null;
22
73
 
23
- // Truncate very long text to stay within token limits (~8k tokens)
24
74
  const truncated = text.slice(0, 25000);
25
75
 
26
- return new Promise((resolve, reject) => {
76
+ return new Promise((resolve) => {
27
77
  const body = JSON.stringify({
28
- model: EMBEDDING_MODEL,
78
+ model: OPENAI_MODEL,
29
79
  input: truncated,
30
80
  encoding_format: 'float'
31
81
  });
@@ -64,6 +114,21 @@ async function getEmbedding(text) {
64
114
  });
65
115
  }
66
116
 
117
+ /**
118
+ * Get an embedding vector for a piece of text.
119
+ * @param {string} text
120
+ * @param {string} [provider] - 'google' to prefer Gemini embeddings
121
+ * @returns {Float32Array|null}
122
+ */
123
+ async function getEmbedding(text, provider) {
124
+ if (!text || !text.trim()) return null;
125
+ if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
126
+ const vec = await getGeminiEmbedding(text);
127
+ if (vec) return vec;
128
+ }
129
+ return getOpenAIEmbedding(text);
130
+ }
131
+
67
132
  /**
68
133
  * Cosine similarity between two Float32Arrays.
69
134
  * Returns a value in [-1, 1]; higher = more similar.
@@ -72,7 +137,7 @@ function cosineSimilarity(a, b) {
72
137
  if (!a || !b || a.length !== b.length) return 0;
73
138
  let dot = 0, magA = 0, magB = 0;
74
139
  for (let i = 0; i < a.length; i++) {
75
- dot += a[i] * b[i];
140
+ dot += a[i] * b[i];
76
141
  magA += a[i] * a[i];
77
142
  magB += b[i] * b[i];
78
143
  }
@@ -122,5 +187,6 @@ module.exports = {
122
187
  serializeEmbedding,
123
188
  deserializeEmbedding,
124
189
  keywordSimilarity,
125
- EMBED_DIM
190
+ EMBED_DIM,
191
+ EMBED_DIM_GOOGLE
126
192
  };