neoagent 2.3.1-beta.2 → 2.3.1-beta.20

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 (51) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/lib/manager.js +231 -7
  7. package/package.json +2 -1
  8. package/server/db/database.js +68 -0
  9. package/server/http/middleware.js +50 -0
  10. package/server/http/routes.js +3 -1
  11. package/server/index.js +1 -0
  12. package/server/public/.last_build_id +1 -1
  13. package/server/public/assets/NOTICES +61 -0
  14. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  15. package/server/public/flutter_bootstrap.js +1 -1
  16. package/server/public/main.dart.js +65262 -64422
  17. package/server/routes/integrations.js +86 -0
  18. package/server/routes/memory.js +11 -2
  19. package/server/routes/screenHistory.js +46 -0
  20. package/server/routes/triggers.js +81 -0
  21. package/server/services/ai/models.js +30 -0
  22. package/server/services/ai/providers/githubCopilot.js +97 -0
  23. package/server/services/ai/providers/openai.js +2 -1
  24. package/server/services/ai/providers/openaiCodex.js +31 -0
  25. package/server/services/ai/settings.js +20 -0
  26. package/server/services/ai/systemPrompt.js +1 -1
  27. package/server/services/ai/tools.js +35 -6
  28. package/server/services/browser/controller.js +47 -3
  29. package/server/services/desktop/screenRecorder.js +172 -0
  30. package/server/services/integrations/env.js +5 -0
  31. package/server/services/integrations/github/common.js +106 -0
  32. package/server/services/integrations/github/provider.js +499 -0
  33. package/server/services/integrations/github/repos.js +1124 -0
  34. package/server/services/integrations/home_assistant/provider.js +306 -26
  35. package/server/services/integrations/manager.js +63 -7
  36. package/server/services/integrations/oauth_provider.js +13 -6
  37. package/server/services/integrations/provider_config_store.js +76 -0
  38. package/server/services/integrations/registry.js +4 -0
  39. package/server/services/integrations/trello/provider.js +744 -0
  40. package/server/services/integrations/whatsapp/provider.js +6 -2
  41. package/server/services/manager.js +22 -0
  42. package/server/services/memory/manager.js +39 -2
  43. package/server/services/skills/base_catalog.js +33 -0
  44. package/server/services/tasks/adapters/index.js +1 -0
  45. package/server/services/tasks/adapters/manual.js +12 -0
  46. package/server/services/tasks/runtime.js +1 -1
  47. package/server/services/voice/openaiClient.js +4 -1
  48. package/server/services/voice/providers.js +2 -1
  49. package/server/services/widgets/service.js +49 -4
  50. package/server/utils/local_secrets.js +56 -0
  51. package/server/utils/logger.js +37 -9
