@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,408 @@
1
+ // ============================================
2
+ // STORAGE MODULE
3
+ // History persistence with encryption support
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+ import { UplinkEncryption } from './encryption.js';
8
+
9
+ const HISTORY_KEY = 'uplink-history';
10
+ const ENCRYPTED_KEY = 'uplink-history-encrypted';
11
+ const SETTINGS_KEY = 'uplink-settings';
12
+ const MAX_MESSAGES = 100;
13
+
14
+ // Queue for serializing concurrent saveMessage calls
15
+ let saveQueue = Promise.resolve();
16
+
17
+ // Get core state
18
+ function getCore() {
19
+ return UplinkCore.state || {};
20
+ }
21
+
22
+ // Get encryption module
23
+ function getCrypto() {
24
+ return UplinkEncryption;
25
+ }
26
+
27
+ // Save a message to history (queued to prevent concurrent write races)
28
+ async function saveMessage(msg) {
29
+ // Chain this save operation after any pending saves
30
+ saveQueue = saveQueue.then(async () => {
31
+ const core = getCore();
32
+ let history = await loadHistory();
33
+
34
+ // Don't store data URLs in history — they're too large for localStorage
35
+ // The server URL will replace it via updateLastImageUrl after upload completes
36
+ const safeMsg = { ...msg };
37
+ if (safeMsg.imageUrl && safeMsg.imageUrl.startsWith('data:')) {
38
+ safeMsg.imageUrl = '__pending_upload__';
39
+ }
40
+
41
+ history.push({
42
+ ...safeMsg,
43
+ timestamp: safeMsg.timestamp || Date.now()
44
+ });
45
+
46
+ // Limit history size
47
+ if (history.length > MAX_MESSAGES) {
48
+ history = history.slice(-MAX_MESSAGES);
49
+ }
50
+
51
+ await persistHistory(history);
52
+ }).catch(err => {
53
+ console.error('Storage: saveMessage error:', err);
54
+ });
55
+
56
+ return saveQueue;
57
+ }
58
+
59
+ // Load message history
60
+ async function loadHistory() {
61
+ const core = getCore();
62
+ const cryptoMod = getCrypto();
63
+
64
+ try {
65
+ let history = null;
66
+ if (core.encryptionEnabled && core.currentPassword && cryptoMod) {
67
+ const encrypted = localStorage.getItem(ENCRYPTED_KEY);
68
+ if (encrypted) {
69
+ history = await cryptoMod.decrypt(JSON.parse(encrypted), core.currentPassword);
70
+ }
71
+ } else {
72
+ const plain = localStorage.getItem(HISTORY_KEY);
73
+ if (plain) {
74
+ history = JSON.parse(plain);
75
+ }
76
+ }
77
+
78
+ if (history) {
79
+ // Sanitize: strip any data URLs that may have leaked into storage
80
+ // These bloat localStorage and cause quota errors
81
+ let sanitized = false;
82
+ for (const msg of history) {
83
+ if (msg.imageUrl && msg.imageUrl.startsWith('data:')) {
84
+ msg.imageUrl = '__pending_upload__';
85
+ sanitized = true;
86
+ }
87
+ }
88
+ if (sanitized) {
89
+ // Persist the cleaned history to free up localStorage space
90
+ // Use try/catch so sanitization failure doesn't lose loaded history
91
+ try {
92
+ await persistHistory(history);
93
+ } catch (sanitizeError) {
94
+ console.warn('Storage: Failed to persist sanitized history, continuing with in-memory version', sanitizeError);
95
+ }
96
+ }
97
+ return history;
98
+ }
99
+ } catch (loadError) {
100
+ console.error('Storage: Failed to load history', loadError);
101
+ }
102
+
103
+ return [];
104
+ }
105
+
106
+ // Persist history to storage
107
+ async function persistHistory(history) {
108
+ const core = getCore();
109
+ const cryptoMod = getCrypto();
110
+
111
+ try {
112
+ if (core.encryptionEnabled && core.currentPassword && cryptoMod) {
113
+ const encrypted = await cryptoMod.encrypt(history, core.currentPassword);
114
+ localStorage.setItem(ENCRYPTED_KEY, JSON.stringify(encrypted));
115
+ localStorage.removeItem(HISTORY_KEY); // Remove plain version
116
+ } else {
117
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
118
+ }
119
+ } catch (persistError) {
120
+ console.error('Storage: Failed to persist history', persistError);
121
+
122
+ // If quota exceeded, try to recover by stripping image URLs and retrying
123
+ if (persistError.name === 'QuotaExceededError' || persistError.code === 22) {
124
+ console.warn('Storage: Quota exceeded, stripping image URLs and retrying...');
125
+ for (const msg of history) {
126
+ if (msg.imageUrl && (msg.imageUrl.startsWith('data:') || msg.imageUrl === '__pending_upload__')) {
127
+ msg.imageUrl = null;
128
+ }
129
+ }
130
+ try {
131
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
132
+ console.log('Storage: Recovery successful after stripping image URLs');
133
+ } catch (retryError) {
134
+ console.error('Storage: Recovery failed, trimming history...', retryError);
135
+ // Last resort: keep only recent messages
136
+ const trimmed = history.slice(-50);
137
+ try {
138
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(trimmed));
139
+ console.log('Storage: Recovery successful after trimming to 50 messages');
140
+ } catch (finalError) {
141
+ console.error('Storage: All recovery attempts failed', finalError);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Clear history
149
+ function clearHistory() {
150
+ localStorage.removeItem(HISTORY_KEY);
151
+ localStorage.removeItem(ENCRYPTED_KEY);
152
+ }
153
+
154
+ // Migrate history between encrypted/unencrypted
155
+ async function migrateHistory(toEncrypted, newPassword) {
156
+ const history = await loadHistory();
157
+ const cryptoMod = getCrypto();
158
+
159
+ if (toEncrypted && newPassword && cryptoMod) {
160
+ const encrypted = await cryptoMod.encrypt(history, newPassword);
161
+ localStorage.setItem(ENCRYPTED_KEY, JSON.stringify(encrypted));
162
+ localStorage.removeItem(HISTORY_KEY);
163
+ } else {
164
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
165
+ localStorage.removeItem(ENCRYPTED_KEY);
166
+ if (cryptoMod) cryptoMod.clearEncryption();
167
+ }
168
+ }
169
+
170
+ // Save settings
171
+ function saveSettings(settings) {
172
+ try {
173
+ const existing = loadSettings();
174
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify({ ...existing, ...settings }));
175
+ } catch (saveError) {
176
+ console.error('Storage: Failed to save settings', saveError);
177
+ }
178
+ }
179
+
180
+ // Load settings
181
+ function loadSettings() {
182
+ try {
183
+ return JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
184
+ } catch (parseError) {
185
+ return {};
186
+ }
187
+ }
188
+
189
+ // ===========================================
190
+ // CROSS-DEVICE SYNC
191
+ // ===========================================
192
+
193
+ // Get auth headers for sync API calls
194
+ function getAuthHeaders() {
195
+ const settings = JSON.parse(localStorage.getItem('uplink-settings') || '{}');
196
+ const token = settings.gatewayToken || '';
197
+ const headers = { 'Content-Type': 'application/json' };
198
+ if (token) headers['Authorization'] = `Bearer ${token}`;
199
+ return headers;
200
+ }
201
+
202
+ // Generate sync ID from password (deterministic hash)
203
+ async function generateSyncId(password) {
204
+ const encoder = new TextEncoder();
205
+ const data = encoder.encode('uplink-sync:' + password);
206
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
207
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
208
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
209
+ }
210
+
211
+ // Push encrypted history to server
212
+ async function pushSync() {
213
+ const core = getCore();
214
+ const cryptoMod = getCrypto();
215
+
216
+ if (!core.encryptionEnabled || !core.currentPassword) {
217
+ throw new Error('Encryption must be enabled for sync');
218
+ }
219
+
220
+ const history = await loadHistory();
221
+ const satellites = localStorage.getItem('uplink-satellites');
222
+
223
+ // Bundle all sync data
224
+ const syncData = {
225
+ history,
226
+ satellites: satellites ? JSON.parse(satellites) : null,
227
+ settings: loadSettings(),
228
+ syncedAt: Date.now()
229
+ };
230
+
231
+ // Encrypt the bundle
232
+ const encrypted = await cryptoMod.encrypt(syncData, core.currentPassword);
233
+ const syncId = await generateSyncId(core.currentPassword);
234
+
235
+ const res = await fetch('/api/sync/push', {
236
+ method: 'POST',
237
+ headers: getAuthHeaders(),
238
+ body: JSON.stringify({
239
+ syncId,
240
+ encryptedData: encrypted,
241
+ timestamp: Date.now()
242
+ })
243
+ });
244
+
245
+ if (!res.ok) throw new Error('Sync push failed');
246
+ return await res.json();
247
+ }
248
+
249
+ // Pull encrypted history from server and decrypt
250
+ async function pullSync() {
251
+ const core = getCore();
252
+ const cryptoMod = getCrypto();
253
+
254
+ if (!core.encryptionEnabled || !core.currentPassword) {
255
+ throw new Error('Encryption must be enabled for sync');
256
+ }
257
+
258
+ const syncId = await generateSyncId(core.currentPassword);
259
+
260
+ const res = await fetch(`/api/sync/pull?syncId=${syncId}`, {
261
+ headers: getAuthHeaders(),
262
+ });
263
+ if (!res.ok) throw new Error('Sync pull failed');
264
+
265
+ const data = await res.json();
266
+ if (!data.encryptedData) {
267
+ return null; // No sync data found
268
+ }
269
+
270
+ // Decrypt the bundle
271
+ const syncData = await cryptoMod.decrypt(data.encryptedData, core.currentPassword);
272
+
273
+ return syncData;
274
+ }
275
+
276
+ // Check if sync data exists
277
+ async function checkSync() {
278
+ const core = getCore();
279
+
280
+ if (!core.encryptionEnabled || !core.currentPassword) {
281
+ return { exists: false, reason: 'encryption_disabled' };
282
+ }
283
+
284
+ const syncId = await generateSyncId(core.currentPassword);
285
+
286
+ const res = await fetch(`/api/sync/check?syncId=${syncId}`, {
287
+ headers: getAuthHeaders(),
288
+ });
289
+ if (!res.ok) return { exists: false, reason: 'check_failed' };
290
+
291
+ return await res.json();
292
+ }
293
+
294
+ // Apply sync data (merge or replace)
295
+ async function applySync(syncData, mode = 'replace') {
296
+ if (!syncData) return;
297
+
298
+ if (mode === 'replace') {
299
+ // Replace local data with synced data
300
+ if (syncData.history) {
301
+ await persistHistory(syncData.history);
302
+ }
303
+ if (syncData.satellites) {
304
+ localStorage.setItem('uplink-satellites', JSON.stringify(syncData.satellites));
305
+ }
306
+ if (syncData.settings) {
307
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(syncData.settings));
308
+ }
309
+ } else if (mode === 'merge') {
310
+ // Merge histories (dedupe by timestamp)
311
+ const local = await loadHistory();
312
+ const remote = syncData.history || [];
313
+ const merged = mergeHistories(local, remote);
314
+ await persistHistory(merged);
315
+
316
+ // For satellites and settings, prefer remote if newer
317
+ if (syncData.satellites && syncData.syncedAt > (loadSettings().lastSyncedAt || 0)) {
318
+ localStorage.setItem('uplink-satellites', JSON.stringify(syncData.satellites));
319
+ }
320
+ }
321
+
322
+ // Record last sync time
323
+ saveSettings({ lastSyncedAt: Date.now() });
324
+ }
325
+
326
+ // Merge two histories, deduping by timestamp
327
+ function mergeHistories(local, remote) {
328
+ const seen = new Set();
329
+ const merged = [];
330
+
331
+ [...local, ...remote]
332
+ .sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0))
333
+ .forEach(msg => {
334
+ const key = `${msg.timestamp}-${msg.type}-${(msg.text || '').substring(0, 50)}`;
335
+ if (!seen.has(key)) {
336
+ seen.add(key);
337
+ merged.push(msg);
338
+ }
339
+ });
340
+
341
+ return merged.slice(-MAX_MESSAGES);
342
+ }
343
+
344
+ /**
345
+ * Update the last user image message's URL from data URL to server URL
346
+ * Prevents localStorage bloat from storing full base64 images
347
+ */
348
+ async function updateLastImageUrl(serverUrl) {
349
+ saveQueue = saveQueue.then(async () => {
350
+ let history = await loadHistory();
351
+
352
+ // Find last user message with a pending or data URL image
353
+ for (let i = history.length - 1; i >= 0; i--) {
354
+ const url = history[i].imageUrl;
355
+ if (history[i].type === 'user' && url && (url === '__pending_upload__' || url.startsWith('data:'))) {
356
+ history[i].imageUrl = serverUrl;
357
+ await persistHistory(history);
358
+ break;
359
+ }
360
+ }
361
+ }).catch(err => {
362
+ console.error('Storage: updateLastImageUrl error:', err);
363
+ });
364
+
365
+ return saveQueue;
366
+ }
367
+
368
+ // Export API
369
+ export const UplinkStorage = {
370
+ saveMessage,
371
+ loadHistory,
372
+ clearHistory,
373
+ migrateHistory,
374
+ saveSettings,
375
+ loadSettings,
376
+ updateLastImageUrl,
377
+ // Sync functions
378
+ pushSync,
379
+ pullSync,
380
+ checkSync,
381
+ applySync,
382
+ generateSyncId,
383
+ MAX_MESSAGES
384
+ };
385
+
386
+ export {
387
+ saveMessage,
388
+ loadHistory,
389
+ clearHistory,
390
+ migrateHistory,
391
+ saveSettings,
392
+ loadSettings,
393
+ updateLastImageUrl,
394
+ pushSync,
395
+ pullSync,
396
+ checkSync,
397
+ applySync,
398
+ generateSyncId,
399
+ MAX_MESSAGES
400
+ };
401
+
402
+ // Backward compat: assign to window
403
+ window.UplinkStorage = UplinkStorage;
404
+
405
+ // Register with core
406
+ UplinkCore.registerModule('storage');
407
+
408
+ console.log('Storage: Loaded');