@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,345 @@
1
+ /**
2
+ * Config & Settings Routes
3
+ * ========================
4
+ * Express routes for the Uplink Configuration System.
5
+ *
6
+ * Endpoints:
7
+ * GET /api/config — Non-sensitive config
8
+ * POST /api/config/test-gateway — Test OpenClaw gateway connection
9
+ * POST /api/config/test-voice — Test TTS with current settings
10
+ * POST /api/onboarding/step/:stepNumber — Save onboarding step progress
11
+ * POST /api/onboarding/complete — Finalize onboarding
12
+ * PUT /api/settings/connections — Update gateway/connection settings
13
+ * PUT /api/settings/voice — Update TTS/voice settings
14
+ * PUT /api/settings/security — Update password/security settings
15
+ * POST /api/settings/export — Download encrypted config backup
16
+ * POST /api/settings/import — Restore config from uploaded backup
17
+ */
18
+
19
+ import { Router } from 'express';
20
+ import http from 'http';
21
+ import https from 'https';
22
+ import { GATEWAY_HEALTH_CHECK_TIMEOUT_MS, MIN_RECONNECT_INTERVAL_MS } from '../config.js';
23
+ import configStore from '../config-store.js';
24
+ import { createLogger } from '../logger.js';
25
+
26
+ const log = createLogger('config-routes');
27
+
28
+ const router = Router();
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function asyncHandler(fn) {
35
+ return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
36
+ }
37
+
38
+ function ok(res, data = {}, status = 200) {
39
+ return res.status(status).json({ success: true, ...data });
40
+ }
41
+
42
+ function fail(res, message, status = 400) {
43
+ return res.status(status).json({ success: false, error: message });
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // 1. GET /api/config
48
+ // ---------------------------------------------------------------------------
49
+ router.get('/api/config', asyncHandler(async (_req, res) => {
50
+ return ok(res, { config: configStore.getSafe() });
51
+ }));
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // 2. POST /api/config/test-gateway
55
+ // ---------------------------------------------------------------------------
56
+ router.post('/api/config/test-gateway', asyncHandler(async (req, res) => {
57
+ const { gatewayUrl, gatewayToken } = req.body || {};
58
+ const raw = configStore.getRaw();
59
+ const url = gatewayUrl || raw.connections.gatewayUrl;
60
+ const token = gatewayToken || raw.connections.gatewayToken;
61
+
62
+ if (!url) {
63
+ return fail(res, 'No gateway URL configured or provided.');
64
+ }
65
+
66
+ try {
67
+ const result = await testGateway(url, token);
68
+ return ok(res, result);
69
+ } catch (err) {
70
+ return ok(res, { reachable: false, latencyMs: null, error: err.message });
71
+ }
72
+ }));
73
+
74
+ function testGateway(gatewayUrl, token) {
75
+ return new Promise((resolve, reject) => {
76
+ let parsed;
77
+ try {
78
+ parsed = new URL(gatewayUrl);
79
+ } catch {
80
+ return reject(new Error('Invalid gateway URL'));
81
+ }
82
+
83
+ parsed.pathname = parsed.pathname.replace(/\/$/, '') + '/status';
84
+ const transport = parsed.protocol === 'https:' ? https : http;
85
+ const headers = {};
86
+ if (token) headers['Authorization'] = `Bearer ${token}`;
87
+
88
+ const start = Date.now();
89
+ const request = transport.get(parsed.toString(), { headers, timeout: GATEWAY_HEALTH_CHECK_TIMEOUT_MS }, (response) => {
90
+ const latencyMs = Date.now() - start;
91
+ let body = '';
92
+ response.on('data', (chunk) => { body += chunk; });
93
+ response.on('end', () => {
94
+ const reachable = response.statusCode >= 200 && response.statusCode < 400;
95
+ let version = null;
96
+ try {
97
+ const json = JSON.parse(body);
98
+ version = json.version || json.serverVersion || null;
99
+ } catch { /* not JSON */ }
100
+ resolve({ reachable, latencyMs, version });
101
+ });
102
+ });
103
+
104
+ request.on('error', (err) => reject(err));
105
+ request.on('timeout', () => {
106
+ request.destroy();
107
+ reject(new Error('Gateway connection timed out'));
108
+ });
109
+ });
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // 3. POST /api/config/test-voice
114
+ // ---------------------------------------------------------------------------
115
+ router.post('/api/config/test-voice', asyncHandler(async (req, res) => {
116
+ const { text } = req.body || {};
117
+ const sampleText = text || 'Hello! This is a test of the voice configuration.';
118
+ const raw = configStore.getRaw();
119
+
120
+ if (!raw.voice.enabled) {
121
+ return fail(res, 'Voice/TTS is not enabled in the current configuration.');
122
+ }
123
+
124
+ // TODO: Wire to actual TTS provider when integrated
125
+ return ok(res, {
126
+ played: true,
127
+ provider: raw.voice.ttsProvider,
128
+ voice: raw.voice.ttsVoice,
129
+ textUsed: sampleText,
130
+ durationMs: Math.ceil(sampleText.length * 60),
131
+ });
132
+ }));
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // 4. POST /api/onboarding/step/:stepNumber
136
+ // ---------------------------------------------------------------------------
137
+ router.post('/api/onboarding/step/:stepNumber', asyncHandler(async (req, res) => {
138
+ const stepNumber = parseInt(req.params.stepNumber, 10);
139
+
140
+ if (isNaN(stepNumber) || stepNumber < 1) {
141
+ return fail(res, 'Invalid step number. Must be a positive integer.');
142
+ }
143
+
144
+ const data = req.body;
145
+ if (!data || Object.keys(data).length === 0) {
146
+ return fail(res, 'Step data is required in the request body.');
147
+ }
148
+
149
+ configStore.saveOnboardingStep(stepNumber, data);
150
+ await configStore.save();
151
+
152
+ return ok(res, {
153
+ step: stepNumber,
154
+ currentStep: configStore.getSafe().onboarding.currentStep,
155
+ });
156
+ }));
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // 5. POST /api/onboarding/complete
160
+ // ---------------------------------------------------------------------------
161
+ router.post('/api/onboarding/complete', asyncHandler(async (req, res) => {
162
+ const { password } = req.body || {};
163
+
164
+ if (!password || typeof password !== 'string' || password.length < 8) {
165
+ return fail(res, 'A password of at least 8 characters is required.');
166
+ }
167
+
168
+ const current = configStore.getSafe();
169
+ if (current.onboarding.completed) {
170
+ return fail(res, 'Onboarding has already been completed.', 409);
171
+ }
172
+
173
+ configStore.setPassword(password);
174
+ configStore.completeOnboarding();
175
+ await configStore.save();
176
+
177
+ return ok(res, {
178
+ onboarding: configStore.getSafe().onboarding,
179
+ message: 'Onboarding complete. Configuration encrypted and saved.',
180
+ }, 201);
181
+ }));
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // 6. PUT /api/settings/connections
185
+ // ---------------------------------------------------------------------------
186
+ router.put('/api/settings/connections', asyncHandler(async (req, res) => {
187
+ const allowed = ['gatewayUrl', 'gatewayToken', 'autoReconnect', 'reconnectIntervalMs'];
188
+ const updates = {};
189
+ let hasUpdate = false;
190
+
191
+ for (const key of allowed) {
192
+ if (req.body[key] !== undefined) {
193
+ updates[key] = req.body[key];
194
+ hasUpdate = true;
195
+ }
196
+ }
197
+
198
+ if (!hasUpdate) {
199
+ return fail(res, `No valid fields provided. Allowed: ${allowed.join(', ')}`);
200
+ }
201
+
202
+ if (updates.gatewayUrl !== undefined && typeof updates.gatewayUrl !== 'string') {
203
+ return fail(res, 'gatewayUrl must be a string.');
204
+ }
205
+ if (updates.reconnectIntervalMs !== undefined) {
206
+ const ms = Number(updates.reconnectIntervalMs);
207
+ if (isNaN(ms) || ms < MIN_RECONNECT_INTERVAL_MS) return fail(res, `reconnectIntervalMs must be >= ${MIN_RECONNECT_INTERVAL_MS}.`);
208
+ updates.reconnectIntervalMs = ms;
209
+ }
210
+
211
+ const updated = configStore.updateSection('connections', updates);
212
+ await configStore.save();
213
+ return ok(res, { connections: updated.connections });
214
+ }));
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // 7. PUT /api/settings/voice
218
+ // ---------------------------------------------------------------------------
219
+ router.put('/api/settings/voice', asyncHandler(async (req, res) => {
220
+ const allowed = ['enabled', 'ttsProvider', 'ttsVoice', 'volume', 'speed'];
221
+ const updates = {};
222
+ let hasUpdate = false;
223
+
224
+ for (const key of allowed) {
225
+ if (req.body[key] !== undefined) {
226
+ updates[key] = req.body[key];
227
+ hasUpdate = true;
228
+ }
229
+ }
230
+
231
+ if (!hasUpdate) {
232
+ return fail(res, `No valid fields provided. Allowed: ${allowed.join(', ')}`);
233
+ }
234
+
235
+ if (updates.volume !== undefined) {
236
+ const v = Number(updates.volume);
237
+ if (isNaN(v) || v < 0 || v > 1) return fail(res, 'volume must be 0-1.');
238
+ updates.volume = v;
239
+ }
240
+ if (updates.speed !== undefined) {
241
+ const s = Number(updates.speed);
242
+ if (isNaN(s) || s < 0.25 || s > 4) return fail(res, 'speed must be 0.25-4.');
243
+ updates.speed = s;
244
+ }
245
+
246
+ const updated = configStore.updateSection('voice', updates);
247
+ await configStore.save();
248
+ return ok(res, { voice: updated.voice });
249
+ }));
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // 8. PUT /api/settings/security
253
+ // ---------------------------------------------------------------------------
254
+ router.put('/api/settings/security', asyncHandler(async (req, res) => {
255
+ const { currentPassword, newPassword, lockTimeoutMin, requirePasswordOnStart } = req.body || {};
256
+
257
+ if (newPassword !== undefined) {
258
+ if (!currentPassword) {
259
+ return fail(res, 'currentPassword is required when setting a new password.', 401);
260
+ }
261
+ if (!configStore.verifyPassword(currentPassword)) {
262
+ return fail(res, 'Current password is incorrect.', 401);
263
+ }
264
+ if (typeof newPassword !== 'string' || newPassword.length < 8) {
265
+ return fail(res, 'newPassword must be at least 8 characters.');
266
+ }
267
+ configStore.setPassword(newPassword);
268
+ }
269
+
270
+ const sectionUpdates = {};
271
+ if (lockTimeoutMin !== undefined) {
272
+ const t = Number(lockTimeoutMin);
273
+ if (isNaN(t) || t < 1) return fail(res, 'lockTimeoutMin must be positive.');
274
+ sectionUpdates.lockTimeoutMin = t;
275
+ }
276
+ if (requirePasswordOnStart !== undefined) {
277
+ sectionUpdates.requirePasswordOnStart = Boolean(requirePasswordOnStart);
278
+ }
279
+
280
+ if (Object.keys(sectionUpdates).length) {
281
+ configStore.updateSection('security', sectionUpdates);
282
+ }
283
+
284
+ await configStore.save();
285
+ return ok(res, { security: configStore.getSafe().security });
286
+ }));
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // 9. POST /api/settings/export
290
+ // ---------------------------------------------------------------------------
291
+ router.post('/api/settings/export', asyncHandler(async (req, res) => {
292
+ const { password } = req.body || {};
293
+
294
+ if (!password) return fail(res, 'Password is required.', 401);
295
+ if (!configStore.verifyPassword(password)) return fail(res, 'Incorrect password.', 401);
296
+
297
+ const envelope = configStore.exportEncrypted(password);
298
+ const payload = JSON.stringify(envelope, null, 2);
299
+
300
+ res.setHeader('Content-Type', 'application/json');
301
+ res.setHeader('Content-Disposition', 'attachment; filename="uplink-config-backup.json"');
302
+ return res.status(200).send(payload);
303
+ }));
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // 10. POST /api/settings/import
307
+ // ---------------------------------------------------------------------------
308
+ router.post('/api/settings/import', asyncHandler(async (req, res) => {
309
+ const { password, backup } = req.body || {};
310
+
311
+ if (!password) return fail(res, 'Password is required.', 401);
312
+ if (!backup || typeof backup !== 'object') {
313
+ return fail(res, 'A "backup" object is required in the request body.');
314
+ }
315
+
316
+ const required = ['salt', 'iv', 'tag', 'ciphertext'];
317
+ const missing = required.filter((f) => !backup[f]);
318
+ if (missing.length) {
319
+ return fail(res, `Invalid backup. Missing: ${missing.join(', ')}`);
320
+ }
321
+
322
+ try {
323
+ const restored = configStore.importEncrypted(backup, password);
324
+ await configStore.save();
325
+ return ok(res, { message: 'Configuration restored.', config: restored });
326
+ } catch (err) {
327
+ const msg = err.message.includes('unable to authenticate')
328
+ ? 'Decryption failed. Wrong password or corrupted backup.'
329
+ : `Import failed: ${err.message}`;
330
+ return fail(res, msg, 401);
331
+ }
332
+ }));
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Error handler
336
+ // ---------------------------------------------------------------------------
337
+ router.use((err, _req, res, _next) => {
338
+ log.error(err);
339
+ return res.status(500).json({
340
+ success: false,
341
+ error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
342
+ });
343
+ });
344
+
345
+ export default router;