@@ -0,0 +1,172 @@
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
+ this.lastBenignSkipAt = 0;
23
+ }
24
+
25
+ _isCaptureInactiveApp(appName) {
26
+ const normalized = String(appName || '').trim().toLowerCase();
27
+ return normalized === '' || normalized === 'loginwindow' || normalized === 'screensaverengine';
28
+ }
29
+
30
+ _isBenignCaptureError(message) {
31
+ const text = String(message || '').toLowerCase();
32
+ return (
33
+ text.includes('operation not permitted') ||
34
+ text.includes('not authorized') ||
35
+ text.includes('user canceled') ||
36
+ text.includes('cgwindowlistcreateimage') ||
37
+ text.includes('screencapture') ||
38
+ text.includes('timed out')
39
+ );
40
+ }
41
+
42
+ _logBenignSkip(reason) {
43
+ const now = Date.now();
44
+ if (now - this.lastBenignSkipAt < 5 * 60 * 1000) {
45
+ return;
46
+ }
47
+ this.lastBenignSkipAt = now;
48
+ console.warn(`[ScreenRecorder] Capture skipped: ${reason}`);
49
+ }
50
+
51
+ start() {
52
+ const enabledEnv = String(process.env.NEOAGENT_SCREEN_RECORDER_ENABLED || '').trim().toLowerCase();
53
+ if (enabledEnv === '0' || enabledEnv === 'false' || enabledEnv === 'off' || enabledEnv === 'no') {
54
+ console.log('[ScreenRecorder] Not starting: disabled by NEOAGENT_SCREEN_RECORDER_ENABLED.');
55
+ return;
56
+ }
57
+
58
+ if (process.platform !== 'darwin') {
59
+ console.log('[ScreenRecorder] Not starting: Screen recording is currently macOS only.');
60
+ return;
61
+ }
62
+
63
+ if (this.isRecording) return;
64
+ this.isRecording = true;
65
+
66
+ console.log('[ScreenRecorder] Starting continuous screen recording (10s interval)');
67
+
68
+ // Start the recording loop
69
+ this.intervalId = setInterval(() => this.captureAndProcess(), this.intervalMs);
70
+
71
+ // Run an initial capture
72
+ this.captureAndProcess();
73
+
74
+ // Start daily cleanup of old records (7 days)
75
+ this.cleanupIntervalId = setInterval(() => this.cleanupOldRecords(), 24 * 60 * 60 * 1000);
76
+ this.cleanupOldRecords();
77
+ }
78
+
79
+ stop() {
80
+ this.isRecording = false;
81
+ if (this.intervalId) {
82
+ clearInterval(this.intervalId);
83
+ this.intervalId = null;
84
+ }
85
+ if (this.cleanupIntervalId) {
86
+ clearInterval(this.cleanupIntervalId);
87
+ this.cleanupIntervalId = null;
88
+ }
89
+ console.log('[ScreenRecorder] Stopped continuous screen recording');
90
+ }
91
+
92
+ async captureAndProcess() {
93
+ if (this.isProcessing || !this.isRecording) return;
94
+ this.isProcessing = true;
95
+
96
+ try {
97
+ // Skip capture when the desktop session is inactive (e.g. locked screen).
98
+ let frontmostApp = '';
99
+ try {
100
+ const { stdout } = await execAsync(`osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true'`);
101
+ frontmostApp = (stdout || '').trim();
102
+ } catch {
103
+ frontmostApp = '';
104
+ }
105
+
106
+ if (this._isCaptureInactiveApp(frontmostApp)) {
107
+ this._logBenignSkip('no active frontmost app');
108
+ return;
109
+ }
110
+
111
+ // Capture screen silently (-x) to file
112
+ await execAsync(`screencapture -x "${this.tempFilePath}"`);
113
+
114
+ // Verify file exists
115
+ await fs.access(this.tempFilePath);
116
+
117
+ // Extract text via local OCR
118
+ const { data } = await tesseract.recognize(this.tempFilePath, 'eng+deu', {
119
+ logger: () => {} // Silence verbose OCR logs
120
+ });
121
+
122
+ const textContent = data.text.trim();
123
+
124
+ // Only store if meaningful text was found
125
+ if (textContent.length > 5) {
126
+ // We need a user ID. For the local desktop agent, usually user 1 or we query the active user.
127
+ const userRow = db.prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
128
+ if (userRow) {
129
+ // Identify the active foreground app via AppleScript
130
+ let appName = frontmostApp || 'Unknown';
131
+
132
+ db.prepare(`
133
+ INSERT INTO screen_history (user_id, app_name, text_content)
134
+ VALUES (?, ?, ?)
135
+ `).run(userRow.id, appName, textContent);
136
+ }
137
+ }
138
+
139
+ } catch (err) {
140
+ const errorMessage = getErrorMessage(err);
141
+ if (this._isBenignCaptureError(errorMessage)) {
142
+ this._logBenignSkip(errorMessage);
143
+ } else {
144
+ console.error('[ScreenRecorder] Capture/OCR failed:', errorMessage);
145
+ }
146
+ } finally {
147
+ // Always cleanup the screenshot image immediately
148
+ try {
149
+ await fs.unlink(this.tempFilePath);
150
+ } catch (e) {
151
+ // Ignore unlink errors if file didn't exist
152
+ }
153
+ this.isProcessing = false;
154
+ }
155
+ }
156
+
157
+ cleanupOldRecords() {
158
+ try {
159
+ const result = db.prepare(`
160
+ DELETE FROM screen_history
161
+ WHERE timestamp < datetime('now', '-7 days')
162
+ `).run();
163
+ if (result.changes > 0) {
164
+ console.log(`[ScreenRecorder] Purged ${result.changes} old screen history records.`);
165
+ }
166
+ } catch (err) {
167
+ console.error('[ScreenRecorder] Cleanup failed:', getErrorMessage(err));
168
+ }
169
+ }
170
+ }
171
+
172
+ module.exports = { ScreenRecorder };
@@ -68,6 +68,10 @@ function resolveSpotifyOAuthConfig() {
68
68
  return resolveOAuthConfig('SPOTIFY');
69
69
  }
