@mooncompany/uplink-chat 0.5.0

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.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * WebSocket Routing Module
3
+ * Message type dispatch, chat handler, abort handling
4
+ */
5
+
6
+ import { log } from '../utils.js';
7
+ import { sendMessage } from '../channel.js';
8
+ import { SESSION_USER, WEBSOCKET } from '../config.js';
9
+ import { validateSetUser, isAuthEnabled } from '../middleware/auth.js';
10
+ import {
11
+ wsClients,
12
+ activeStreamingRequests,
13
+ safeSend,
14
+ trimStreamingRequests,
15
+ WS_CONNECTION_LIMITS,
16
+ } from './connections.js';
17
+ import {
18
+ generateMessageId,
19
+ broadcastSyncMessage,
20
+ broadcastSyncThinking,
21
+ broadcastSyncTool,
22
+ sendToClient,
23
+ } from './broadcast.js';
24
+ import {
25
+ broadcastSyncDelta,
26
+ cleanupSyncDeltaThrottle,
27
+ } from './sync.js';
28
+
29
+ // ============================================
30
+ // Chat Handler
31
+ // ============================================
32
+
33
+ async function handleWebSocketChat(clientId, payload, requestHelpers, saveMessageToSync) {
34
+ const client = wsClients.get(clientId);
35
+ if (!client) return;
36
+
37
+ const { message, mode = 'text', requestId } = payload;
38
+ const { canAcceptRequest, startRequest, endRequest, textToSpeech } = requestHelpers;
39
+
40
+ if (!message) {
41
+ safeSend(client.ws, {
42
+ type: 'error',
43
+ requestId,
44
+ error: 'No message provided'
45
+ });
46
+ return;
47
+ }
48
+
49
+ if (!canAcceptRequest()) {
50
+ safeSend(client.ws, {
51
+ type: 'error',
52
+ requestId,
53
+ error: 'Server busy. Please wait a moment.'
54
+ });
55
+ return;
56
+ }
57
+
58
+ const trackingId = startRequest('ws-chat');
59
+ const startTime = Date.now();
60
+
61
+ const abortController = new AbortController();
62
+
63
+ // Ensure streaming requests map doesn't grow unbounded
64
+ trimStreamingRequests();
65
+
66
+ activeStreamingRequests.set(requestId, { abortController, clientId, startedAt: Date.now() });
67
+
68
+ log('debug', `[WS Chat] "${message.substring(0, 50)}..." from ${clientId}`);
69
+
70
+ // Extract satellite ID from session user (format: SESSION_USER or SESSION_USER:satelliteId)
71
+ // Must be declared before any usage to avoid TDZ error in strict mode
72
+ const sessionParts = (client.sessionUser || SESSION_USER).split(':');
73
+ const satelliteId = sessionParts[1] || 'main';
74
+
75
+ await saveMessageToSync('user', message);
76
+
77
+ // Generate message ID for sync
78
+ const userMessageId = generateMessageId();
79
+
80
+ // Broadcast user message to other clients for sync
81
+ broadcastSyncMessage('user', message, satelliteId, userMessageId, clientId);
82
+
83
+ safeSend(client.ws, {
84
+ type: 'chat_start',
85
+ requestId,
86
+ timestamp: new Date().toISOString()
87
+ });
88
+
89
+ // Generate a request ID for correlating sync stream deltas with final message
90
+ const syncRequestId = generateMessageId();
91
+
92
+ try {
93
+ let fullResponse = '';
94
+ let detectedTools = [];
95
+
96
+ // Use unified sendMessage (routes through channel if enabled)
97
+ const result = await sendMessage({
98
+ message,
99
+ satelliteId,
100
+ mode,
101
+ requestId,
102
+ signal: abortController.signal,
103
+ onThinking: () => {
104
+ sendToClient(clientId, {
105
+ type: 'chat_thinking',
106
+ requestId,
107
+ timestamp: Date.now()
108
+ });
109
+ broadcastSyncThinking(syncRequestId, satelliteId, clientId);
110
+ },
111
+ onChunk: (content) => {
112
+ fullResponse += content;
113
+ sendToClient(clientId, {
114
+ type: 'chat_chunk',
115
+ requestId,
116
+ content,
117
+ timestamp: Date.now()
118
+ });
119
+ broadcastSyncDelta(syncRequestId, content, satelliteId, clientId);
120
+ },
121
+ onTool: (tool) => {
122
+ if (!detectedTools.includes(tool)) {
123
+ detectedTools.push(tool);
124
+ sendToClient(clientId, {
125
+ type: 'tool_used',
126
+ requestId,
127
+ tool,
128
+ timestamp: Date.now()
129
+ });
130
+ }
131
+ broadcastSyncTool(syncRequestId, tool, satelliteId, clientId);
132
+ },
133
+ });
134
+
135
+ const elapsed = Date.now() - startTime;
136
+
137
+ if (result.response) {
138
+ await saveMessageToSync('assistant', result.response);
139
+
140
+ // Flush remaining deltas before sending final sync message
141
+ cleanupSyncDeltaThrottle(syncRequestId);
142
+
143
+ // Broadcast assistant response to other clients for sync (with requestId for stream finalization)
144
+ const assistantMessageId = generateMessageId();
145
+ broadcastSyncMessage('assistant', result.response, satelliteId, assistantMessageId, clientId, syncRequestId);
146
+ }
147
+
148
+ let audioUrl = null;
149
+ if (mode === 'voice' && result.response && textToSpeech) {
150
+ try {
151
+ audioUrl = await textToSpeech(result.response);
152
+ } catch (e) {
153
+ log('error', '[WS Chat] TTS failed:', e.message);
154
+ }
155
+ }
156
+
157
+ sendToClient(clientId, {
158
+ type: 'chat_complete',
159
+ requestId,
160
+ response: result.response,
161
+ audioUrl,
162
+ elapsed,
163
+ usage: result.usage,
164
+ tools: result.tools,
165
+ timestamp: Date.now()
166
+ });
167
+
168
+ log('debug', `[WS Chat] Complete in ${elapsed}ms, tokens: ${result.usage?.total_tokens || '?'}${result.usage?.estimated ? ' (est)' : ''}`);
169
+
170
+ } catch (err) {
171
+ cleanupSyncDeltaThrottle(syncRequestId);
172
+ if (err.name === 'AbortError') {
173
+ sendToClient(clientId, {
174
+ type: 'chat_aborted',
175
+ requestId,
176
+ response: '',
177
+ timestamp: Date.now()
178
+ });
179
+ log('info', `[WS Chat] Aborted by user`);
180
+ } else {
181
+ log('error', `[WS Chat] Error:`, err.message);
182
+ sendToClient(clientId, {
183
+ type: 'chat_error',
184
+ requestId,
185
+ error: err.message,
186
+ timestamp: Date.now()
187
+ });
188
+ }
189
+ } finally {
190
+ cleanupSyncDeltaThrottle(syncRequestId);
191
+ activeStreamingRequests.delete(requestId);
192
+ endRequest(trackingId);
193
+ }
194
+ }
195
+
196
+ // ============================================
197
+ // Message Dispatcher
198
+ // ============================================
199
+
200
+ /**
201
+ * Handle an incoming WebSocket message, dispatching by type
202
+ */
203
+ export async function handleWebSocketMessage(clientId, message, requestHelpers, saveMessageToSync) {
204
+ const client = wsClients.get(clientId);
205
+ if (!client) return;
206
+
207
+ const { type, ...payload } = message;
208
+ const { isProcessing, activeRequests, MAX_CONCURRENT_REQUESTS } = requestHelpers;
209
+
210
+ switch (type) {
211
+ case 'ping':
212
+ client.lastPing = Date.now();
213
+ safeSend(client.ws, { type: 'pong', timestamp: Date.now() });
214
+ break;
215
+
216
+ case 'status':
217
+ safeSend(client.ws, {
218
+ type: 'status',
219
+ data: {
220
+ processing: isProcessing(),
221
+ activeRequests: activeRequests.size,
222
+ maxConcurrent: MAX_CONCURRENT_REQUESTS,
223
+ connectedClients: wsClients.size,
224
+ maxConnections: WS_CONNECTION_LIMITS.maxTotal,
225
+ maxPerIp: WS_CONNECTION_LIMITS.maxPerIp,
226
+ timestamp: new Date().toISOString()
227
+ }
228
+ });
229
+ break;
230
+
231
+ case 'chat':
232
+ await handleWebSocketChat(clientId, payload, requestHelpers, saveMessageToSync);
233
+ break;
234
+
235
+ case 'abort':
236
+ if (payload.requestId) {
237
+ const activeReq = activeStreamingRequests.get(payload.requestId);
238
+ if (activeReq && activeReq.clientId === clientId) {
239
+ log('info', `[WS] Aborting request ${payload.requestId}`);
240
+ activeReq.abortController.abort();
241
+ activeStreamingRequests.delete(payload.requestId);
242
+ safeSend(client.ws, {
243
+ type: 'chat_aborted',
244
+ requestId: payload.requestId,
245
+ timestamp: Date.now()
246
+ });
247
+ }
248
+ }
249
+ break;
250
+
251
+ case 'set_user':
252
+ if (payload.user) {
253
+ // M-39 (H-02): Validate set_user when auth is enabled to prevent impersonation
254
+ if (!validateSetUser(payload.user, client.sessionUser)) {
255
+ log('warn', `[WS] set_user rejected for ${clientId}: cannot impersonate ${payload.user}`);
256
+ safeSend(client.ws, {
257
+ type: 'error',
258
+ message: 'Authentication required to change user identity'
259
+ });
260
+ break;
261
+ }
262
+
263
+ client.sessionUser = payload.user;
264
+ safeSend(client.ws, {
265
+ type: 'user_set',
266
+ user: payload.user
267
+ });
268
+ }
269
+ break;
270
+
271
+ default:
272
+ safeSend(client.ws, {
273
+ type: 'error',
274
+ error: `Unknown message type: ${type}`
275
+ });
276
+ }
277
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * WebSocket Sync Module
3
+ * Sync delta throttling, cross-device streaming state
4
+ */
5
+
6
+ import { log } from '../utils.js';
7
+ import { WEBSOCKET } from '../config.js';
8
+ import { broadcastToProxyClients } from '../gateway-proxy.js';
9
+ import { broadcast } from './broadcast.js';
10
+
11
+ // ============================================
12
+ // Sync Delta Throttling
13
+ // ============================================
14
+
15
+ /** Sync delta throttles: requestId -> { lastSent, pendingContent, timer, satelliteId, excludeClientId } */
16
+ const syncDeltaThrottles = new Map();
17
+ const SYNC_DELTA_THROTTLE_MS = WEBSOCKET.syncDeltaThrottleMs;
18
+
19
+ /**
20
+ * Flush accumulated sync delta content to all clients
21
+ */
22
+ function flushSyncDelta(requestId) {
23
+ const state = syncDeltaThrottles.get(requestId);
24
+ if (!state || !state.pendingContent) return;
25
+
26
+ const message = {
27
+ type: 'event',
28
+ event: 'sync.delta',
29
+ payload: {
30
+ requestId,
31
+ content: state.pendingContent,
32
+ satelliteId: state.satelliteId || 'main',
33
+ },
34
+ };
35
+ broadcast(message, state.excludeClientId);
36
+ broadcastToProxyClients(message);
37
+
38
+ state.pendingContent = '';
39
+ state.lastSent = Date.now();
40
+ if (state.timer) {
41
+ clearTimeout(state.timer);
42
+ state.timer = null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Broadcast sync.delta event with throttling
48
+ * Accumulates content and flushes at most every SYNC_DELTA_THROTTLE_MS
49
+ */
50
+ export function broadcastSyncDelta(requestId, content, satelliteId, excludeClientId = null) {
51
+ let state = syncDeltaThrottles.get(requestId);
52
+ if (!state) {
53
+ state = { lastSent: 0, pendingContent: '', timer: null, satelliteId, excludeClientId };
54
+ syncDeltaThrottles.set(requestId, state);
55
+ }
56
+
57
+ state.pendingContent += content;
58
+
59
+ const elapsed = Date.now() - state.lastSent;
60
+ if (elapsed >= SYNC_DELTA_THROTTLE_MS) {
61
+ // Enough time has passed, flush immediately
62
+ flushSyncDelta(requestId);
63
+ } else if (!state.timer) {
64
+ // Schedule a flush for the remaining time
65
+ const remaining = SYNC_DELTA_THROTTLE_MS - elapsed;
66
+ state.timer = setTimeout(() => {
67
+ state.timer = null;
68
+ flushSyncDelta(requestId);
69
+ }, remaining);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Cleanup sync delta throttle state for a completed request
75
+ * Flushes any remaining content and removes the entry
76
+ */
77
+ export function cleanupSyncDeltaThrottle(requestId) {
78
+ const state = syncDeltaThrottles.get(requestId);
79
+ if (!state) return;
80
+
81
+ // Flush remaining content
82
+ if (state.pendingContent) {
83
+ flushSyncDelta(requestId);
84
+ }
85
+ if (state.timer) {
86
+ clearTimeout(state.timer);
87
+ }
88
+ syncDeltaThrottles.delete(requestId);
89
+ }
90
+
91
+ /**
92
+ * Clean up sync delta throttles for a specific client (called on disconnect)
93
+ * @param {string} clientId - The client ID to clean up throttles for
94
+ */
95
+ export function cleanupSyncForClient(clientId) {
96
+ for (const [reqId, state] of syncDeltaThrottles) {
97
+ if (state.excludeClientId === clientId) {
98
+ if (state.timer) clearTimeout(state.timer);
99
+ syncDeltaThrottles.delete(reqId);
100
+ }
101
+ }
102
+ }