@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,307 @@
1
+ /**
2
+ * File Routes - General file upload with text extraction
3
+ * Supports: PDF, DOCX, XLSX/CSV, plain text, code files
4
+ * Extracted text is saved alongside originals for agent access via `read` tool
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { randomUUID } from 'crypto';
10
+ import { fileTypeFromBuffer } from 'file-type';
11
+ import { badRequest, internalError, ErrorCodes } from '../../utils/errors.js';
12
+
13
+ // Lazy-load heavy parsers (only when needed)
14
+ let pdfParse = null;
15
+ let mammoth = null;
16
+ let ExcelJS = null;
17
+
18
+ async function loadPdfParse() {
19
+ if (!pdfParse) {
20
+ const mod = await import('pdf-parse');
21
+ pdfParse = mod.PDFParse || mod.default || mod;
22
+ }
23
+ return pdfParse;
24
+ }
25
+
26
+ async function loadMammoth() {
27
+ if (!mammoth) {
28
+ const mod = await import('mammoth');
29
+ mammoth = mod.default || mod;
30
+ }
31
+ return mammoth;
32
+ }
33
+
34
+ async function loadExcelJS() {
35
+ if (!ExcelJS) {
36
+ const mod = await import('exceljs');
37
+ ExcelJS = mod.default || mod;
38
+ }
39
+ return ExcelJS;
40
+ }
41
+
42
+ import { MAX_FILE_EXTRACT_SIZE, FILE_EXTRACTION_TIMEOUT_MS, FILE_CLEANUP_DELAY_MS } from '../config.js';
43
+
44
+ // Max text extraction size (prevent memory bombs)
45
+ const MAX_EXTRACT_SIZE = MAX_FILE_EXTRACT_SIZE;
46
+ const EXTRACTION_TIMEOUT_MS = FILE_EXTRACTION_TIMEOUT_MS;
47
+
48
+ // File categories for routing
49
+ const TEXT_EXTENSIONS = new Set([
50
+ '.txt', '.md', '.json', '.js', '.ts', '.jsx', '.tsx', '.py', '.rb',
51
+ '.html', '.css', '.scss', '.less', '.xml', '.yaml', '.yml', '.toml',
52
+ '.sh', '.bat', '.ps1', '.sql', '.csv', '.env', '.gitignore', '.log',
53
+ '.ini', '.cfg', '.conf', '.dockerfile', '.makefile', '.rs', '.go',
54
+ '.java', '.kt', '.c', '.cpp', '.h', '.hpp', '.cs', '.swift', '.r',
55
+ '.lua', '.php', '.pl', '.ex', '.exs', '.erl', '.hs', '.ml',
56
+ ]);
57
+
58
+ const EXTRACTABLE_TYPES = {
59
+ 'application/pdf': 'pdf',
60
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
61
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
62
+ 'application/vnd.ms-excel': 'xls',
63
+ };
64
+
65
+ /**
66
+ * Run a function with a timeout
67
+ */
68
+ function withTimeout(fn, ms) {
69
+ return Promise.race([
70
+ fn(),
71
+ new Promise((_, reject) =>
72
+ setTimeout(() => reject(new Error(`Extraction timed out after ${ms}ms`)), ms)
73
+ ),
74
+ ]);
75
+ }
76
+
77
+ /**
78
+ * Extract text from a PDF buffer (pdf-parse v2 API)
79
+ */
80
+ async function extractPdf(buffer) {
81
+ const PDFParse = await loadPdfParse();
82
+ const parser = new PDFParse({ data: buffer });
83
+ const result = await parser.getText();
84
+ const text = result?.text || '';
85
+ await parser.destroy();
86
+ return text.slice(0, MAX_EXTRACT_SIZE);
87
+ }
88
+
89
+ /**
90
+ * Extract text from a DOCX buffer
91
+ */
92
+ async function extractDocx(buffer) {
93
+ const mam = await loadMammoth();
94
+ const result = await mam.extractRawText({ buffer });
95
+ return result.value?.slice(0, MAX_EXTRACT_SIZE) || '';
96
+ }
97
+
98
+ /**
99
+ * Extract text from an XLSX/XLS buffer
100
+ */
101
+ async function extractSpreadsheet(buffer) {
102
+ const EJ = await loadExcelJS();
103
+ const workbook = new EJ.Workbook();
104
+ await workbook.xlsx.load(buffer);
105
+ const sheets = [];
106
+
107
+ workbook.eachSheet((sheet) => {
108
+ const rows = [];
109
+ sheet.eachRow((row) => {
110
+ const values = [];
111
+ row.eachCell({ includeEmpty: true }, (cell) => {
112
+ const val = cell.text ?? cell.value ?? '';
113
+ values.push(String(val).replace(/,/g, ' '));
114
+ });
115
+ rows.push(values.join(','));
116
+ });
117
+ sheets.push(`## Sheet: ${sheet.name}\n${rows.join('\n')}`);
118
+
119
+ // Check total size
120
+ const totalSize = sheets.join('\n\n').length;
121
+ if (totalSize > MAX_EXTRACT_SIZE) return;
122
+ });
123
+
124
+ return sheets.join('\n\n').slice(0, MAX_EXTRACT_SIZE);
125
+ }
126
+
127
+ /**
128
+ * Determine if a file is plain text based on extension
129
+ */
130
+ function isTextByExtension(filename) {
131
+ const ext = path.extname(filename).toLowerCase();
132
+ return TEXT_EXTENSIONS.has(ext);
133
+ }
134
+
135
+ /**
136
+ * Setup file routes
137
+ * @param {Express} app - Express app instance
138
+ * @param {Object} deps - Dependencies
139
+ */
140
+ export function setupFileRoutes(app, context) {
141
+ const {
142
+ fileUpload,
143
+ fetch: fetchWithTimeout,
144
+ log,
145
+ config,
146
+ uploadsDir,
147
+ } = context;
148
+
149
+ const { GATEWAY_URL, GATEWAY_TOKEN, SESSION_USER, REQUEST_TIMEOUT } = config;
150
+
151
+ app.post('/api/file', (req, res) => {
152
+ fileUpload(req, res, async (err) => {
153
+ if (err) {
154
+ log('error', '[File] Upload error:', err);
155
+ const errorDetail = process.env.NODE_ENV === 'production'
156
+ ? 'File upload failed'
157
+ : err.message;
158
+ return badRequest(res, 'Upload failed: ' + errorDetail, ErrorCodes.UPLOAD_FAILED);
159
+ }
160
+
161
+ try {
162
+ if (!req.file) {
163
+ return badRequest(res, 'No file provided', ErrorCodes.MISSING_FIELD);
164
+ }
165
+
166
+ const { originalname, path: tempPath, size } = req.file;
167
+ const caption = req.body?.caption?.slice(0, 500) || '';
168
+ const satelliteId = String(req.body?.satelliteId || 'main').replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 32);
169
+
170
+ log('info', `[File] Received: ${originalname} (${(size / 1024).toFixed(1)} KB)`);
171
+
172
+ // Read the uploaded file
173
+ const fileBuffer = await fs.readFile(tempPath);
174
+
175
+ // Detect actual file type via magic bytes
176
+ const detectedType = await fileTypeFromBuffer(fileBuffer);
177
+ const mimeType = detectedType?.mime || 'application/octet-stream';
178
+ const ext = path.extname(originalname).toLowerCase() || (detectedType?.ext ? `.${detectedType.ext}` : '');
179
+
180
+ // Generate unique filename preserving original extension
181
+ const fileId = randomUUID();
182
+ const safeOrigName = originalname.replace(/[^a-zA-Z0-9._-]/g, '_').substring(0, 100);
183
+ const savedFilename = `file-${fileId}${ext}`;
184
+ const savedPath = path.join(uploadsDir, savedFilename);
185
+
186
+ // Save original file
187
+ await fs.mkdir(uploadsDir, { recursive: true });
188
+ await fs.writeFile(savedPath, fileBuffer);
189
+
190
+ // Clean up temp file
191
+ await fs.unlink(tempPath).catch(() => {});
192
+
193
+ // Attempt text extraction
194
+ let extractedText = null;
195
+ let extractedFilename = null;
196
+ let extractionMethod = 'none';
197
+
198
+ try {
199
+ if (isTextByExtension(originalname)) {
200
+ // Plain text — read directly
201
+ extractedText = fileBuffer.toString('utf-8').slice(0, MAX_EXTRACT_SIZE);
202
+ extractionMethod = 'text';
203
+ } else if (EXTRACTABLE_TYPES[mimeType] === 'pdf') {
204
+ extractedText = await withTimeout(() => extractPdf(fileBuffer), EXTRACTION_TIMEOUT_MS);
205
+ extractionMethod = 'pdf-parse';
206
+ } else if (EXTRACTABLE_TYPES[mimeType] === 'docx') {
207
+ extractedText = await withTimeout(() => extractDocx(fileBuffer), EXTRACTION_TIMEOUT_MS);
208
+ extractionMethod = 'mammoth';
209
+ } else if (EXTRACTABLE_TYPES[mimeType] === 'xlsx' || EXTRACTABLE_TYPES[mimeType] === 'xls') {
210
+ extractedText = await withTimeout(() => extractSpreadsheet(fileBuffer), EXTRACTION_TIMEOUT_MS);
211
+ extractionMethod = 'exceljs';
212
+ }
213
+ } catch (extractErr) {
214
+ log('warn', `[File] Text extraction failed for ${originalname}: ${extractErr.message}`);
215
+ // Continue without extracted text — agent can still see the file exists
216
+ }
217
+
218
+ // Save extracted text alongside original
219
+ if (extractedText && extractedText.trim().length > 0) {
220
+ extractedFilename = `file-${fileId}.extracted.txt`;
221
+ const extractedPath = path.join(uploadsDir, extractedFilename);
222
+ const header = `# Extracted from: ${safeOrigName}\n# Type: ${mimeType}\n# Method: ${extractionMethod}\n# Size: ${(size / 1024).toFixed(1)} KB\n---\n\n`;
223
+ await fs.writeFile(extractedPath, header + extractedText, 'utf-8');
224
+ }
225
+
226
+ // Build the message to send to the agent
227
+ const filePath = `uplink/uploads/${savedFilename}`;
228
+ const extractedPath = extractedFilename ? `uplink/uploads/${extractedFilename}` : null;
229
+
230
+ let prompt;
231
+ if (extractedPath) {
232
+ prompt = caption
233
+ ? `[File uploaded: ${safeOrigName}] [Original at: ${filePath}] [Extracted text at: ${extractedPath} — read this file for the content] ${caption}`
234
+ : `[File uploaded: ${safeOrigName}] [Original at: ${filePath}] [Extracted text at: ${extractedPath} — read this file for the content] The user sent a file. Read the extracted text and summarize or respond to its contents.`;
235
+ } else {
236
+ prompt = caption
237
+ ? `[File uploaded: ${safeOrigName}] [Saved at: ${filePath}] [Type: ${mimeType}, Size: ${(size / 1024).toFixed(1)} KB] ${caption}`
238
+ : `[File uploaded: ${safeOrigName}] [Saved at: ${filePath}] [Type: ${mimeType}, Size: ${(size / 1024).toFixed(1)} KB] The user sent a file. Acknowledge receipt and describe what you can do with it.`;
239
+ }
240
+
241
+ // SSE response (same pattern as image upload)
242
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
243
+ res.setHeader('Cache-Control', 'no-cache');
244
+ res.setHeader('Connection', 'keep-alive');
245
+ res.setHeader('X-Accel-Buffering', 'no');
246
+ res.flushHeaders();
247
+
248
+ const keepalive = setInterval(() => {
249
+ res.write(': keepalive\n\n');
250
+ }, FILE_CLEANUP_DELAY_MS);
251
+
252
+ let reply = 'I received your file.';
253
+
254
+ try {
255
+ // Derive session key
256
+ const sessionKey = satelliteId === 'main'
257
+ ? 'agent:main:main'
258
+ : `agent:main:uplink:satellite:${satelliteId}`;
259
+
260
+ const response = await fetchWithTimeout(`${GATEWAY_URL}/v1/chat/completions`, {
261
+ method: 'POST',
262
+ headers: {
263
+ 'Content-Type': 'application/json',
264
+ 'Authorization': `Bearer ${GATEWAY_TOKEN}`,
265
+ 'x-openclaw-session-key': sessionKey,
266
+ },
267
+ body: JSON.stringify({
268
+ model: 'openclaw',
269
+ user: SESSION_USER,
270
+ messages: [{ role: 'user', content: prompt }],
271
+ }),
272
+ }, REQUEST_TIMEOUT);
273
+
274
+ if (!response.ok) {
275
+ const text = await response.text();
276
+ throw new Error(`Chat API error: ${response.status} - ${text}`);
277
+ }
278
+
279
+ const data = await response.json();
280
+ reply = data.choices?.[0]?.message?.content || reply;
281
+ } finally {
282
+ clearInterval(keepalive);
283
+ }
284
+
285
+ // Send result
286
+ res.write(`data: ${JSON.stringify({
287
+ response: reply,
288
+ filename: safeOrigName,
289
+ filePath,
290
+ extractedPath,
291
+ extractionMethod,
292
+ })}\n\n`);
293
+ res.end();
294
+
295
+ log('info', `[File] Processed: ${safeOrigName} (extraction: ${extractionMethod})`);
296
+ } catch (error) {
297
+ log('error', '[File] Error:', error);
298
+ if (res.headersSent) {
299
+ res.write(`data: ${JSON.stringify({ error: true, message: 'File processing failed' })}\n\n`);
300
+ res.end();
301
+ } else {
302
+ internalError(res, 'File processing failed', ErrorCodes.INTERNAL_ERROR);
303
+ }
304
+ }
305
+ });
306
+ });
307
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Routes Index - Export all route modules
3
+ */
4
+
5
+ export { setupVoiceRoutes } from './voice.js';
6
+ export { setupChatRoutes } from './chat.js';
7
+ export { setupMediaRoutes } from './media.js';
8
+ export { setupWebhookRoutes } from './webhooks.js';
9
+ export { setupConfigRoutes } from './config.js';
10
+ export { setupStatusRoutes } from './status.js';
11
+ export { setupSatelliteRoutes } from './satellite.js';
12
+ export { setupMissedMessagesRoutes, addMissedMessage } from './missed-messages.js';
13
+ export { setupPushRoutes, sendPushNotification } from './push.js';
14
+ export { setupSTTRoutes } from './stt.js';
15
+ export { setupFileRoutes } from './files.js';
16
+ export { setupAgentRoutes } from './agents.js';
17
+ export { setupPremiumRoutes } from './premium.js';
18
+ export { setupArtifactsRoutes } from './artifacts.js';