@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,538 @@
1
+ // ============================================
2
+ // GATEWAY CHAT MODULE
3
+ // Handles Gateway WebSocket events with streaming fix
4
+ // Subscribes to agent events for deltas, chat events for final state
5
+ // ============================================
6
+
7
+ import { UplinkCore } from './core.js';
8
+
9
+ // WebSocket reference (from connection.js)
10
+ let ws = null;
11
+
12
+ // Message listeners for external consumers
13
+ let messageListeners = [];
14
+
15
+ // Track streaming message state
16
+ let currentStreamingMessage = null;
17
+ let currentMessageId = null;
18
+
19
+ // History state
20
+ let historyLoaded = false;
21
+
22
+ // Gateway API endpoint
23
+ const GATEWAY_API_BASE = '/api/gateway';
24
+
25
+ // Module init retry state
26
+ let initRetryCount = 0;
27
+ const MAX_INIT_RETRIES = 10;
28
+
29
+ /**
30
+ * Initialize the gateway chat module
31
+ * Sets up WebSocket listeners using the shared connection
32
+ */
33
+ function init() {
34
+ // Wait for connection module to be ready
35
+ if (!window.UplinkConnection) {
36
+ if (initRetryCount >= MAX_INIT_RETRIES) {
37
+ logger.error('GatewayChat: UplinkConnection not found after max retries, giving up');
38
+ return;
39
+ }
40
+ initRetryCount++;
41
+ const delay = Math.min(100 * Math.pow(2, initRetryCount), 5000);
42
+ logger.debug(`GatewayChat: Waiting for UplinkConnection (${initRetryCount}/${MAX_INIT_RETRIES})...`);
43
+ setTimeout(init, delay);
44
+ return;
45
+ }
46
+
47
+ // Subscribe to connection events
48
+ window.UplinkConnection.onConnection((event, data) => {
49
+ if (event === 'connected') {
50
+ onConnected();
51
+ } else if (event === 'disconnected') {
52
+ onDisconnected();
53
+ } else if (event === 'message') {
54
+ handleWebSocketMessage(data);
55
+ }
56
+ });
57
+
58
+ // Get initial WebSocket if already connected
59
+ ws = window.UplinkConnection.getWebSocket();
60
+ if (ws && window.UplinkConnection.isConnected()) {
61
+ onConnected();
62
+ }
63
+
64
+ logger.debug('GatewayChat: Initialized');
65
+ }
66
+
67
+ /**
68
+ * Called when WebSocket connects
69
+ */
70
+ function onConnected() {
71
+ ws = window.UplinkConnection.getWebSocket();
72
+ logger.debug('GatewayChat: Connected to Gateway');
73
+
74
+ // Load history on connect if not already loaded
75
+ if (!historyLoaded) {
76
+ loadHistory();
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Called when WebSocket disconnects
82
+ */
83
+ function onDisconnected() {
84
+ ws = null;
85
+ logger.debug('GatewayChat: Disconnected from Gateway');
86
+
87
+ // Finalize any streaming message
88
+ if (currentStreamingMessage) {
89
+ finalizeStreamingMessage();
90
+ }
91
+ }
92
+
93
+ // Track malformed message count for rate limiting error display
94
+ let malformedMessageCount = 0;
95
+ let lastMalformedErrorShown = 0;
96
+ const MALFORMED_ERROR_COOLDOWN_MS = 10000; // Only show error every 10 seconds
97
+
98
+ /**
99
+ * Handle incoming WebSocket messages
100
+ * @param {string} data - Raw message data
101
+ */
102
+ function handleWebSocketMessage(data) {
103
+ try {
104
+ const message = JSON.parse(data);
105
+
106
+ // Reset malformed count on successful parse
107
+ malformedMessageCount = 0;
108
+
109
+ // Handle agent events - streaming deltas
110
+ // Gateway sends streaming deltas on agent events at payload.data.delta
111
+ if (message.type === 'agent' || message.event === 'agent') {
112
+ handleAgentEvent(message);
113
+ return;
114
+ }
115
+
116
+ // Handle chat events - final message state
117
+ if (message.type === 'chat' || message.event === 'chat') {
118
+ handleChatEvent(message);
119
+ return;
120
+ }
121
+
122
+ // Handle pong/heartbeat
123
+ if (message.type === 'pong') {
124
+ return;
125
+ }
126
+
127
+ // Notify listeners of other message types
128
+ notifyListeners('message', message);
129
+
130
+ } catch (e) {
131
+ // Log malformed message with details for debugging
132
+ malformedMessageCount++;
133
+ const truncatedData = data && data.length > 200 ? data.substring(0, 200) + '...' : data;
134
+ logger.warn('GatewayChat: Malformed message received', {
135
+ error: e.message,
136
+ dataPreview: truncatedData,
137
+ dataLength: data ? data.length : 0,
138
+ count: malformedMessageCount
139
+ });
140
+
141
+ // Notify listeners about the malformed message
142
+ notifyListeners('malformed-message', {
143
+ error: e.message,
144
+ dataPreview: truncatedData,
145
+ count: malformedMessageCount
146
+ });
147
+
148
+ // Show user-friendly error if we're getting repeated malformed messages
149
+ // This suggests a critical protocol issue
150
+ const now = Date.now();
151
+ if (malformedMessageCount >= 5 && (now - lastMalformedErrorShown) > MALFORMED_ERROR_COOLDOWN_MS) {
152
+ lastMalformedErrorShown = now;
153
+ showMalformedMessageError();
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Show user-friendly error for repeated malformed messages
160
+ */
161
+ function showMalformedMessageError() {
162
+ const errorMsg = 'Communication issue detected. Some messages may not display correctly.';
163
+
164
+ // Try to use UplinkChat to show the error
165
+ if (window.UplinkChat?.addMessage) {
166
+ window.UplinkChat.addMessage(errorMsg, 'system', null, false);
167
+ } else if (window.addMessage) {
168
+ window.addMessage(errorMsg, 'system', null, false);
169
+ }
170
+
171
+ // Also notify listeners
172
+ notifyListeners('critical-error', {
173
+ type: 'malformed-messages',
174
+ message: errorMsg,
175
+ count: malformedMessageCount
176
+ });
177
+
178
+ logger.error('GatewayChat: Critical - repeated malformed messages detected', {
179
+ count: malformedMessageCount
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Handle agent events - streaming deltas from Gateway
185
+ * @param {object} message - The agent event message
186
+ */
187
+ function handleAgentEvent(message) {
188
+ // Extract delta from payload.data.delta (per Gateway spec)
189
+ const delta = message.payload?.data?.delta ||
190
+ message.data?.delta ||
191
+ message.delta;
192
+
193
+ if (!delta) {
194
+ // No delta in this agent event, might be status update
195
+ if (message.payload?.status || message.status) {
196
+ const status = message.payload?.status || message.status;
197
+ notifyListeners('status', { status, type: 'agent' });
198
+ }
199
+ return;
200
+ }
201
+
202
+ // Check if this is tool output that should be filtered
203
+ if (isToolOutput(delta)) {
204
+ // Route to activity panel instead of chat
205
+ routeToActivityPanel(delta, 'tool');
206
+ return;
207
+ }
208
+
209
+ // Start or continue streaming message
210
+ if (!currentStreamingMessage) {
211
+ currentStreamingMessage = delta;
212
+ currentMessageId = message.messageId || message.id || generateId();
213
+
214
+ notifyListeners('stream-start', {
215
+ messageId: currentMessageId,
216
+ content: delta
217
+ });
218
+ } else {
219
+ currentStreamingMessage += delta;
220
+
221
+ notifyListeners('stream-delta', {
222
+ messageId: currentMessageId,
223
+ delta: delta,
224
+ content: currentStreamingMessage
225
+ });
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Handle chat events - final message state from Gateway
231
+ * @param {object} message - The chat event message
232
+ */
233
+ function handleChatEvent(message) {
234
+ const content = message.payload?.content ||
235
+ message.payload?.text ||
236
+ message.content ||
237
+ message.text;
238
+
239
+ const messageId = message.messageId || message.id || generateId();
240
+
241
+ // Finalize any ongoing streaming
242
+ if (currentStreamingMessage) {
243
+ finalizeStreamingMessage();
244
+ }
245
+
246
+ // Handle tool output filtering for final messages too
247
+ if (content && isToolOutput(content)) {
248
+ routeToActivityPanel(content, 'tool');
249
+ return;
250
+ }
251
+
252
+ // Notify listeners of final message
253
+ notifyListeners('message-final', {
254
+ messageId: messageId,
255
+ content: content,
256
+ role: message.payload?.role || message.role || 'assistant',
257
+ timestamp: message.timestamp || Date.now()
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Finalize the current streaming message
263
+ */
264
+ function finalizeStreamingMessage() {
265
+ if (!currentStreamingMessage) return;
266
+
267
+ notifyListeners('stream-end', {
268
+ messageId: currentMessageId,
269
+ content: currentStreamingMessage
270
+ });
271
+
272
+ currentStreamingMessage = null;
273
+ currentMessageId = null;
274
+ }
275
+
276
+ /**
277
+ * Send a message to the Gateway
278
+ * @param {string} text - The message text to send
279
+ * @returns {Promise<boolean>} - Success status
280
+ */
281
+ function sendMessage(text) {
282
+ if (!text || !text.trim()) {
283
+ logger.warn('GatewayChat: Cannot send empty message');
284
+ return Promise.resolve(false);
285
+ }
286
+
287
+ // Check connection
288
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
289
+ logger.error('GatewayChat: Cannot send message - not connected');
290
+ return Promise.resolve(false);
291
+ }
292
+
293
+ const message = {
294
+ type: 'chat',
295
+ content: text.trim(),
296
+ timestamp: Date.now(),
297
+ messageId: generateId()
298
+ };
299
+
300
+ try {
301
+ ws.send(JSON.stringify(message));
302
+
303
+ notifyListeners('message-sent', {
304
+ messageId: message.messageId,
305
+ content: message.content
306
+ });
307
+
308
+ logger.debug('GatewayChat: Message sent', message.messageId);
309
+ return Promise.resolve(true);
310
+ } catch (e) {
311
+ logger.error('GatewayChat: Failed to send message', e);
312
+ return Promise.resolve(false);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Load chat history from Gateway
318
+ * @returns {Promise<Array>} - Array of history messages
319
+ */
320
+ async function loadHistory() {
321
+ try {
322
+ const response = await fetch(`${GATEWAY_API_BASE}/history`);
323
+
324
+ if (!response.ok) {
325
+ throw new Error(`HTTP ${response.status}`);
326
+ }
327
+
328
+ const data = await response.json();
329
+ const messages = data.messages || data.history || data || [];
330
+
331
+ historyLoaded = true;
332
+
333
+ notifyListeners('history-loaded', { messages });
334
+
335
+ logger.debug('GatewayChat: History loaded', messages.length, 'messages');
336
+ return messages;
337
+ } catch (e) {
338
+ logger.error('GatewayChat: Failed to load history', e);
339
+ notifyListeners('history-error', { error: e.message });
340
+ return [];
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Register a callback for message events
346
+ * @param {Function} callback - Function to call with (eventType, data)
347
+ * @returns {Function} - Unsubscribe function
348
+ */
349
+ function onMessage(callback) {
350
+ if (typeof callback !== 'function') {
351
+ logger.warn('GatewayChat: onMessage requires a function');
352
+ return () => {};
353
+ }
354
+
355
+ messageListeners.push(callback);
356
+
357
+ // Return unsubscribe function
358
+ return () => {
359
+ messageListeners = messageListeners.filter(cb => cb !== callback);
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Notify all registered listeners
365
+ * @param {string} eventType - Type of event
366
+ * @param {object} data - Event data
367
+ */
368
+ function notifyListeners(eventType, data) {
369
+ messageListeners.forEach(callback => {
370
+ try {
371
+ callback(eventType, data);
372
+ } catch (e) {
373
+ logger.error('GatewayChat: Listener error', e);
374
+ }
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Check if text is tool output that should be filtered from chat
380
+ * @param {string} text - Text to check
381
+ * @returns {boolean} - True if this is tool output
382
+ */
383
+ function isToolOutput(text) {
384
+ if (!text || typeof text !== 'string') return false;
385
+
386
+ const lines = text.split('\n');
387
+
388
+ // Check each line for tool output patterns
389
+ for (const line of lines) {
390
+ const trimmed = line.trim();
391
+
392
+ // Lines starting with "SUCCESS:"
393
+ if (trimmed.startsWith('SUCCESS:')) {
394
+ return true;
395
+ }
396
+
397
+ // Lines starting with "FAILED:" or "ERROR:"
398
+ if (trimmed.startsWith('FAILED:') || trimmed.startsWith('ERROR:')) {
399
+ return true;
400
+ }
401
+
402
+ // File paths with line numbers (e.g., path/file.js:123)
403
+ if (/^[\w\/\\.-]+\.[\w]+:\d+/.test(trimmed)) {
404
+ return true;
405
+ }
406
+
407
+ // TCP connection output patterns
408
+ if (/TCP\s+(connection|socket|listen|accept)/i.test(trimmed)) {
409
+ return true;
410
+ }
411
+ if (/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+/.test(trimmed)) {
412
+ return true; // IP:port pattern
413
+ }
414
+
415
+ // Command output indicators
416
+ if (trimmed.startsWith('$ ') || trimmed.startsWith('> ')) {
417
+ return true;
418
+ }
419
+
420
+ // Process output patterns
421
+ if (/^\[.*\]\s*(stdout|stderr|exit|pid)/i.test(trimmed)) {
422
+ return true;
423
+ }
424
+ }
425
+
426
+ // Check for command output blocks (multiple lines that look like terminal output)
427
+ const codeBlockPatterns = [
428
+ /```(?:bash|sh|shell|powershell|cmd)/i,
429
+ /```\n\$/,
430
+ /```\n>/
431
+ ];
432
+
433
+ for (const pattern of codeBlockPatterns) {
434
+ if (pattern.test(text)) {
435
+ return true;
436
+ }
437
+ }
438
+
439
+ return false;
440
+ }
441
+
442
+ /**
443
+ * Route tool output to the activity panel instead of chat
444
+ * @param {string} content - The content to route
445
+ * @param {string} type - Type of content (tool, command, etc.)
446
+ */
447
+ function routeToActivityPanel(content, type) {
448
+ logger.debug('GatewayChat: Routing to activity panel', type);
449
+
450
+ // Try to use the developer/activity panel
451
+ if (window.UplinkDeveloper) {
452
+ // Log as a tool call
453
+ if (type === 'tool') {
454
+ window.UplinkDeveloper.logTool('Gateway Tool Output');
455
+ window.UplinkDeveloper.logToolCall('output', { content: truncate(content, 500) }, null);
456
+ }
457
+ }
458
+
459
+ // Also notify listeners so they can handle it
460
+ notifyListeners('tool-output', {
461
+ content: content,
462
+ type: type,
463
+ timestamp: Date.now()
464
+ });
465
+ }
466
+
467
+ /**
468
+ * Generate a unique message ID
469
+ * @returns {string} - Unique ID
470
+ */
471
+ function generateId() {
472
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
473
+ }
474
+
475
+ /**
476
+ * Truncate string to max length
477
+ * @param {string} str - String to truncate
478
+ * @param {number} maxLen - Maximum length
479
+ * @returns {string} - Truncated string
480
+ */
481
+ function truncate(str, maxLen) {
482
+ if (!str || str.length <= maxLen) return str;
483
+ return str.slice(0, maxLen) + '... [truncated]';
484
+ }
485
+
486
+ /**
487
+ * Get current connection state
488
+ * @returns {object} - Connection state
489
+ */
490
+ function getState() {
491
+ return {
492
+ connected: ws !== null && ws.readyState === WebSocket.OPEN,
493
+ streaming: currentStreamingMessage !== null,
494
+ historyLoaded: historyLoaded,
495
+ messageId: currentMessageId
496
+ };
497
+ }
498
+
499
+ /**
500
+ * Clear/reset the module state
501
+ */
502
+ function clear() {
503
+ currentStreamingMessage = null;
504
+ currentMessageId = null;
505
+ historyLoaded = false;
506
+ messageListeners = [];
507
+ }
508
+
509
+ // Export API
510
+ export const gatewayChat = {
511
+ init,
512
+ sendMessage,
513
+ loadHistory,
514
+ onMessage,
515
+ isToolOutput,
516
+ getState,
517
+ clear
518
+ };
519
+
520
+ // Backward compat: assign to window
521
+ window.gatewayChat = gatewayChat;
522
+ window.gatewayChat.send = sendMessage;
523
+
524
+ // Helper for init retry with exponential backoff
525
+ function scheduleInitRetry() {
526
+ if (initRetryCount >= MAX_INIT_RETRIES) {
527
+ logger.warn('GatewayChat: Max init retries reached, giving up');
528
+ return;
529
+ }
530
+ initRetryCount++;
531
+ const delay = Math.min(50 * Math.pow(2, initRetryCount), 5000);
532
+ setTimeout(init, delay);
533
+ }
534
+
535
+ // Register with core for coordinated initialization
536
+ UplinkCore.registerModule('gateway-chat', init);
537
+
538
+ logger.debug('GatewayChat: Module loaded');
@@ -0,0 +1,112 @@
1
+ // ============================================
2
+ // LOGGER MODULE
3
+ // Centralized logging with level control
4
+ // ============================================
5
+
6
+ // Log levels
7
+ const LEVELS = {
8
+ debug: 0,
9
+ info: 1,
10
+ warn: 2,
11
+ error: 3,
12
+ none: 4
13
+ };
14
+
15
+ // Default level (production = warn, can be overridden)
16
+ let currentLevel = LEVELS.warn;
17
+
18
+ // Load level from settings if available
19
+ function loadLevel() {
20
+ try {
21
+ const settings = JSON.parse(localStorage.getItem('uplink-settings') || '{}');
22
+ if (settings.logLevel && LEVELS[settings.logLevel] !== undefined) {
23
+ currentLevel = LEVELS[settings.logLevel];
24
+ }
25
+ } catch (e) {
26
+ // Ignore
27
+ }
28
+ }
29
+
30
+ // Set log level
31
+ function setLevel(level) {
32
+ if (LEVELS[level] !== undefined) {
33
+ currentLevel = LEVELS[level];
34
+ try {
35
+ const settings = JSON.parse(localStorage.getItem('uplink-settings') || '{}');
36
+ settings.logLevel = level;
37
+ localStorage.setItem('uplink-settings', JSON.stringify(settings));
38
+ } catch (e) {
39
+ // Ignore
40
+ }
41
+ }
42
+ }
43
+
44
+ // Get current level name
45
+ function getLevel() {
46
+ return Object.keys(LEVELS).find(k => LEVELS[k] === currentLevel) || 'warn';
47
+ }
48
+
49
+ // Logging functions
50
+ function debug(...args) {
51
+ if (currentLevel <= LEVELS.debug) {
52
+ console.log('[DEBUG]', ...args);
53
+ }
54
+ }
55
+
56
+ function info(...args) {
57
+ if (currentLevel <= LEVELS.info) {
58
+ console.log('[INFO]', ...args);
59
+ }
60
+ }
61
+
62
+ function warn(...args) {
63
+ if (currentLevel <= LEVELS.warn) {
64
+ console.warn('[WARN]', ...args);
65
+ }
66
+ }
67
+
68
+ function error(...args) {
69
+ if (currentLevel <= LEVELS.error) {
70
+ console.error('[ERROR]', ...args);
71
+ }
72
+ }
73
+
74
+ // Group logging (for structured output)
75
+ function group(label, fn) {
76
+ if (currentLevel <= LEVELS.debug) {
77
+ console.group(label);
78
+ try {
79
+ fn();
80
+ } finally {
81
+ console.groupEnd();
82
+ }
83
+ } else {
84
+ fn();
85
+ }
86
+ }
87
+
88
+ // Initialize
89
+ loadLevel();
90
+
91
+ // Export API
92
+ export const UplinkLogger = {
93
+ debug,
94
+ info,
95
+ warn,
96
+ error,
97
+ group,
98
+ setLevel,
99
+ getLevel,
100
+ LEVELS
101
+ };
102
+
103
+ export { debug, info, warn, error, group, setLevel, getLevel, LEVELS };
104
+
105
+ // Backward compat: assign to window
106
+ if (typeof window !== 'undefined') {
107
+ window.UplinkLogger = UplinkLogger;
108
+ window.logger = UplinkLogger;
109
+ }
110
+
111
+ // Logger initialized - use debug for init message (only shown if level is debug)
112
+ debug('Logger: Initialized (level=' + getLevel() + ')');