@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,203 @@
1
+ /**
2
+ * Webhook Routes - External integrations and OpenClaw push
3
+ */
4
+
5
+ import crypto from 'crypto';
6
+ import { badRequest, unauthorized, internalError, ErrorCodes } from '../../utils/errors.js';
7
+
8
+ /**
9
+ * Setup webhook routes
10
+ * @param {Express} app - Express app instance
11
+ * @param {Object} context - Request context
12
+ */
13
+ export function setupWebhookRoutes(app, context) {
14
+ const {
15
+ verifyWebhookToken,
16
+ fetch: fetchWithTimeout,
17
+ broadcastOpenClawPush,
18
+ broadcastToAll,
19
+ wsClients,
20
+ log,
21
+ config,
22
+ } = context;
23
+
24
+ const { GATEWAY_URL, GATEWAY_TOKEN, OPENCLAW_CALLBACK_SECRET, REQUEST_TIMEOUT } = config;
25
+
26
+ // ===========================================
27
+ // OpenClaw Channel Push
28
+ // ===========================================
29
+
30
+ app.post('/api/openclaw/push', async (req, res) => {
31
+ const headerSecret = req.get('X-Uplink-Secret') || '';
32
+
33
+ // Validate callback secret using timing-safe comparison
34
+ let isValid = false;
35
+ if (OPENCLAW_CALLBACK_SECRET && headerSecret) {
36
+ const providedBuf = Buffer.from(headerSecret);
37
+ const expectedBuf = Buffer.from(OPENCLAW_CALLBACK_SECRET);
38
+ if (providedBuf.length === expectedBuf.length) {
39
+ isValid = crypto.timingSafeEqual(providedBuf, expectedBuf);
40
+ }
41
+ }
42
+
43
+ if (!isValid) {
44
+ log('warn', '[OpenClaw Push] Unauthorized request - invalid or missing secret');
45
+ return unauthorized(res, 'Invalid or missing secret');
46
+ }
47
+
48
+ const payload = req.body || {};
49
+ const allowedTypes = new Set(['message', 'chunk', 'tool', 'complete', 'error']);
50
+ const hasValidType = allowedTypes.has(payload.type);
51
+ const hasSatelliteId = typeof payload.satelliteId === 'string' && payload.satelliteId.length > 0;
52
+ const hasTimestamp = typeof payload.timestamp === 'number';
53
+
54
+ if (!hasValidType || !hasSatelliteId || !hasTimestamp) {
55
+ log('warn', `[OpenClaw Push] Invalid payload: type=${payload.type}, satelliteId=${payload.satelliteId}`);
56
+ return badRequest(res, 'Invalid payload: type, satelliteId, and timestamp required', ErrorCodes.VALIDATION_ERROR);
57
+ }
58
+
59
+ // Broadcast to WebSocket clients
60
+ broadcastOpenClawPush(payload);
61
+
62
+ // Check if any WebSocket clients received the message
63
+ const wsDelivered = wsClients.size > 0;
64
+
65
+ if (!wsDelivered && payload.type === 'message' && payload.message) {
66
+ // No WebSocket clients connected
67
+
68
+ // 1. Queue message for when they open Uplink
69
+ const { addMissedMessage } = await import('./missed-messages.js');
70
+ await addMissedMessage({
71
+ type: payload.type,
72
+ message: payload.message,
73
+ satelliteId: payload.satelliteId,
74
+ author: 'Assistant'
75
+ });
76
+ log('debug', `[OpenClaw Push] Queued missed message for satellite ${payload.satelliteId}`);
77
+
78
+ // 2. Send push notification to wake them up
79
+ try {
80
+ const { sendPushNotification } = await import('./push.js');
81
+ const pushResult = await sendPushNotification('default', {
82
+ title: 'Uplink',
83
+ body: payload.message.length > 100 ? payload.message.substring(0, 100) + '...' : payload.message,
84
+ url: '/',
85
+ satelliteId: payload.satelliteId,
86
+ tag: 'uplink-message'
87
+ });
88
+ if (pushResult.ok) {
89
+ log('debug', `[OpenClaw Push] Sent push notification for satellite ${payload.satelliteId}`);
90
+ }
91
+ } catch (pushErr) {
92
+ log('debug', `[OpenClaw Push] Push notification failed: ${pushErr.message}`);
93
+ }
94
+ }
95
+
96
+ log('debug', `[OpenClaw Push] Broadcasted ${payload.type} for satellite ${payload.satelliteId} (WS: ${wsDelivered})`);
97
+
98
+ return res.json({ ok: true });
99
+ });
100
+
101
+ // ===========================================
102
+ // Webhooks
103
+ // ===========================================
104
+
105
+ app.post('/api/webhook/message', verifyWebhookToken, async (req, res) => {
106
+ try {
107
+ const { message, source = 'webhook', metadata = {} } = req.body;
108
+
109
+ if (!message) {
110
+ return badRequest(res, 'message required', ErrorCodes.MISSING_FIELD);
111
+ }
112
+
113
+ // Validate message length (security)
114
+ if (message.length > config.MAX_INPUT_LENGTHS.MESSAGE) {
115
+ return badRequest(res, `message exceeds maximum length of ${config.MAX_INPUT_LENGTHS.MESSAGE} characters`, ErrorCodes.VALIDATION_ERROR);
116
+ }
117
+
118
+ log('info', `[Webhook] Message from ${source}: "${message.substring(0, 50)}..."`);
119
+
120
+ const formattedMessage = `[External: ${source}] ${message}`;
121
+
122
+ const chatResponse = await fetchWithTimeout(`${GATEWAY_URL}/v1/chat/completions`, {
123
+ method: 'POST',
124
+ headers: {
125
+ 'Content-Type': 'application/json',
126
+ 'Authorization': `Bearer ${GATEWAY_TOKEN}`
127
+ },
128
+ body: JSON.stringify({
129
+ model: 'openclaw',
130
+ user: 'webhook',
131
+ messages: [{ role: 'user', content: formattedMessage }]
132
+ })
133
+ }, REQUEST_TIMEOUT);
134
+
135
+ if (!chatResponse.ok) {
136
+ throw new Error(`Gateway error: ${chatResponse.status}`);
137
+ }
138
+
139
+ const data = await chatResponse.json();
140
+ const response = data.choices?.[0]?.message?.content || 'No response';
141
+
142
+ broadcastToAll(JSON.stringify({
143
+ type: 'webhook_message',
144
+ source,
145
+ message,
146
+ response,
147
+ metadata,
148
+ timestamp: Date.now()
149
+ }));
150
+
151
+ res.json({ ok: true, response });
152
+ } catch (e) {
153
+ log('error', '[Webhook] Message error:', e.message);
154
+ internalError(res, e.message, ErrorCodes.GATEWAY_ERROR);
155
+ }
156
+ });
157
+
158
+ app.post('/api/webhook/notify', verifyWebhookToken, async (req, res) => {
159
+ try {
160
+ const { title, body, type = 'info', data = {} } = req.body;
161
+
162
+ if (!title && !body) {
163
+ return badRequest(res, 'title or body required', ErrorCodes.MISSING_FIELD);
164
+ }
165
+
166
+ log('info', `[Webhook] Notification: ${title || body}`);
167
+
168
+ broadcastToAll(JSON.stringify({
169
+ type: 'notification',
170
+ notification: { title, body, type, data, timestamp: Date.now() }
171
+ }));
172
+
173
+ res.json({ ok: true, delivered: wsClients.size });
174
+ } catch (e) {
175
+ log('error', '[Webhook] Notify error:', e.message);
176
+ internalError(res, e.message, ErrorCodes.INTERNAL_ERROR);
177
+ }
178
+ });
179
+
180
+ app.post('/api/webhook/trigger', verifyWebhookToken, async (req, res) => {
181
+ try {
182
+ const { action, params = {} } = req.body;
183
+
184
+ if (!action) {
185
+ return badRequest(res, 'action required', ErrorCodes.MISSING_FIELD);
186
+ }
187
+
188
+ log('info', `[Webhook] Trigger: ${action}`);
189
+
190
+ broadcastToAll(JSON.stringify({
191
+ type: 'trigger',
192
+ action,
193
+ params,
194
+ timestamp: Date.now()
195
+ }));
196
+
197
+ res.json({ ok: true, action, delivered: wsClients.size });
198
+ } catch (e) {
199
+ log('error', '[Webhook] Trigger error:', e.message);
200
+ internalError(res, e.message, ErrorCodes.INTERNAL_ERROR);
201
+ }
202
+ });
203
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Routes Module - API endpoints orchestrator
3
+ *
4
+ * This module coordinates all route handlers by importing modular route files
5
+ * and passing shared dependencies to each.
6
+ */
7
+
8
+ import multer from 'multer';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ import { log, fetchWithTimeout } from './utils.js';
14
+ import { transcribe, chat, chatWithParallelTTS, generateTTS } from './chat.js';
15
+ import { isPrivateIP, verifyWebhookToken, strictLimiter } from './middleware.js';
16
+ import { broadcastOpenClawPush, broadcastToAll, wsClients } from './websocket/index.js';
17
+ import { sendMessage } from './channel.js';
18
+ import { getClientConfig, saveConfig, needsOnboarding, loadConfig } from './runtime-config.js';
19
+ import { createRequestContext } from './context.js';
20
+ import {
21
+ GATEWAY_URL, GATEWAY_TOKEN,
22
+ ALLOWED_IMAGE_TYPES, ALLOWED_AUDIO_TYPES,
23
+ ROOT_DIR, ACTIVITY_FILE, MESSAGES_FILE,
24
+ MAX_ACTIVITY_ITEMS, MAX_SYNC_MESSAGES,
25
+ WAKE_WORD, TTS_VOICE_NAME, SESSION_USER,
26
+ OPENCLAW_CALLBACK_SECRET, REQUEST_TIMEOUT
27
+ } from './config.js';
28
+
29
+ // Route modules
30
+ import {
31
+ setupVoiceRoutes,
32
+ setupChatRoutes,
33
+ setupMediaRoutes,
34
+ setupWebhookRoutes,
35
+ setupConfigRoutes,
36
+ setupStatusRoutes,
37
+ setupSatelliteRoutes,
38
+ setupMissedMessagesRoutes,
39
+ addMissedMessage,
40
+ setupPushRoutes,
41
+ sendPushNotification,
42
+ setupSTTRoutes,
43
+ setupFileRoutes,
44
+ setupAgentRoutes,
45
+ setupPremiumRoutes,
46
+ setupArtifactsRoutes,
47
+ } from './routes/index.js';
48
+ import { initPremium } from './premium/index.js';
49
+
50
+ // ===========================================
51
+ // Directory Setup
52
+ // ===========================================
53
+
54
+ const TEMP_DIR = path.join(os.tmpdir(), 'voice-chat');
55
+ const UPLOADS_DIR = path.join(ROOT_DIR, 'uploads');
56
+
57
+ // ===========================================
58
+ // Multer configurations
59
+ // ===========================================
60
+
61
+ const audioUpload = multer({
62
+ dest: TEMP_DIR,
63
+ limits: { fileSize: 25 * 1024 * 1024 }
64
+ }).single('audio');
65
+
66
+ const imageUpload = multer({
67
+ dest: path.join(TEMP_DIR, 'images'),
68
+ limits: { fileSize: 10 * 1024 * 1024 }
69
+ }).single('image');
70
+
71
+ const videoUpload = multer({
72
+ dest: path.join(TEMP_DIR, 'videos'),
73
+ limits: { fileSize: 50 * 1024 * 1024 }
74
+ }).single('video');
75
+
76
+ const fileUpload = multer({
77
+ dest: path.join(TEMP_DIR, 'files'),
78
+ limits: { fileSize: 10 * 1024 * 1024 }
79
+ }).single('file');
80
+
81
+ // ===========================================
82
+ // Message Sync Helper (sequential write queue to prevent race conditions)
83
+ // ===========================================
84
+
85
+ let syncWriteQueue = Promise.resolve();
86
+
87
+ export async function saveMessageToSync(role, content, timestamp = new Date().toISOString()) {
88
+ syncWriteQueue = syncWriteQueue.then(async () => {
89
+ try {
90
+ let messages = [];
91
+ try {
92
+ const fileContent = await fs.readFile(MESSAGES_FILE, 'utf8');
93
+ messages = JSON.parse(fileContent);
94
+ } catch (readError) { /* File may not exist yet */ }
95
+
96
+ messages.push({
97
+ id: Date.now(),
98
+ role,
99
+ content,
100
+ timestamp
101
+ });
102
+
103
+ if (messages.length > MAX_SYNC_MESSAGES) {
104
+ messages = messages.slice(-MAX_SYNC_MESSAGES);
105
+ }
106
+
107
+ await fs.writeFile(MESSAGES_FILE, JSON.stringify(messages, null, 2));
108
+ } catch (err) {
109
+ log('error', 'Failed to save message to sync:', err.message);
110
+ }
111
+ }).catch(err => {
112
+ log('error', 'Sync write queue error:', err.message);
113
+ });
114
+ return syncWriteQueue;
115
+ }
116
+
117
+ // ===========================================
118
+ // Setup Routes - Main Orchestrator
119
+ // ===========================================
120
+
121
+ export function setupRoutes(app, requestHelpers) {
122
+ // Shared configuration object
123
+ const config = {
124
+ GATEWAY_URL,
125
+ GATEWAY_TOKEN,
126
+ ALLOWED_IMAGE_TYPES,
127
+ ALLOWED_AUDIO_TYPES,
128
+ ACTIVITY_FILE,
129
+ MESSAGES_FILE,
130
+ MAX_ACTIVITY_ITEMS,
131
+ MAX_SYNC_MESSAGES,
132
+ WAKE_WORD,
133
+ TTS_VOICE_NAME,
134
+ SESSION_USER,
135
+ OPENCLAW_CALLBACK_SECRET,
136
+ REQUEST_TIMEOUT,
137
+ };
138
+
139
+ // Create standardized request context for all route modules
140
+ const context = createRequestContext({
141
+ // Configuration
142
+ config,
143
+
144
+ // Core utilities
145
+ log,
146
+ fetchWithTimeout,
147
+
148
+ // Chat functions
149
+ transcribe,
150
+ chat,
151
+ chatWithParallelTTS,
152
+ generateTTS,
153
+ sendMessage,
154
+ saveMessageToSync,
155
+
156
+ // WebSocket broadcasting
157
+ broadcastOpenClawPush,
158
+ broadcastToAll,
159
+ wsClients,
160
+
161
+ // Middleware
162
+ verifyWebhookToken,
163
+ strictLimiter,
164
+ isPrivateIP,
165
+
166
+ // Config management
167
+ loadConfig,
168
+ getClientConfig,
169
+ saveConfig,
170
+ needsOnboarding,
171
+
172
+ // File uploads
173
+ audioUpload,
174
+ imageUpload,
175
+ videoUpload,
176
+ fileUpload,
177
+
178
+ // Directories
179
+ uploadsDir: UPLOADS_DIR,
180
+ tempDir: TEMP_DIR,
181
+
182
+ // Request tracking
183
+ requestHelpers,
184
+ });
185
+
186
+ // Initialize premium from stored license key
187
+ initPremium();
188
+
189
+ // Initialize all route modules with standardized context
190
+ setupPremiumRoutes(app, context);
191
+ setupStatusRoutes(app, context);
192
+ setupSatelliteRoutes(app, context);
193
+ setupVoiceRoutes(app, context);
194
+ setupChatRoutes(app, context);
195
+ setupMediaRoutes(app, context);
196
+ setupWebhookRoutes(app, context);
197
+ setupConfigRoutes(app, context);
198
+ setupMissedMessagesRoutes(app, context);
199
+ setupPushRoutes(app, context);
200
+ setupSTTRoutes(app, context);
201
+ setupFileRoutes(app, context);
202
+ setupAgentRoutes(app, context);
203
+ setupArtifactsRoutes(app, context);
204
+
205
+ log('info', 'Routes initialized: premium, status, satellite, voice, chat, media, webhooks, config, missed-messages, push, stt, files, agents, artifacts');
206
+ }