70
70
 
71
+ function resolveGithubOAuthConfig() {
72
+ return resolveOAuthConfig('GITHUB');
73
+ }
74
+
71
75
  function describeEnvStatus(config, options = {}) {
72
76
  const label = String(options.label || 'This integration').trim() || 'This integration';
73
77
  if (config.configured) {
@@ -96,4 +100,5 @@ module.exports = {
96
100
  resolvePublicBaseUrl,
97
101
  resolveSpotifyOAuthConfig,
98
102
  resolveSlackOAuthConfig,
103
+ resolveGithubOAuthConfig,
99
104
  };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function base64UrlSha256(value) {
6
+ return crypto
7
+ .createHash('sha256')
8
+ .update(String(value || ''))
9
+ .digest('base64')
10
+ .replace(/\+/g, '-')
11
+ .replace(/\//g, '_')
12
+ .replace(/=+$/g, '');
13
+ }
14
+
15
+ async function githubApiRequest(auth, options = {}) {
16
+ const {
17
+ method = 'GET',
18
+ path,
19
+ query = null,
20
+ body = null,
21
+ baseUrl = 'https://api.github.com',
22
+ token: overrideToken = '',
23
+ } = options;
24
+
25
+ const token = String(overrideToken || auth?.token || '').trim();
26
+ if (!token) {
27
+ throw new Error('GitHub authentication token is required for GitHub API requests.');
28
+ }
29
+
30
+ const url = new URL(path, baseUrl);
31
+ if (query && typeof query === 'object') {
32
+ for (const [key, value] of Object.entries(query)) {
33
+ if (value !== undefined && value !== null) {
34
+ url.searchParams.set(key, String(value));
35
+ }
36
+ }
37
+ }
38
+
39
+ const headers = {
40
+ 'Accept': 'application/vnd.github.v3+json',
41
+ 'Authorization': `Bearer ${token}`,
42
+ 'X-GitHub-Api-Version': '2022-11-28',
43
+ };
44
+
45
+ if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
46
+ headers['Content-Type'] = 'application/json';
47
+ }
48
+
49
+ const response = await fetch(url.toString(), {
50
+ method,
51
+ headers,
52
+ body: body ? JSON.stringify(body) : undefined,
53
+ });
54
+
55
+ let data = null;
56
+ if (response.status !== 204 && response.status !== 205) {
57
+ const rawBody = await response.text();
58
+ if (rawBody.trim()) {
59
+ try {
60
+ data = JSON.parse(rawBody);
61
+ } catch {
62
+ if (!response.ok) {
63
+ const error = new Error(`GitHub API error ${response.status}: ${rawBody}`);
64
+ error.status = response.status;
65
+ error.data = rawBody;
66
+ throw error;
67
+ }
68
+ data = rawBody;
69
+ }
70
+ }
71
+ }
72
+
73
+ if (!response.ok) {
74
+ const errorMessage =
75
+ (data && typeof data === 'object' ? data.message : null) ||
76
+ `GitHub API error: ${response.status}`;
77
+ const error = new Error(errorMessage);
78
+ error.status = response.status;
79
+ error.data = data;
80
+ throw error;
81
+ }
82
+
83
+ return data;
84
+ }
85
+
86
+ function buildPaginationParams(options = {}) {
87
+ const params = {};
88
+ if (options.page) params.page = Number(options.page);
89
+ if (options.per_page) params.per_page = Math.min(Number(options.per_page) || 30, 100);
90
+ return params;
91
+ }
92
+
93
+ function parseOwnerRepo(ownerRepo) {
94
+ const parts = String(ownerRepo || '').split('/');
95
+ if (parts.length !== 2) {
96
+ throw new Error('owner_repo must be in format "owner/repo"');
97
+ }
98
+ return { owner: parts[0], repo: parts[1] };
99
+ }
100
+
101
+ module.exports = {
102
+ base64UrlSha256,
103
+ buildPaginationParams,
104
+ githubApiRequest,
105
+ parseOwnerRepo,
106
+ };