neoagent 2.4.1-beta.8 → 2.4.1
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.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -1,175 +1,280 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs/promises');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const os = require('os');
|
|
6
|
-
const {
|
|
7
|
+
const { execFile } = require('child_process');
|
|
7
8
|
const { promisify } = require('util');
|
|
8
9
|
const tesseract = require('tesseract.js');
|
|
9
10
|
const db = require('../../db/database');
|
|
10
11
|
const { getErrorMessage } = require('../bootstrap_helpers');
|
|
12
|
+
const {
|
|
13
|
+
CLEANUP_INTERVAL_MS,
|
|
14
|
+
DEFAULT_CAPTURE_INTERVAL_MS,
|
|
15
|
+
DEFAULT_RETENTION_DAYS,
|
|
16
|
+
FRONTMOST_APP_SCRIPT,
|
|
17
|
+
MINIMUM_TEXT_LENGTH,
|
|
18
|
+
MIN_CAPTURE_INTERVAL_MS,
|
|
19
|
+
hasOpenConnectionForUser,
|
|
20
|
+
isExplicitlyEnabled,
|
|
21
|
+
parsePositiveInteger,
|
|
22
|
+
} = require('./screen_recorder_support');
|
|
11
23
|
|
|
12
|
-
const
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
13
25
|
|
|
14
26
|
class ScreenRecorder {
|
|
15
27
|
constructor(options = {}) {
|
|
16
|
-
this.
|
|
28
|
+
this.env = options.env || process.env;
|
|
29
|
+
this.platform = options.platform || process.platform;
|
|
30
|
+
this.db = options.db || db;
|
|
31
|
+
this.fs = options.fs || fs;
|
|
32
|
+
this.execFile = options.execFile || execFileAsync;
|
|
33
|
+
this.recognize = options.recognize || tesseract.recognize;
|
|
34
|
+
this.setInterval = options.setInterval || setInterval;
|
|
35
|
+
this.clearInterval = options.clearInterval || clearInterval;
|
|
36
|
+
this.now = options.now || Date.now;
|
|
37
|
+
this.hasActiveCaptureSessionForUser =
|
|
38
|
+
typeof options.hasActiveCaptureSessionForUser === 'function'
|
|
39
|
+
? options.hasActiveCaptureSessionForUser
|
|
40
|
+
: () => false;
|
|
41
|
+
|
|
17
42
|
this.intervalId = null;
|
|
18
43
|
this.cleanupIntervalId = null;
|
|
19
44
|
this.isRecording = false;
|
|
20
|
-
this.
|
|
21
|
-
this.tempFilePath = path.join(
|
|
45
|
+
this.activeCapturePromise = null;
|
|
46
|
+
this.tempFilePath = options.tempFilePath || path.join(
|
|
47
|
+
os.tmpdir(),
|
|
48
|
+
`neoagent-screen-${process.pid}-${crypto.randomUUID()}.png`,
|
|
49
|
+
);
|
|
22
50
|
this.lastBenignSkipAt = 0;
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
this.lastErrorLogAt = 0;
|
|
52
|
+
this.ownerUserId = null;
|
|
53
|
+
this.intervalMs = DEFAULT_CAPTURE_INTERVAL_MS;
|
|
54
|
+
this.retentionDays = DEFAULT_RETENTION_DAYS;
|
|
55
|
+
this.state = 'idle';
|
|
56
|
+
this.reason = 'not started';
|
|
57
|
+
this.lastCaptureAt = null;
|
|
58
|
+
this.lastSuccessAt = null;
|
|
59
|
+
this.lastSkipReason = null;
|
|
60
|
+
this.lastError = null;
|
|
26
61
|
}
|
|
27
62
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
getStatus() {
|
|
64
|
+
return {
|
|
65
|
+
state: this.state,
|
|
66
|
+
reason: this.reason,
|
|
67
|
+
ownerUserId: this.ownerUserId,
|
|
68
|
+
intervalMs: this.intervalMs,
|
|
69
|
+
retentionDays: this.retentionDays,
|
|
70
|
+
lastCaptureAt: this.lastCaptureAt,
|
|
71
|
+
lastSuccessAt: this.lastSuccessAt,
|
|
72
|
+
lastSkipReason: this.lastSkipReason,
|
|
73
|
+
lastError: this.lastError,
|
|
74
|
+
};
|
|
31
75
|
}
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
text.includes('user canceled') ||
|
|
39
|
-
text.includes('cgwindowlistcreateimage') ||
|
|
40
|
-
text.includes('screencapture') ||
|
|
41
|
-
text.includes('timed out')
|
|
42
|
-
);
|
|
77
|
+
_setInactiveState(state, reason) {
|
|
78
|
+
this.state = state;
|
|
79
|
+
this.reason = reason;
|
|
80
|
+
this.isRecording = false;
|
|
81
|
+
return this.getStatus();
|
|
43
82
|
}
|
|
44
83
|
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
_loadConfiguration() {
|
|
85
|
+
const ownerUserId = parsePositiveInteger(
|
|
86
|
+
this.env.NEOAGENT_SCREEN_RECORDER_USER_ID,
|
|
87
|
+
'NEOAGENT_SCREEN_RECORDER_USER_ID',
|
|
88
|
+
null,
|
|
89
|
+
);
|
|
90
|
+
if (ownerUserId == null) {
|
|
91
|
+
throw new Error('NEOAGENT_SCREEN_RECORDER_USER_ID is required when screen recording is enabled.');
|
|
49
92
|
}
|
|
50
|
-
|
|
51
|
-
|
|
93
|
+
|
|
94
|
+
this.intervalMs = parsePositiveInteger(
|
|
95
|
+
this.env.NEOAGENT_SCREEN_RECORDER_INTERVAL_MS,
|
|
96
|
+
'NEOAGENT_SCREEN_RECORDER_INTERVAL_MS',
|
|
97
|
+
DEFAULT_CAPTURE_INTERVAL_MS,
|
|
98
|
+
MIN_CAPTURE_INTERVAL_MS,
|
|
99
|
+
);
|
|
100
|
+
this.retentionDays = parsePositiveInteger(
|
|
101
|
+
this.env.NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS,
|
|
102
|
+
'NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS',
|
|
103
|
+
DEFAULT_RETENTION_DAYS,
|
|
104
|
+
);
|
|
105
|
+
this.ownerUserId = ownerUserId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_ownerExists() {
|
|
109
|
+
return Boolean(
|
|
110
|
+
this.db.prepare('SELECT id FROM users WHERE id = ?').get(this.ownerUserId),
|
|
111
|
+
);
|
|
52
112
|
}
|
|
53
113
|
|
|
54
114
|
start() {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log('[ScreenRecorder] Not starting: disabled by NEOAGENT_SCREEN_RECORDER_ENABLED.');
|
|
58
|
-
return;
|
|
115
|
+
if (this.isRecording) {
|
|
116
|
+
return this.getStatus();
|
|
59
117
|
}
|
|
60
118
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
if (!isExplicitlyEnabled(this.env.NEOAGENT_SCREEN_RECORDER_ENABLED)) {
|
|
120
|
+
return this._setInactiveState('disabled', 'NEOAGENT_SCREEN_RECORDER_ENABLED is not enabled');
|
|
121
|
+
}
|
|
122
|
+
if (this.platform !== 'darwin') {
|
|
123
|
+
return this._setInactiveState('unsupported', 'screen recording is currently supported only on macOS');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
this._loadConfiguration();
|
|
128
|
+
if (!this._ownerExists()) {
|
|
129
|
+
return this._setInactiveState(
|
|
130
|
+
'misconfigured',
|
|
131
|
+
`configured screen recorder user ${this.ownerUserId} does not exist`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return this._setInactiveState('misconfigured', getErrorMessage(err));
|
|
64
136
|
}
|
|
65
137
|
|
|
66
|
-
if (this.isRecording) return;
|
|
67
138
|
this.isRecording = true;
|
|
139
|
+
this.state = 'running';
|
|
140
|
+
this.reason = null;
|
|
141
|
+
this.lastError = null;
|
|
142
|
+
|
|
143
|
+
this.intervalId = this.setInterval(() => {
|
|
144
|
+
void this.captureAndProcess();
|
|
145
|
+
}, this.intervalMs);
|
|
146
|
+
this.intervalId?.unref?.();
|
|
68
147
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Run an initial capture
|
|
75
|
-
this.captureAndProcess();
|
|
148
|
+
this.cleanupIntervalId = this.setInterval(
|
|
149
|
+
() => this.cleanupOldRecords(),
|
|
150
|
+
CLEANUP_INTERVAL_MS,
|
|
151
|
+
);
|
|
152
|
+
this.cleanupIntervalId?.unref?.();
|
|
76
153
|
|
|
77
|
-
|
|
78
|
-
this.cleanupIntervalId = setInterval(() => this.cleanupOldRecords(), 24 * 60 * 60 * 1000);
|
|
154
|
+
void this.captureAndProcess();
|
|
79
155
|
this.cleanupOldRecords();
|
|
156
|
+
return this.getStatus();
|
|
80
157
|
}
|
|
81
158
|
|
|
82
|
-
stop() {
|
|
159
|
+
async stop() {
|
|
160
|
+
const wasRunning = this.isRecording;
|
|
83
161
|
this.isRecording = false;
|
|
162
|
+
|
|
84
163
|
if (this.intervalId) {
|
|
85
|
-
clearInterval(this.intervalId);
|
|
164
|
+
this.clearInterval(this.intervalId);
|
|
86
165
|
this.intervalId = null;
|
|
87
166
|
}
|
|
88
167
|
if (this.cleanupIntervalId) {
|
|
89
|
-
clearInterval(this.cleanupIntervalId);
|
|
168
|
+
this.clearInterval(this.cleanupIntervalId);
|
|
90
169
|
this.cleanupIntervalId = null;
|
|
91
170
|
}
|
|
92
|
-
|
|
171
|
+
|
|
172
|
+
if (this.activeCapturePromise) {
|
|
173
|
+
await this.activeCapturePromise;
|
|
174
|
+
}
|
|
175
|
+
if (wasRunning) {
|
|
176
|
+
this.state = 'stopped';
|
|
177
|
+
this.reason = 'service stopped';
|
|
178
|
+
}
|
|
179
|
+
return this.getStatus();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_isCaptureInactiveApp(appName) {
|
|
183
|
+
const normalized = String(appName || '').trim().toLowerCase();
|
|
184
|
+
return normalized === '' || normalized === 'loginwindow' || normalized === 'screensaverengine';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
_recordSkip(reason) {
|
|
188
|
+
this.lastSkipReason = reason;
|
|
189
|
+
const now = this.now();
|
|
190
|
+
if (now - this.lastBenignSkipAt < 5 * 60 * 1000) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.lastBenignSkipAt = now;
|
|
194
|
+
console.log(`[ScreenRecorder] Capture skipped: ${reason}`);
|
|
93
195
|
}
|
|
94
196
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
197
|
+
_recordError(err) {
|
|
198
|
+
this.lastError = getErrorMessage(err);
|
|
199
|
+
const now = this.now();
|
|
200
|
+
if (now - this.lastErrorLogAt < 5 * 60 * 1000) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.lastErrorLogAt = now;
|
|
204
|
+
console.error('[ScreenRecorder] Capture/OCR failed:', this.lastError);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
captureAndProcess() {
|
|
208
|
+
if (!this.isRecording) {
|
|
209
|
+
return Promise.resolve();
|
|
210
|
+
}
|
|
211
|
+
if (this.activeCapturePromise) {
|
|
212
|
+
return this.activeCapturePromise;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.activeCapturePromise = this._captureAndProcess().finally(() => {
|
|
216
|
+
this.activeCapturePromise = null;
|
|
217
|
+
});
|
|
218
|
+
return this.activeCapturePromise;
|
|
219
|
+
}
|
|
98
220
|
|
|
221
|
+
async _captureAndProcess() {
|
|
222
|
+
this.lastCaptureAt = new Date(this.now()).toISOString();
|
|
99
223
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!this.hasActiveRemoteCaptureSession()) {
|
|
103
|
-
this._logBenignSkip('no active external capture session');
|
|
224
|
+
if (!this.hasActiveCaptureSessionForUser(this.ownerUserId)) {
|
|
225
|
+
this._recordSkip('no active external capture session for the configured user');
|
|
104
226
|
return;
|
|
105
227
|
}
|
|
106
228
|
|
|
107
|
-
// Skip capture when the desktop session is inactive (e.g. locked screen).
|
|
108
229
|
let frontmostApp = '';
|
|
109
230
|
try {
|
|
110
|
-
const { stdout } = await
|
|
111
|
-
frontmostApp = (stdout || '').trim();
|
|
231
|
+
const { stdout } = await this.execFile('osascript', ['-e', FRONTMOST_APP_SCRIPT]);
|
|
232
|
+
frontmostApp = String(stdout || '').trim();
|
|
112
233
|
} catch {
|
|
113
234
|
frontmostApp = '';
|
|
114
235
|
}
|
|
115
236
|
|
|
116
237
|
if (this._isCaptureInactiveApp(frontmostApp)) {
|
|
117
|
-
this.
|
|
238
|
+
this._recordSkip('no active frontmost app');
|
|
118
239
|
return;
|
|
119
240
|
}
|
|
120
241
|
|
|
121
|
-
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
// Verify file exists
|
|
125
|
-
await fs.access(this.tempFilePath);
|
|
242
|
+
await this.execFile('screencapture', ['-x', this.tempFilePath]);
|
|
243
|
+
await this.fs.access(this.tempFilePath);
|
|
126
244
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
logger: () => {} // Silence verbose OCR logs
|
|
245
|
+
const { data } = await this.recognize(this.tempFilePath, 'eng+deu', {
|
|
246
|
+
logger: () => {},
|
|
130
247
|
});
|
|
248
|
+
const textContent = String(data?.text || '').trim();
|
|
131
249
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Only store if meaningful text was found
|
|
135
|
-
if (textContent.length > 5) {
|
|
136
|
-
// We need a user ID. For the local desktop agent, usually user 1 or we query the active user.
|
|
137
|
-
const userRow = db.prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get();
|
|
138
|
-
if (userRow) {
|
|
139
|
-
// Identify the active foreground app via AppleScript
|
|
140
|
-
let appName = frontmostApp || 'Unknown';
|
|
141
|
-
|
|
142
|
-
db.prepare(`
|
|
143
|
-
INSERT INTO screen_history (user_id, app_name, text_content)
|
|
144
|
-
VALUES (?, ?, ?)
|
|
145
|
-
`).run(userRow.id, appName, textContent);
|
|
146
|
-
}
|
|
250
|
+
if (!this.isRecording || textContent.length <= MINIMUM_TEXT_LENGTH) {
|
|
251
|
+
return;
|
|
147
252
|
}
|
|
148
253
|
|
|
254
|
+
this.db.prepare(`
|
|
255
|
+
INSERT INTO screen_history (user_id, app_name, text_content)
|
|
256
|
+
VALUES (?, ?, ?)
|
|
257
|
+
`).run(this.ownerUserId, frontmostApp, textContent);
|
|
258
|
+
this.lastSuccessAt = new Date(this.now()).toISOString();
|
|
259
|
+
this.lastSkipReason = null;
|
|
260
|
+
this.lastError = null;
|
|
149
261
|
} catch (err) {
|
|
150
|
-
|
|
151
|
-
if (this._isBenignCaptureError(errorMessage)) {
|
|
152
|
-
this._logBenignSkip(errorMessage);
|
|
153
|
-
} else {
|
|
154
|
-
console.error('[ScreenRecorder] Capture/OCR failed:', errorMessage);
|
|
155
|
-
}
|
|
262
|
+
this._recordError(err);
|
|
156
263
|
} finally {
|
|
157
|
-
// Always cleanup the screenshot image immediately
|
|
158
264
|
try {
|
|
159
|
-
await fs.unlink(this.tempFilePath);
|
|
160
|
-
} catch
|
|
161
|
-
//
|
|
265
|
+
await this.fs.unlink(this.tempFilePath);
|
|
266
|
+
} catch {
|
|
267
|
+
// The file is absent when capture is skipped or fails before creating it.
|
|
162
268
|
}
|
|
163
|
-
this.isProcessing = false;
|
|
164
269
|
}
|
|
165
270
|
}
|
|
166
271
|
|
|
167
272
|
cleanupOldRecords() {
|
|
168
273
|
try {
|
|
169
|
-
const result = db.prepare(`
|
|
170
|
-
DELETE FROM screen_history
|
|
171
|
-
WHERE timestamp < datetime('now',
|
|
172
|
-
`).run();
|
|
274
|
+
const result = this.db.prepare(`
|
|
275
|
+
DELETE FROM screen_history
|
|
276
|
+
WHERE timestamp < datetime('now', ?)
|
|
277
|
+
`).run(`-${this.retentionDays} days`);
|
|
173
278
|
if (result.changes > 0) {
|
|
174
279
|
console.log(`[ScreenRecorder] Purged ${result.changes} old screen history records.`);
|
|
175
280
|
}
|
|
@@ -179,4 +284,9 @@ class ScreenRecorder {
|
|
|
179
284
|
}
|
|
180
285
|
}
|
|
181
286
|
|
|
182
|
-
module.exports = {
|
|
287
|
+
module.exports = {
|
|
288
|
+
ScreenRecorder,
|
|
289
|
+
hasOpenConnectionForUser,
|
|
290
|
+
isExplicitlyEnabled,
|
|
291
|
+
parsePositiveInteger,
|
|
292
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CAPTURE_INTERVAL_MS = 10 * 1000;
|
|
4
|
+
const DEFAULT_RETENTION_DAYS = 7;
|
|
5
|
+
const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const MIN_CAPTURE_INTERVAL_MS = 1000;
|
|
7
|
+
const MINIMUM_TEXT_LENGTH = 5;
|
|
8
|
+
const FRONTMOST_APP_SCRIPT = 'tell application "System Events" to get name of first application process whose frontmost is true';
|
|
9
|
+
|
|
10
|
+
function isExplicitlyEnabled(value) {
|
|
11
|
+
return ['1', 'true', 'on', 'yes'].includes(String(value || '').trim().toLowerCase());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parsePositiveInteger(value, name, fallback, minimum = 1) {
|
|
15
|
+
if (value == null || String(value).trim() === '') {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum) {
|
|
21
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasOpenConnectionForUser(registry, userId) {
|
|
27
|
+
const connections = registry?.connectionsByUser?.get(String(userId));
|
|
28
|
+
if (connections instanceof Map) {
|
|
29
|
+
return Array.from(connections.values()).some(
|
|
30
|
+
(connection) => typeof connection?.isOpen === 'function' && connection.isOpen(),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return typeof connections?.isOpen === 'function' && connections.isOpen();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
CLEANUP_INTERVAL_MS,
|
|
38
|
+
DEFAULT_CAPTURE_INTERVAL_MS,
|
|
39
|
+
DEFAULT_RETENTION_DAYS,
|
|
40
|
+
FRONTMOST_APP_SCRIPT,
|
|
41
|
+
MINIMUM_TEXT_LENGTH,
|
|
42
|
+
MIN_CAPTURE_INTERVAL_MS,
|
|
43
|
+
hasOpenConnectionForUser,
|
|
44
|
+
isExplicitlyEnabled,
|
|
45
|
+
parsePositiveInteger,
|
|
46
|
+
};
|
|
@@ -129,6 +129,19 @@ async function buildAuthorizedClient(connection) {
|
|
|
129
129
|
credentials = {};
|
|
130
130
|
}
|
|
131
131
|
client.setCredentials(credentials);
|
|
132
|
+
// Capture any token refresh that happens during the request so the new
|
|
133
|
+
// access_token + expiry_date are always available on client.credentials.
|
|
134
|
+
// The library preserves refresh_token internally; this listener just ensures
|
|
135
|
+
// client.credentials stays in sync when refreshes happen inside deep call stacks.
|
|
136
|
+
client.on('tokens', (tokens) => {
|
|
137
|
+
if (tokens.access_token) {
|
|
138
|
+
client.setCredentials({
|
|
139
|
+
...client.credentials,
|
|
140
|
+
...tokens,
|
|
141
|
+
refresh_token: tokens.refresh_token || client.credentials.refresh_token,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
132
145
|
return client;
|
|
133
146
|
}
|
|
134
147
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HOME_ASSISTANT_PROVIDER_KEY = 'home_assistant';
|
|
4
|
+
|
|
5
|
+
const HOME_ASSISTANT_APP = {
|
|
6
|
+
id: 'home_assistant',
|
|
7
|
+
label: 'Home Assistant',
|
|
8
|
+
description: 'Connect one Home Assistant instance for states and service calls.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const HOME_ASSISTANT_TOOL_DEFINITIONS = [
|
|
12
|
+
{
|
|
13
|
+
appId: HOME_ASSISTANT_APP.id,
|
|
14
|
+
name: 'home_assistant_get_config',
|
|
15
|
+
access: 'read',
|
|
16
|
+
description: 'Get basic configuration for the connected Home Assistant instance.',
|
|
17
|
+
parameters: { type: 'object', properties: {} },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
appId: HOME_ASSISTANT_APP.id,
|
|
21
|
+
name: 'home_assistant_list_states',
|
|
22
|
+
access: 'read',
|
|
23
|
+
description: 'List Home Assistant entity states. Use entity_domain to narrow results such as light, sensor, switch, climate, or automation.',
|
|
24
|
+
parameters: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
entity_domain: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Optional entity domain prefix before the dot, for example light or sensor.',
|
|
30
|
+
},
|
|
31
|
+
limit: { type: 'number', description: 'Maximum number of states to return, default 100.' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
appId: HOME_ASSISTANT_APP.id,
|
|
37
|
+
name: 'home_assistant_get_state',
|
|
38
|
+
access: 'read',
|
|
39
|
+
description: 'Get one Home Assistant entity state by entity_id.',
|
|
40
|
+
parameters: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
entity_id: { type: 'string', description: 'Home Assistant entity ID, for example light.kitchen.' },
|
|
44
|
+
},
|
|
45
|
+
required: ['entity_id'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
appId: HOME_ASSISTANT_APP.id,
|
|
50
|
+
name: 'home_assistant_call_service',
|
|
51
|
+
access: 'write',
|
|
52
|
+
description: 'Call a Home Assistant service such as light.turn_on or script.turn_on.',
|
|
53
|
+
parameters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
domain: { type: 'string', description: 'Service domain, for example light, switch, climate, script, or automation.' },
|
|
57
|
+
service: { type: 'string', description: 'Service name, for example turn_on, turn_off, toggle, set_temperature, or reload.' },
|
|
58
|
+
service_data: { type: 'object', description: 'Optional Home Assistant service payload.' },
|
|
59
|
+
},
|
|
60
|
+
required: ['domain', 'service'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
appId: HOME_ASSISTANT_APP.id,
|
|
65
|
+
name: 'home_assistant_api_request',
|
|
66
|
+
access: 'dynamic_http_method',
|
|
67
|
+
description: 'Make an authenticated Home Assistant REST API request under /api for advanced operations.',
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
method: { type: 'string', description: 'HTTP method: GET, POST, PUT, PATCH, or DELETE.' },
|
|
72
|
+
path: { type: 'string', description: 'Path under the same Home Assistant origin. Must start with /api/.' },
|
|
73
|
+
query: { type: 'object', description: 'Optional query parameters.' },
|
|
74
|
+
body: { type: 'object', description: 'Optional JSON request body.' },
|
|
75
|
+
},
|
|
76
|
+
required: ['method', 'path'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const toolAppMap = new Map(HOME_ASSISTANT_TOOL_DEFINITIONS.map((tool) => [tool.name, tool.appId]));
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
HOME_ASSISTANT_APP,
|
|
85
|
+
HOME_ASSISTANT_PROVIDER_KEY,
|
|
86
|
+
HOME_ASSISTANT_TOOL_DEFINITIONS,
|
|
87
|
+
toolAppMap,
|
|
88
|
+
};
|