@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,581 @@
1
+ // ============================================
2
+ // SLASH COMMANDS MODULE
3
+ // Handles /command parsing, execution, and autocomplete
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+
8
+ // Command registry
9
+ const commands = {
10
+ help: {
11
+ description: 'Show available commands',
12
+ usage: '/help',
13
+ execute: showHelp
14
+ },
15
+ clear: {
16
+ description: 'Clear conversation history',
17
+ usage: '/clear',
18
+ execute: clearConversation
19
+ },
20
+ export: {
21
+ description: 'Export conversation',
22
+ usage: '/export [format]',
23
+ args: ['markdown', 'json'],
24
+ execute: exportConversation
25
+ },
26
+ status: {
27
+ description: 'Show connection and session status',
28
+ usage: '/status',
29
+ execute: showStatus
30
+ },
31
+ voice: {
32
+ description: 'Toggle voice mode',
33
+ usage: '/voice',
34
+ execute: toggleVoice
35
+ },
36
+ model: {
37
+ description: 'Show current model info',
38
+ usage: '/model',
39
+ execute: showModel
40
+ },
41
+ theme: {
42
+ description: 'Change color theme',
43
+ usage: '/theme [name]',
44
+ args: ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'],
45
+ execute: changeTheme
46
+ },
47
+ stats: {
48
+ description: 'Show usage dashboard',
49
+ usage: '/stats',
50
+ execute: showDashboard
51
+ },
52
+ remind: {
53
+ description: 'Set a reminder',
54
+ usage: '/remind <time> <message>',
55
+ args: ['5m', '15m', '30m', '1h', '2h', 'tomorrow'],
56
+ execute: setReminder
57
+ },
58
+ satellites: {
59
+ description: 'Open satellites panel',
60
+ usage: '/satellites',
61
+ execute: () => window.UplinkSatellites?.toggleNavigator()
62
+ },
63
+ share: {
64
+ description: 'Create shareable link for this conversation',
65
+ usage: '/share [title]',
66
+ execute: createShareLink
67
+ },
68
+ memdump: {
69
+ description: 'Dump current session state to memory file',
70
+ usage: '/memdump',
71
+ execute: () => sendAgentCommand('/m')
72
+ },
73
+ m: {
74
+ description: 'Shortcut for /memdump',
75
+ usage: '/m',
76
+ execute: () => sendAgentCommand('/m')
77
+ },
78
+ compact: {
79
+ description: 'Compact session context',
80
+ usage: '/compact',
81
+ execute: () => sendAgentCommand('/compact')
82
+ }
83
+ };
84
+
85
+ // Commands that should be passed through to the gateway as-is
86
+ // (OpenClaw native slash commands — gateway intercepts these before the agent)
87
+ const gatewayPassthroughPrefixes = [
88
+ 'help', 'commands', 'skill', 'status', 'allowlist', 'approve',
89
+ 'context', 'tts', 'whoami', 'subagents', 'config', 'debug',
90
+ 'usage', 'stop', 'restart', 'activation', 'send', 'reset',
91
+ 'new', 'compact', 'think', 'verbose', 'reasoning', 'elevated',
92
+ 'exec', 'model', 'models', 'queue', 'bash', 'prose'
93
+ ];
94
+
95
+ // Autocomplete state
96
+ let autocompleteVisible = false;
97
+ let autocompleteIndex = 0;
98
+ let filteredCommands = [];
99
+
100
+ // Create autocomplete dropdown with ARIA attributes
101
+ const autocompleteEl = document.createElement('div');
102
+ autocompleteEl.className = 'command-autocomplete';
103
+ autocompleteEl.style.display = 'none';
104
+ autocompleteEl.id = 'command-autocomplete-listbox';
105
+ autocompleteEl.role = 'listbox';
106
+
107
+ // Initialize when DOM is ready
108
+ function init() {
109
+ const textInput = document.getElementById('textInput');
110
+ const inputArea = document.querySelector('.input-area');
111
+
112
+ if (!textInput || !inputArea) {
113
+ logger.warn('Commands: Required elements not found, retrying...');
114
+ setTimeout(init, 100);
115
+ return;
116
+ }
117
+
118
+ // Insert autocomplete before input row
119
+ const textInputRow = document.getElementById('textInputRow');
120
+ textInputRow.parentNode.insertBefore(autocompleteEl, textInputRow);
121
+
122
+ // Add ARIA attributes to input for accessibility
123
+ textInput.role = 'combobox';
124
+ textInput.setAttribute('aria-autocomplete', 'list');
125
+ textInput.setAttribute('aria-controls', 'command-autocomplete-listbox');
126
+ textInput.setAttribute('aria-expanded', 'false');
127
+ textInput.setAttribute('aria-activedescendant', '');
128
+
129
+ // Listen for input changes
130
+ textInput.addEventListener('input', handleInput);
131
+ textInput.addEventListener('keydown', handleKeydown);
132
+ textInput.addEventListener('blur', () => {
133
+ // Delay hide to allow click on autocomplete
134
+ setTimeout(hideAutocomplete, 150);
135
+ });
136
+
137
+ // Share button handler
138
+ const shareBtn = document.getElementById('shareBtn');
139
+ shareBtn?.addEventListener('click', () => createShareLink(''));
140
+
141
+ logger.debug('Commands: Initialized');
142
+ }
143
+
144
+ function handleInput(e) {
145
+ const value = e.target.value;
146
+
147
+ if (value.startsWith('/')) {
148
+ const query = value.slice(1).toLowerCase();
149
+ filteredCommands = Object.keys(commands).filter(cmd =>
150
+ cmd.startsWith(query)
151
+ );
152
+
153
+ if (filteredCommands.length > 0 && !value.includes(' ')) {
154
+ showAutocomplete(filteredCommands);
155
+ } else {
156
+ hideAutocomplete();
157
+ }
158
+ } else {
159
+ hideAutocomplete();
160
+ }
161
+ }
162
+
163
+ function handleKeydown(e) {
164
+ if (!autocompleteVisible) {
165
+ // Check if Enter on a command
166
+ if (e.key === 'Enter' && !e.shiftKey) {
167
+ const value = e.target.value.trim();
168
+ if (value.startsWith('/')) {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ executeCommand(value);
172
+ return;
173
+ }
174
+ }
175
+ return;
176
+ }
177
+
178
+ switch (e.key) {
179
+ case 'ArrowDown':
180
+ e.preventDefault();
181
+ autocompleteIndex = Math.min(autocompleteIndex + 1, filteredCommands.length - 1);
182
+ updateAutocompleteSelection();
183
+ break;
184
+ case 'ArrowUp':
185
+ e.preventDefault();
186
+ autocompleteIndex = Math.max(autocompleteIndex - 1, 0);
187
+ updateAutocompleteSelection();
188
+ break;
189
+ case 'Tab':
190
+ e.preventDefault();
191
+ selectAutocomplete(filteredCommands[autocompleteIndex], false);
192
+ break;
193
+ case 'Enter':
194
+ e.preventDefault();
195
+ e.stopPropagation();
196
+ selectAutocomplete(filteredCommands[autocompleteIndex], true);
197
+ break;
198
+ case 'Escape':
199
+ e.preventDefault();
200
+ hideAutocomplete();
201
+ break;
202
+ }
203
+ }
204
+
205
+ function showAutocomplete(cmds) {
206
+ autocompleteEl.innerHTML = cmds.map((cmd, i) => `
207
+ <div class="command-option ${i === 0 ? 'selected' : ''}" data-command="${cmd}" role="option" id="command-option-${i}" aria-selected="${i === 0 ? 'true' : 'false'}">
208
+ <span class="command-name">/${cmd}</span>
209
+ <span class="command-desc">${commands[cmd].description}</span>
210
+ </div>
211
+ `).join('');
212
+
213
+ // Add click handlers
214
+ autocompleteEl.querySelectorAll('.command-option').forEach(el => {
215
+ el.addEventListener('click', () => {
216
+ selectAutocomplete(el.dataset.command, true);
217
+ });
218
+ });
219
+
220
+ autocompleteEl.style.display = 'block';
221
+ autocompleteVisible = true;
222
+ autocompleteIndex = 0;
223
+
224
+ // Update ARIA attributes on input
225
+ const textInput = document.getElementById('textInput');
226
+ textInput.setAttribute('aria-expanded', 'true');
227
+ if (cmds.length > 0) {
228
+ textInput.setAttribute('aria-activedescendant', 'command-option-0');
229
+ }
230
+ }
231
+
232
+ function hideAutocomplete() {
233
+ autocompleteEl.style.display = 'none';
234
+ autocompleteVisible = false;
235
+ autocompleteIndex = 0;
236
+
237
+ // Update ARIA attributes on input
238
+ const textInput = document.getElementById('textInput');
239
+ if (textInput) {
240
+ textInput.setAttribute('aria-expanded', 'false');
241
+ textInput.setAttribute('aria-activedescendant', '');
242
+ }
243
+ }
244
+
245
+ function updateAutocompleteSelection() {
246
+ const textInput = document.getElementById('textInput');
247
+
248
+ autocompleteEl.querySelectorAll('.command-option').forEach((el, i) => {
249
+ const isSelected = i === autocompleteIndex;
250
+ el.classList.toggle('selected', isSelected);
251
+ el.setAttribute('aria-selected', isSelected ? 'true' : 'false');
252
+
253
+ if (isSelected && textInput) {
254
+ textInput.setAttribute('aria-activedescendant', `command-option-${i}`);
255
+ }
256
+ });
257
+ }
258
+
259
+ function selectAutocomplete(cmd, execute = false) {
260
+ const textInput = document.getElementById('textInput');
261
+ hideAutocomplete();
262
+
263
+ if (execute) {
264
+ // Execute the command directly
265
+ textInput.value = '';
266
+ executeCommand('/' + cmd);
267
+ } else {
268
+ // Just fill in the command (Tab was pressed)
269
+ textInput.value = '/' + cmd + ' ';
270
+ textInput.focus();
271
+ }
272
+ }
273
+
274
+ function executeCommand(input) {
275
+ const textInput = document.getElementById('textInput');
276
+ textInput.value = '';
277
+
278
+ const parts = input.slice(1).split(' ');
279
+ const cmdName = parts[0].toLowerCase();
280
+ const args = parts.slice(1);
281
+
282
+ // Check Uplink-local commands first
283
+ const cmd = commands[cmdName];
284
+ if (cmd) {
285
+ cmd.execute(args);
286
+ return;
287
+ }
288
+
289
+ // Check if it's a Gateway passthrough command (OpenClaw native)
290
+ const isGatewayCmd = gatewayPassthroughPrefixes.some(p => cmdName === p || cmdName.startsWith(p + '.'));
291
+ if (isGatewayCmd) {
292
+ // Send as a regular message — Gateway intercepts native /commands
293
+ sendAgentCommand(input);
294
+ return;
295
+ }
296
+
297
+ // Unknown command — still pass through to Gateway in case it's a native command
298
+ // Gateway will handle it or the agent will see it as a message
299
+ sendAgentCommand(input);
300
+ }
301
+
302
+ // Send a message to the AI agent as a command
303
+ function sendAgentCommand(message) {
304
+ if (window.UplinkChat?.sendTextMessage) {
305
+ window.UplinkChat.sendTextMessage(message);
306
+ } else {
307
+ addSystemMessage('Chat module not available.');
308
+ }
309
+ }
310
+
311
+ // Command implementations
312
+ function showHelp() {
313
+ const helpText = Object.entries(commands).map(([name, cmd]) =>
314
+ `**/${name}** — ${cmd.description}`
315
+ ).join('\n');
316
+
317
+ addSystemMessage('**Available Commands:**\n' + helpText);
318
+ }
319
+
320
+ function clearConversation() {
321
+ if (confirm('Clear chat history? Your assistant still remembers the conversation.')) {
322
+ const messages = document.getElementById('messages');
323
+ const emptyState = document.getElementById('emptyState');
324
+ messages.innerHTML = '';
325
+ emptyState.style.display = 'flex';
326
+ messages.appendChild(emptyState);
327
+
328
+ // Clear from storage
329
+ localStorage.removeItem('uplink-history');
330
+ localStorage.removeItem('uplink-history-encrypted');
331
+
332
+ addSystemMessage('Chat cleared.');
333
+ }
334
+ }
335
+
336
+ function exportConversation(args) {
337
+ const format = args[0] || 'markdown';
338
+ const messages = document.querySelectorAll('.message:not(.system)');
339
+
340
+ if (messages.length === 0) {
341
+ addSystemMessage('No messages to export.');
342
+ return;
343
+ }
344
+
345
+ let content;
346
+ let filename;
347
+ let mimeType;
348
+
349
+ if (format === 'json') {
350
+ const data = Array.from(messages).map(msg => ({
351
+ role: msg.classList.contains('user') ? 'user' : 'assistant',
352
+ content: msg.textContent,
353
+ timestamp: new Date().toISOString()
354
+ }));
355
+ content = JSON.stringify(data, null, 2);
356
+ filename = `uplink-export-${Date.now()}.json`;
357
+ mimeType = 'application/json';
358
+ } else {
359
+ // Markdown format
360
+ const lines = Array.from(messages).map(msg => {
361
+ const role = msg.classList.contains('user') ? '**You:**' : '**Assistant:**';
362
+ return `${role}\n${msg.textContent}\n`;
363
+ });
364
+ content = `# Uplink Conversation Export\n\n${lines.join('\n---\n\n')}`;
365
+ filename = `uplink-export-${Date.now()}.md`;
366
+ mimeType = 'text/markdown';
367
+ }
368
+
369
+ // Download
370
+ const blob = new Blob([content], { type: mimeType });
371
+ const url = URL.createObjectURL(blob);
372
+ const a = document.createElement('a');
373
+ a.href = url;
374
+ a.download = filename;
375
+ a.click();
376
+ URL.revokeObjectURL(url);
377
+
378
+ addSystemMessage(`Exported as ${filename}`);
379
+ }
380
+
381
+ function showStatus() {
382
+ const statusDot = document.querySelector('.status-dot');
383
+ const isConnected = statusDot && statusDot.style.background !== 'var(--error)';
384
+
385
+ // Get session info from localStorage
386
+ const config = JSON.parse(localStorage.getItem('uplink-config') || '{}');
387
+ const encrypted = config.encryptionEnabled ? 'Yes' : 'No';
388
+
389
+ // Count messages
390
+ const messageCount = document.querySelectorAll('.message:not(.system)').length;
391
+
392
+ const status = `**Status:**
393
+ • Connection: ${isConnected ? '🟢 Connected' : '🔴 Disconnected'}
394
+ • Gateway: ${config.gatewayUrl || 'Not configured'}
395
+ • Encryption: ${encrypted}
396
+ • Messages: ${messageCount}
397
+ • Session: Active`;
398
+
399
+ addSystemMessage(status);
400
+ }
401
+
402
+ function toggleVoice() {
403
+ const voiceModeTab = document.getElementById('voiceModeTab');
404
+ const textModeTab = document.getElementById('textModeTab');
405
+ const isVoice = voiceModeTab.classList.contains('active');
406
+
407
+ if (isVoice) {
408
+ textModeTab.click();
409
+ addSystemMessage('Switched to text mode.');
410
+ } else {
411
+ voiceModeTab.click();
412
+ addSystemMessage('Switched to voice mode.');
413
+ }
414
+ }
415
+
416
+ function showModel() {
417
+ const config = JSON.parse(localStorage.getItem('uplink-config') || '{}');
418
+ const gatewayUrl = config.gatewayUrl || 'http://localhost:18789';
419
+
420
+ const voiceName = config.voiceName || config.assistantName || 'Default';
421
+ const assistantName = config.assistantName || 'Assistant';
422
+
423
+ const modelInfo = `**Model Info:**
424
+ • Gateway: ${gatewayUrl}
425
+ • Model: openclaw (routed by gateway)
426
+ • Voice: ${voiceName} (ElevenLabs)
427
+ • Assistant: ${assistantName}
428
+
429
+ *Model selection is managed by the OpenClaw gateway.*`;
430
+
431
+ addSystemMessage(modelInfo);
432
+ }
433
+
434
+ function changeTheme(args) {
435
+ const themeName = args[0];
436
+
437
+ if (!themeName) {
438
+ // Show current theme and available options
439
+ const currentTheme = window.UplinkThemes?.get() || localStorage.getItem('uplink-theme') || 'dark';
440
+ const available = window.UplinkThemes?.list() || ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'];
441
+ addSystemMessage(`**Current theme:** ${currentTheme}\n**Available:** ${available.join(', ')}`);
442
+ return;
443
+ }
444
+
445
+ // Validate theme name
446
+ const available = window.UplinkThemes?.list() || ['dark', 'light', 'midnight', 'sunset', 'forest', 'high-contrast'];
447
+ if (!available.includes(themeName)) {
448
+ addSystemMessage(`Unknown theme: ${themeName}\n**Available:** ${available.join(', ')}`);
449
+ return;
450
+ }
451
+
452
+ // Use the themes module if available
453
+ if (window.UplinkThemes && typeof window.UplinkThemes.apply === 'function') {
454
+ window.UplinkThemes.apply(themeName);
455
+ addSystemMessage(`Theme changed to: ${themeName}`);
456
+ } else {
457
+ // Fallback: try to set directly
458
+ document.documentElement.setAttribute('data-theme', themeName);
459
+ localStorage.setItem('uplink-theme', themeName);
460
+ addSystemMessage(`Theme changed to: ${themeName}`);
461
+ }
462
+ }
463
+
464
+ function showDashboard() {
465
+ if (window.UplinkDashboard) {
466
+ window.UplinkDashboard.show();
467
+ } else {
468
+ addSystemMessage('Dashboard module not loaded.');
469
+ }
470
+ }
471
+
472
+ function setReminder(args) {
473
+ if (args.length < 2) {
474
+ addSystemMessage(`**Usage:** /remind <time> <message>\n\n**Examples:**\n• /remind 30m check the build\n• /remind 2h call mom\n• /remind tomorrow morning brief`);
475
+ return;
476
+ }
477
+
478
+ const timeArg = args[0];
479
+ const message = args.slice(1).join(' ');
480
+
481
+ // Format as a natural language request for the AI to process
482
+ const request = `Please set a reminder for ${timeArg} from now: "${message}"`;
483
+
484
+ // Send through chat (AI will use cron tool)
485
+ if (window.UplinkChat?.sendTextMessage) {
486
+ window.UplinkChat.sendTextMessage(request);
487
+ } else {
488
+ addSystemMessage('Chat module not available.');
489
+ }
490
+ }
491
+
492
+ async function createShareLink(args) {
493
+ const title = args.length > 0 ? args.join(' ') : null;
494
+
495
+ // Get conversation messages
496
+ const storage = window.UplinkStorage;
497
+ if (!storage) {
498
+ addSystemMessage('Storage module not available.');
499
+ return;
500
+ }
501
+
502
+ const messages = storage.getHistory?.() || [];
503
+
504
+ if (messages.length === 0) {
505
+ addSystemMessage('No messages to share.');
506
+ return;
507
+ }
508
+
509
+ addSystemMessage('Creating share link...');
510
+
511
+ try {
512
+ const response = await fetch('/api/share/create', {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({
516
+ title: title || `Conversation on ${new Date().toLocaleDateString()}`,
517
+ messages: messages.slice(-50) // Share last 50 messages max
518
+ })
519
+ });
520
+
521
+ if (!response.ok) {
522
+ addSystemMessage(`Error: Server returned ${response.status}`);
523
+ return;
524
+ }
525
+ const data = await response.json();
526
+
527
+ if (data.error) {
528
+ addSystemMessage(`Error: ${data.error}`);
529
+ return;
530
+ }
531
+
532
+ const shareUrl = window.location.origin + data.shareUrl;
533
+
534
+ // Copy to clipboard
535
+ try {
536
+ await navigator.clipboard.writeText(shareUrl);
537
+ addSystemMessage(`**Share link created!**\n\n${shareUrl}\n\n*(Copied to clipboard - expires in 7 days)*`);
538
+ } catch (e) {
539
+ addSystemMessage(`**Share link created!**\n\n${shareUrl}\n\n*(Expires in 7 days)*`);
540
+ }
541
+ } catch (e) {
542
+ addSystemMessage(`Failed to create share link: ${e.message}`);
543
+ }
544
+ }
545
+
546
+ // Helper to add system messages
547
+ function addSystemMessage(text) {
548
+ // Use the global addMessage if available, otherwise create our own
549
+ if (typeof window.addMessage === 'function') {
550
+ window.addMessage(text, 'system');
551
+ } else {
552
+ const messages = document.getElementById('messages');
553
+ const emptyState = document.getElementById('emptyState');
554
+ if (emptyState) emptyState.style.display = 'none';
555
+
556
+ const div = document.createElement('div');
557
+ div.className = 'message system';
558
+ div.innerHTML = formatMarkdown(text);
559
+ messages.appendChild(div);
560
+ messages.scrollTop = messages.scrollHeight;
561
+ }
562
+ }
563
+
564
+ // Simple markdown formatter for system messages
565
+ function formatMarkdown(text) {
566
+ return text
567
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
568
+ .replace(/\n/g, '<br>');
569
+ }
570
+
571
+ // Expose for external use
572
+ export const UplinkCommands = {
573
+ execute: executeCommand,
574
+ list: () => Object.keys(commands)
575
+ };
576
+
577
+ // Backward compat: assign to window
578
+ window.UplinkCommands = UplinkCommands;
579
+
580
+ // Register with core for coordinated initialization
581
+ UplinkCore.registerModule('commands', init);
@@ -0,0 +1,121 @@
1
+ // ============================================
2
+ // CONNECTION API MODULE
3
+ // HTTP API calls for chat endpoints
4
+ // ============================================
5
+
6
+ const logger = window.logger || console;
7
+
8
+ /**
9
+ * Check server health
10
+ */
11
+ export async function checkServerHealth() {
12
+ try {
13
+ const res = await fetch('/api/status', { method: 'GET', cache: 'no-cache' });
14
+ return res.ok;
15
+ } catch (e) {
16
+ logger.debug('Connection: Server health check failed', e);
17
+ return false;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Fetch chat history
23
+ */
24
+ export async function fetchChatHistory(satelliteId = 'main') {
25
+ try {
26
+ const url = satelliteId === 'main'
27
+ ? '/api/history'
28
+ : `/api/history?satelliteId=${encodeURIComponent(satelliteId)}`;
29
+
30
+ const res = await fetch(url, { method: 'GET' });
31
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
32
+
33
+ const data = await res.json();
34
+ return data.messages || [];
35
+ } catch (err) {
36
+ logger.error('Connection: Failed to fetch history', err);
37
+ throw err;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Send a chat message (without streaming)
43
+ */
44
+ export async function sendMessage(message, satelliteId = 'main', agentId = 'main') {
45
+ try {
46
+ const res = await fetch('/api/chat', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({
50
+ message,
51
+ stream: false,
52
+ satelliteId,
53
+ agentId
54
+ })
55
+ });
56
+
57
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
58
+
59
+ const data = await res.json();
60
+ return data.response;
61
+ } catch (err) {
62
+ logger.error('Connection: Failed to send message', err);
63
+ throw err;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Upload a file
69
+ */
70
+ export async function uploadFile(file, satelliteId = 'main') {
71
+ const formData = new FormData();
72
+ formData.append('file', file);
73
+ if (satelliteId !== 'main') {
74
+ formData.append('satelliteId', satelliteId);
75
+ }
76
+
77
+ try {
78
+ const res = await fetch('/api/upload', {
79
+ method: 'POST',
80
+ body: formData
81
+ });
82
+
83
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
84
+
85
+ const data = await res.json();
86
+ return data.url;
87
+ } catch (err) {
88
+ logger.error('Connection: Failed to upload file', err);
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Clear chat history
95
+ */
96
+ export async function clearHistory(satelliteId = 'main') {
97
+ try {
98
+ const url = satelliteId === 'main'
99
+ ? '/api/clear'
100
+ : `/api/clear?satelliteId=${encodeURIComponent(satelliteId)}`;
101
+
102
+ const res = await fetch(url, { method: 'POST' });
103
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
104
+
105
+ return true;
106
+ } catch (err) {
107
+ logger.error('Connection: Failed to clear history', err);
108
+ throw err;
109
+ }
110
+ }
111
+
112
+ // Expose on window for backward compatibility
113
+ if (typeof window !== 'undefined') {
114
+ window.UplinkConnectionAPI = {
115
+ checkServerHealth,
116
+ fetchChatHistory,
117
+ sendMessage,
118
+ uploadFile,
119
+ clearHistory
120
+ };
121
+ }