neoagent 3.2.1-beta.1 → 3.2.1-beta.10
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +11 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +390 -162
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +121 -5
- package/server/services/ai/loop/conversation_loop.js +510 -332
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +22 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const DESKTOP_COMPANION_WS_PATH = '/api/desktop/ws';
|
|
2
4
|
|
|
3
5
|
const DESKTOP_COMMANDS = Object.freeze({
|
|
@@ -17,6 +19,7 @@ const DESKTOP_COMMANDS = Object.freeze({
|
|
|
17
19
|
GET_TREE: 'getTree',
|
|
18
20
|
PAUSE_CONTROL: 'pauseControl',
|
|
19
21
|
EXECUTE_COMMAND: 'executeCommand',
|
|
22
|
+
CANCEL_COMMAND: 'cancelCommand',
|
|
20
23
|
PING: 'ping',
|
|
21
24
|
MOUSE_MOVE: 'mouseMove',
|
|
22
25
|
});
|
|
@@ -1,28 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const fs = require('fs');
|
|
2
4
|
const path = require('path');
|
|
3
5
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
6
|
+
const { writeBufferAtomic } = require('../../utils/files');
|
|
7
|
+
const { decodeBase64Image } = require('../../utils/image_payload');
|
|
4
8
|
const {
|
|
5
9
|
DESKTOP_COMMANDS,
|
|
6
|
-
DesktopCompanionSelectionError,
|
|
7
10
|
DesktopCompanionUnavailableError,
|
|
8
11
|
} = require('./protocol');
|
|
9
12
|
|
|
10
13
|
const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
11
14
|
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
12
15
|
|
|
13
|
-
function extractBase64Image(value) {
|
|
14
|
-
const text = String(value || '');
|
|
15
|
-
if (!text) return null;
|
|
16
|
-
const match = text.match(/^data:image\/(?:png|jpeg|jpg);base64,(.+)$/i);
|
|
17
|
-
return match ? match[1] : text;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function guessExtension(result = {}) {
|
|
21
|
-
const mime = String(result.contentType || result.mimeType || 'image/png').toLowerCase();
|
|
22
|
-
if (mime.includes('jpeg') || mime.includes('jpg')) return 'jpg';
|
|
23
|
-
return 'png';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
class DesktopProvider {
|
|
27
17
|
constructor(options = {}) {
|
|
28
18
|
this.registry = options.registry;
|
|
@@ -36,24 +26,21 @@ class DesktopProvider {
|
|
|
36
26
|
}
|
|
37
27
|
}
|
|
38
28
|
|
|
39
|
-
_writeScreenshotArtifact(
|
|
40
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
41
|
-
const extension = guessExtension(result);
|
|
42
|
-
const contentType = result.contentType || (extension === 'jpg' ? 'image/jpeg' : 'image/png');
|
|
29
|
+
async _writeScreenshotArtifact(image, result = {}, options = {}) {
|
|
43
30
|
if (this.artifactStore && this.userId != null) {
|
|
44
|
-
const artifact = this.artifactStore.
|
|
31
|
+
const artifact = await this.artifactStore.createBufferArtifact(this.userId, {
|
|
45
32
|
kind: 'desktop-screenshot',
|
|
46
33
|
backend: 'desktop-companion',
|
|
47
|
-
extension,
|
|
48
|
-
contentType,
|
|
34
|
+
extension: image.extension,
|
|
35
|
+
contentType: image.contentType,
|
|
49
36
|
filenameBase: 'desktop-companion-screenshot',
|
|
37
|
+
content: image.buffer,
|
|
38
|
+
signal: options.signal,
|
|
50
39
|
metadata: {
|
|
51
40
|
deviceId: result.device?.deviceId || null,
|
|
52
41
|
displayId: result.displayId || result.device?.activeDisplayId || null,
|
|
53
42
|
},
|
|
54
43
|
});
|
|
55
|
-
fs.writeFileSync(artifact.storagePath, buffer);
|
|
56
|
-
this.artifactStore.finalizeFile(artifact.artifactId, artifact.storagePath);
|
|
57
44
|
return {
|
|
58
45
|
screenshotPath: artifact.url,
|
|
59
46
|
artifactId: artifact.artifactId,
|
|
@@ -62,9 +49,9 @@ class DesktopProvider {
|
|
|
62
49
|
};
|
|
63
50
|
}
|
|
64
51
|
|
|
65
|
-
const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${extension}`;
|
|
52
|
+
const filename = `desktop_${Date.now()}_${Math.random().toString(16).slice(2)}.${image.extension}`;
|
|
66
53
|
const fullPath = path.join(SCREENSHOTS_DIR, filename);
|
|
67
|
-
|
|
54
|
+
await writeBufferAtomic(fullPath, image.buffer, { signal: options.signal });
|
|
68
55
|
return {
|
|
69
56
|
screenshotPath: `/screenshots/${filename}`,
|
|
70
57
|
artifactId: null,
|
|
@@ -73,13 +60,14 @@ class DesktopProvider {
|
|
|
73
60
|
};
|
|
74
61
|
}
|
|
75
62
|
|
|
76
|
-
_materialize(result) {
|
|
63
|
+
async _materialize(result, options = {}) {
|
|
77
64
|
if (!result || typeof result !== 'object') return result;
|
|
78
65
|
const raw = result.screenshotDataUrl || result.screenshotData || result.screenshotBase64;
|
|
79
66
|
if (!raw) return result;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
const image = decodeBase64Image(raw, {
|
|
68
|
+
allowedTypes: ['image/png', 'image/jpeg'],
|
|
69
|
+
});
|
|
70
|
+
const screenshot = await this._writeScreenshotArtifact(image, result, options);
|
|
83
71
|
const next = { ...result, ...screenshot };
|
|
84
72
|
delete next.screenshotDataUrl;
|
|
85
73
|
delete next.screenshotData;
|
|
@@ -89,16 +77,19 @@ class DesktopProvider {
|
|
|
89
77
|
|
|
90
78
|
async _dispatch(command, payload = {}, options = {}) {
|
|
91
79
|
this._assertReady();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
80
|
+
const safePayload = { ...(payload || {}) };
|
|
81
|
+
const signal = options.signal || safePayload.signal || null;
|
|
82
|
+
const timeoutMs = options.timeoutMs ?? safePayload.timeoutMs;
|
|
83
|
+
delete safePayload.signal;
|
|
84
|
+
delete safePayload.timeoutMs;
|
|
85
|
+
const result = await this.registry.dispatch(
|
|
86
|
+
this.userId,
|
|
87
|
+
safePayload.deviceId || null,
|
|
88
|
+
command,
|
|
89
|
+
safePayload,
|
|
90
|
+
{ ...options, signal, timeoutMs },
|
|
91
|
+
);
|
|
92
|
+
return this._materialize(result, { signal });
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
getStatus() {
|
|
@@ -121,9 +112,9 @@ class DesktopProvider {
|
|
|
121
112
|
return this.registry.revoke(this.userId, deviceId);
|
|
122
113
|
}
|
|
123
114
|
|
|
124
|
-
pauseDevice(deviceId, paused = true) {
|
|
115
|
+
pauseDevice(deviceId, paused = true, options = {}) {
|
|
125
116
|
this._assertReady();
|
|
126
|
-
return this.registry.pause(this.userId, deviceId, paused);
|
|
117
|
+
return this.registry.pause(this.userId, deviceId, paused, options);
|
|
127
118
|
}
|
|
128
119
|
|
|
129
120
|
screenshot(options = {}) {
|
|
@@ -137,7 +128,7 @@ class DesktopProvider {
|
|
|
137
128
|
|
|
138
129
|
stopStream(options = {}) {
|
|
139
130
|
this._assertReady();
|
|
140
|
-
return this.registry.stopStream(this.userId, options.deviceId || null);
|
|
131
|
+
return this.registry.stopStream(this.userId, options.deviceId || null, options);
|
|
141
132
|
}
|
|
142
133
|
|
|
143
134
|
observe(options = {}) {
|
|
@@ -192,6 +183,12 @@ class DesktopProvider {
|
|
|
192
183
|
stdin_input: options.stdinInput || null,
|
|
193
184
|
pty: options.pty === true,
|
|
194
185
|
inputs: options.inputs || [],
|
|
186
|
+
signal: options.signal,
|
|
187
|
+
}, {
|
|
188
|
+
signal: options.signal,
|
|
189
|
+
timeoutMs: Number(options.timeout || 0) > 0
|
|
190
|
+
? Number(options.timeout) + 10_000
|
|
191
|
+
: undefined,
|
|
195
192
|
});
|
|
196
193
|
}
|
|
197
194
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const crypto = require('crypto');
|
|
2
4
|
const db = require('../../db/database');
|
|
5
|
+
const { createAbortError } = require('../../utils/abort');
|
|
3
6
|
const {
|
|
4
7
|
DESKTOP_COMMANDS,
|
|
5
8
|
FRAME_TYPE_VIDEO,
|
|
@@ -66,6 +69,7 @@ class DesktopCompanionRegistry {
|
|
|
66
69
|
|| DEFAULT_PRESENCE_TOUCH_INTERVAL_MS,
|
|
67
70
|
);
|
|
68
71
|
this.connectionsByUser = new Map();
|
|
72
|
+
this.closed = false;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
_getUserMap(userId, create = false) {
|
|
@@ -199,6 +203,9 @@ class DesktopCompanionRegistry {
|
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
registerConnection({ userId, sessionId, ws, hello, remoteAddress = null, userAgent = null }) {
|
|
206
|
+
if (this.closed) {
|
|
207
|
+
throw new DesktopCompanionUnavailableError('Desktop companion registry is shutting down.');
|
|
208
|
+
}
|
|
202
209
|
const record = this._upsertDeviceRecord(userId, hello, sessionId);
|
|
203
210
|
const userMap = this._getUserMap(userId, true);
|
|
204
211
|
const existing = userMap.get(record.deviceId);
|
|
@@ -249,11 +256,15 @@ class DesktopCompanionRegistry {
|
|
|
249
256
|
// Only mark offline in the DB when this connection is still the active owner.
|
|
250
257
|
// If a newer connection has already taken over (reconnect race), its
|
|
251
258
|
// _upsertDeviceRecord already wrote status='online' and we must not clobber it.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
try {
|
|
260
|
+
this.db.prepare(
|
|
261
|
+
`UPDATE desktop_companion_devices
|
|
262
|
+
SET status = 'offline', updated_at = datetime('now')
|
|
263
|
+
WHERE user_id = ? AND device_id = ?`
|
|
264
|
+
).run(connection.userId, connection.deviceId);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.warn('[DesktopCompanion] Failed to record disconnect:', error?.message);
|
|
267
|
+
}
|
|
257
268
|
}
|
|
258
269
|
}
|
|
259
270
|
|
|
@@ -300,18 +311,22 @@ class DesktopCompanionRegistry {
|
|
|
300
311
|
const userMap = this._getUserMap(userId);
|
|
301
312
|
const connection = userMap?.get(String(deviceId));
|
|
302
313
|
if (!connection?.isOpen()) return;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
314
|
+
try {
|
|
315
|
+
this.db.prepare(
|
|
316
|
+
`UPDATE desktop_companion_devices
|
|
317
|
+
SET status = 'online',
|
|
318
|
+
last_seen_at = datetime('now'),
|
|
319
|
+
updated_at = datetime('now')
|
|
320
|
+
WHERE user_id = ? AND device_id = ? AND revoked_at IS NULL`
|
|
321
|
+
).run(userId, deviceId);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.warn('[DesktopCompanion] Failed to update presence:', error?.message);
|
|
324
|
+
}
|
|
310
325
|
}
|
|
311
326
|
|
|
312
327
|
isConnected(userId) {
|
|
313
328
|
const userMap = this._getUserMap(userId);
|
|
314
|
-
return userMap != null && userMap.
|
|
329
|
+
return userMap != null && Array.from(userMap.values()).some((connection) => connection.isOpen());
|
|
315
330
|
}
|
|
316
331
|
|
|
317
332
|
getConnection(userId, deviceId) {
|
|
@@ -394,15 +409,19 @@ class DesktopCompanionRegistry {
|
|
|
394
409
|
throw new DesktopCompanionUnavailableError();
|
|
395
410
|
}
|
|
396
411
|
const result = await connection.sendCommand(command, payload, options);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
412
|
+
try {
|
|
413
|
+
this.touchConnection(userId, device.deviceId, {
|
|
414
|
+
label: result?.device?.label,
|
|
415
|
+
paused: typeof result?.paused === 'boolean' ? result.paused : null,
|
|
416
|
+
activeDisplayId: result?.activeDisplayId || result?.device?.activeDisplayId,
|
|
417
|
+
permissions: result?.permissions,
|
|
418
|
+
capabilities: result?.capabilities,
|
|
419
|
+
displays: result?.displays || result?.device?.displays,
|
|
420
|
+
metadata: result?.device?.metadata,
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.warn('[DesktopCompanion] Failed to record command result:', error?.message);
|
|
424
|
+
}
|
|
406
425
|
return {
|
|
407
426
|
...result,
|
|
408
427
|
device: this.getDeviceRecordByDeviceId(userId, device.deviceId),
|
|
@@ -429,13 +448,13 @@ class DesktopCompanionRegistry {
|
|
|
429
448
|
};
|
|
430
449
|
}
|
|
431
450
|
|
|
432
|
-
async stopStream(userId, deviceId) {
|
|
451
|
+
async stopStream(userId, deviceId, options = {}) {
|
|
433
452
|
const device = this.resolveDevice(userId, deviceId);
|
|
434
453
|
const connection = this.getConnection(userId, device.deviceId);
|
|
435
454
|
if (!connection || !connection.isOpen()) {
|
|
436
455
|
throw new DesktopCompanionUnavailableError();
|
|
437
456
|
}
|
|
438
|
-
const result = await connection.sendCommand(DESKTOP_COMMANDS.STREAM_STOP, {});
|
|
457
|
+
const result = await connection.sendCommand(DESKTOP_COMMANDS.STREAM_STOP, {}, options);
|
|
439
458
|
connection._streaming = false;
|
|
440
459
|
return {
|
|
441
460
|
...result,
|
|
@@ -489,7 +508,7 @@ class DesktopCompanionRegistry {
|
|
|
489
508
|
return { success: true, deviceId: normalizedDeviceId };
|
|
490
509
|
}
|
|
491
510
|
|
|
492
|
-
pause(userId, deviceId, paused = true) {
|
|
511
|
+
async pause(userId, deviceId, paused = true, options = {}) {
|
|
493
512
|
const normalizedDeviceId = String(deviceId || '').trim();
|
|
494
513
|
if (!normalizedDeviceId) {
|
|
495
514
|
throw new DesktopCompanionUnavailableError();
|
|
@@ -500,27 +519,40 @@ class DesktopCompanionRegistry {
|
|
|
500
519
|
if (!existing) {
|
|
501
520
|
throw new DesktopCompanionUnavailableError();
|
|
502
521
|
}
|
|
503
|
-
this.db.prepare(
|
|
504
|
-
`UPDATE desktop_companion_devices
|
|
505
|
-
SET paused = ?, updated_at = datetime('now')
|
|
506
|
-
WHERE user_id = ? AND device_id = ?`
|
|
507
|
-
).run(paused ? 1 : 0, userId, normalizedDeviceId);
|
|
508
522
|
const connection = this.getConnection(userId, normalizedDeviceId);
|
|
509
|
-
if (connection) {
|
|
510
|
-
|
|
523
|
+
if (!connection?.isOpen()) {
|
|
524
|
+
throw new DesktopCompanionUnavailableError();
|
|
525
|
+
}
|
|
526
|
+
const result = await connection.sendCommand(
|
|
527
|
+
DESKTOP_COMMANDS.PAUSE_CONTROL,
|
|
528
|
+
{ paused },
|
|
529
|
+
options,
|
|
530
|
+
);
|
|
531
|
+
if (result?.success !== false) {
|
|
532
|
+
try {
|
|
533
|
+
this.db.prepare(
|
|
534
|
+
`UPDATE desktop_companion_devices
|
|
535
|
+
SET paused = ?, updated_at = datetime('now')
|
|
536
|
+
WHERE user_id = ? AND device_id = ?`
|
|
537
|
+
).run(paused ? 1 : 0, userId, normalizedDeviceId);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.warn('[DesktopCompanion] Failed to record pause state:', error?.message);
|
|
540
|
+
}
|
|
511
541
|
}
|
|
512
542
|
return {
|
|
513
|
-
|
|
543
|
+
...result,
|
|
544
|
+
success: result?.success !== false,
|
|
514
545
|
deviceId: normalizedDeviceId,
|
|
515
546
|
paused,
|
|
516
547
|
};
|
|
517
548
|
}
|
|
518
549
|
|
|
519
550
|
closeAll() {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
551
|
+
this.closed = true;
|
|
552
|
+
const connections = Array.from(this.connectionsByUser.values())
|
|
553
|
+
.flatMap((userMap) => Array.from(userMap.values()));
|
|
554
|
+
for (const connection of connections) {
|
|
555
|
+
connection.close('server shutdown');
|
|
524
556
|
}
|
|
525
557
|
this.connectionsByUser.clear();
|
|
526
558
|
}
|
|
@@ -555,24 +587,37 @@ class DesktopCompanionConnection {
|
|
|
555
587
|
this.lastPongAt = Date.now();
|
|
556
588
|
this.lastPresenceTouchAt = 0;
|
|
557
589
|
this.heartbeatTimer = null;
|
|
590
|
+
this.closed = false;
|
|
558
591
|
|
|
559
|
-
ws.on('message', (data) =>
|
|
592
|
+
ws.on('message', (data) => {
|
|
593
|
+
try {
|
|
594
|
+
this._handleMessage(data);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
try { ws.terminate(); } catch {}
|
|
597
|
+
this._closePending(error);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
560
600
|
ws.on('pong', () => {
|
|
561
601
|
this.lastPongAt = Date.now();
|
|
562
602
|
this.touchPresence();
|
|
563
603
|
});
|
|
564
604
|
ws.on('close', () => this._closePending(new DesktopCompanionUnavailableError('Desktop companion disconnected.')));
|
|
565
|
-
ws.on('error', (error) =>
|
|
605
|
+
ws.on('error', (error) => {
|
|
606
|
+
try { ws.terminate(); } catch {}
|
|
607
|
+
this._closePending(error);
|
|
608
|
+
});
|
|
566
609
|
this._startHeartbeat();
|
|
567
610
|
}
|
|
568
611
|
|
|
569
612
|
isOpen() {
|
|
570
|
-
return this.ws && this.ws.readyState === 1;
|
|
613
|
+
return !this.closed && this.ws && this.ws.readyState === 1;
|
|
571
614
|
}
|
|
572
615
|
|
|
573
616
|
close(reason) {
|
|
617
|
+
const wasOpen = this.isOpen();
|
|
618
|
+
this.closed = true;
|
|
574
619
|
try {
|
|
575
|
-
if (
|
|
620
|
+
if (wasOpen) {
|
|
576
621
|
this.ws.close(1000, String(reason || 'closing').slice(0, 120));
|
|
577
622
|
}
|
|
578
623
|
} catch {}
|
|
@@ -602,19 +647,51 @@ class DesktopCompanionConnection {
|
|
|
602
647
|
return Promise.reject(new DesktopCompanionUnavailableError());
|
|
603
648
|
}
|
|
604
649
|
const id = crypto.randomUUID();
|
|
605
|
-
const
|
|
650
|
+
const requestedTimeout = Number(options.timeoutMs || this.timeoutMs);
|
|
651
|
+
const timeoutMs = Number.isFinite(requestedTimeout)
|
|
652
|
+
? Math.min(30 * 60 * 1000, Math.max(100, requestedTimeout))
|
|
653
|
+
: DEFAULT_COMMAND_TIMEOUT_MS;
|
|
606
654
|
const message = createDesktopCommandMessage(id, command, payload);
|
|
607
655
|
return new Promise((resolve, reject) => {
|
|
608
|
-
const
|
|
656
|
+
const signal = options.signal || null;
|
|
657
|
+
const sendCancellation = () => {
|
|
658
|
+
if (!this.isOpen() || command === DESKTOP_COMMANDS.CANCEL_COMMAND) return;
|
|
659
|
+
try {
|
|
660
|
+
this.ws.send(JSON.stringify(createDesktopCommandMessage(
|
|
661
|
+
crypto.randomUUID(),
|
|
662
|
+
DESKTOP_COMMANDS.CANCEL_COMMAND,
|
|
663
|
+
{ commandId: id },
|
|
664
|
+
)));
|
|
665
|
+
} catch {}
|
|
666
|
+
};
|
|
667
|
+
const cleanup = () => {
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
signal?.removeEventListener('abort', onAbort);
|
|
609
670
|
this.pending.delete(id);
|
|
610
|
-
|
|
671
|
+
};
|
|
672
|
+
const onAbort = () => {
|
|
673
|
+
cleanup();
|
|
674
|
+
sendCancellation();
|
|
675
|
+
reject(createAbortError(signal, `Desktop companion command aborted: ${command}`));
|
|
676
|
+
};
|
|
677
|
+
const timer = setTimeout(() => {
|
|
678
|
+
cleanup();
|
|
679
|
+
sendCancellation();
|
|
680
|
+
const error = new Error(`Desktop companion command timed out: ${command}`);
|
|
681
|
+
error.code = 'DESKTOP_COMPANION_COMMAND_TIMEOUT';
|
|
682
|
+
reject(error);
|
|
611
683
|
}, timeoutMs);
|
|
612
|
-
|
|
684
|
+
timer.unref?.();
|
|
685
|
+
if (signal?.aborted) {
|
|
686
|
+
onAbort();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
690
|
+
this.pending.set(id, { resolve, reject, timer, command, signal, onAbort });
|
|
613
691
|
try {
|
|
614
692
|
this.ws.send(JSON.stringify(message));
|
|
615
693
|
} catch (error) {
|
|
616
|
-
|
|
617
|
-
this.pending.delete(id);
|
|
694
|
+
cleanup();
|
|
618
695
|
reject(error);
|
|
619
696
|
}
|
|
620
697
|
});
|
|
@@ -636,10 +713,14 @@ class DesktopCompanionConnection {
|
|
|
636
713
|
|
|
637
714
|
if (message.type === 'event') {
|
|
638
715
|
if (message.event === 'statusChanged' || message.event === 'permissionsChanged') {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
716
|
+
try {
|
|
717
|
+
this.registry.touchConnection(this.userId, this.deviceId, {
|
|
718
|
+
...message.payload,
|
|
719
|
+
metadata: message.payload?.metadata,
|
|
720
|
+
});
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.warn('[DesktopCompanion] Failed to record device event:', error?.message);
|
|
723
|
+
}
|
|
643
724
|
}
|
|
644
725
|
return;
|
|
645
726
|
}
|
|
@@ -648,6 +729,7 @@ class DesktopCompanionConnection {
|
|
|
648
729
|
const pending = this.pending.get(message.id);
|
|
649
730
|
if (!pending) return;
|
|
650
731
|
clearTimeout(pending.timer);
|
|
732
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
651
733
|
this.pending.delete(message.id);
|
|
652
734
|
if (message.ok === false) {
|
|
653
735
|
const error = new Error(String(message.error || `Desktop companion command failed: ${pending.command}`));
|
|
@@ -655,16 +737,18 @@ class DesktopCompanionConnection {
|
|
|
655
737
|
pending.reject(error);
|
|
656
738
|
return;
|
|
657
739
|
}
|
|
658
|
-
pending.resolve(message.payload
|
|
740
|
+
pending.resolve(message.payload ?? {});
|
|
659
741
|
}
|
|
660
742
|
|
|
661
743
|
_closePending(error) {
|
|
744
|
+
this.closed = true;
|
|
662
745
|
if (this.heartbeatTimer) {
|
|
663
746
|
clearInterval(this.heartbeatTimer);
|
|
664
747
|
this.heartbeatTimer = null;
|
|
665
748
|
}
|
|
666
749
|
for (const pending of this.pending.values()) {
|
|
667
750
|
clearTimeout(pending.timer);
|
|
751
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
668
752
|
pending.reject(error);
|
|
669
753
|
}
|
|
670
754
|
this.pending.clear();
|
|
@@ -702,5 +786,6 @@ class DesktopCompanionConnection {
|
|
|
702
786
|
}
|
|
703
787
|
|
|
704
788
|
module.exports = {
|
|
789
|
+
DesktopCompanionConnection,
|
|
705
790
|
DesktopCompanionRegistry,
|
|
706
791
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const BITWARDEN_PROVIDER_KEY = 'bitwarden';
|
|
4
|
+
|
|
5
|
+
const BITWARDEN_APP = {
|
|
6
|
+
id: 'password_manager',
|
|
7
|
+
label: 'Password manager',
|
|
8
|
+
description: 'Use selected Bitwarden items without exposing their secret values to the AI.',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
BITWARDEN_APP,
|
|
13
|
+
BITWARDEN_PROVIDER_KEY,
|
|
14
|
+
};
|