@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.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// ACTIVITY PANEL MODULE
|
|
3
|
+
// Token usage, raw API responses, error logs, tool visibility
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
// ========== CONSTANTS ==========
|
|
9
|
+
const MAX_LOGS = 50;
|
|
10
|
+
const INIT_RETRY_DELAY_MS = 100;
|
|
11
|
+
const ERROR_FLASH_DURATION_MS = 2000;
|
|
12
|
+
const TOOL_FLASH_DURATION_MS = 1000;
|
|
13
|
+
const RESPONSE_TRUNCATE_LENGTH = 500;
|
|
14
|
+
|
|
15
|
+
// State
|
|
16
|
+
let panelVisible = false;
|
|
17
|
+
let tokenUsage = { prompt: 0, completion: 0, total: 0 };
|
|
18
|
+
let apiLogs = [];
|
|
19
|
+
let errorLogs = [];
|
|
20
|
+
let toolCalls = [];
|
|
21
|
+
|
|
22
|
+
// Create activity panel
|
|
23
|
+
const panel = document.createElement('div');
|
|
24
|
+
panel.className = 'dev-panel';
|
|
25
|
+
panel.innerHTML = `
|
|
26
|
+
<div class="dev-panel-header">
|
|
27
|
+
<span class="dev-panel-title">Activity</span>
|
|
28
|
+
<button class="dev-panel-close" title="Close">×</button>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="dev-panel-tabs">
|
|
31
|
+
<button class="dev-tab active" data-tab="tokens">Tokens</button>
|
|
32
|
+
<button class="dev-tab" data-tab="api">API</button>
|
|
33
|
+
<button class="dev-tab" data-tab="tools">Tools</button>
|
|
34
|
+
<button class="dev-tab" data-tab="errors">Errors</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="dev-panel-content">
|
|
37
|
+
<div class="dev-tab-content active" id="devTokens">
|
|
38
|
+
<div class="token-stats">
|
|
39
|
+
<div class="token-stat">
|
|
40
|
+
<span class="token-label">Prompt</span>
|
|
41
|
+
<span class="token-value" id="tokenPrompt">0</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="token-stat">
|
|
44
|
+
<span class="token-label">Completion</span>
|
|
45
|
+
<span class="token-value" id="tokenCompletion">0</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="token-stat total">
|
|
48
|
+
<span class="token-label">Total</span>
|
|
49
|
+
<span class="token-value" id="tokenTotal">0</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="token-session">
|
|
53
|
+
<span class="token-label">Session Total</span>
|
|
54
|
+
<span class="token-value" id="tokenSession">0</span>
|
|
55
|
+
</div>
|
|
56
|
+
<button class="dev-btn" id="resetTokens">Reset Session</button>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="dev-tab-content" id="devApi">
|
|
59
|
+
<div class="api-log-list" id="apiLogList">
|
|
60
|
+
<div class="empty-log">No API calls yet</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="dev-tab-content" id="devTools">
|
|
64
|
+
<div class="tool-list" id="toolList">
|
|
65
|
+
<div class="empty-log">No tool calls yet</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="dev-tab-content" id="devErrors">
|
|
69
|
+
<div class="error-list" id="errorList">
|
|
70
|
+
<div class="empty-log">No errors 🎉</div>
|
|
71
|
+
</div>
|
|
72
|
+
<button class="dev-btn" id="clearErrors">Clear Errors</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// Session token tracking
|
|
78
|
+
let sessionTokens = 0;
|
|
79
|
+
|
|
80
|
+
function init() {
|
|
81
|
+
const app = document.querySelector('.app');
|
|
82
|
+
if (!app) {
|
|
83
|
+
setTimeout(init, 100);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add panel to DOM
|
|
88
|
+
document.body.appendChild(panel);
|
|
89
|
+
|
|
90
|
+
// Listen for activity button click (in header)
|
|
91
|
+
const activityBtn = document.getElementById('activityBtn');
|
|
92
|
+
if (activityBtn) {
|
|
93
|
+
activityBtn.addEventListener('click', togglePanel);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Close button
|
|
97
|
+
panel.querySelector('.dev-panel-close').addEventListener('click', () => {
|
|
98
|
+
if (window.UplinkPanels) {
|
|
99
|
+
window.UplinkPanels.close('activity');
|
|
100
|
+
} else {
|
|
101
|
+
panelVisible = false;
|
|
102
|
+
panel.classList.remove('visible');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Tab switching
|
|
107
|
+
panel.querySelectorAll('.dev-tab').forEach(tab => {
|
|
108
|
+
tab.addEventListener('click', () => switchTab(tab.dataset.tab));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Reset tokens button
|
|
112
|
+
document.getElementById('resetTokens').addEventListener('click', () => {
|
|
113
|
+
sessionTokens = 0;
|
|
114
|
+
updateTokenDisplay();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Clear errors button
|
|
118
|
+
document.getElementById('clearErrors').addEventListener('click', () => {
|
|
119
|
+
errorLogs = [];
|
|
120
|
+
updateErrorDisplay();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Intercept fetch to capture API calls
|
|
124
|
+
interceptFetch();
|
|
125
|
+
|
|
126
|
+
// Register with panel manager
|
|
127
|
+
if (window.UplinkPanels) {
|
|
128
|
+
window.UplinkPanels.register('activity', {
|
|
129
|
+
element: panel,
|
|
130
|
+
isOpen: () => panelVisible,
|
|
131
|
+
open: () => {
|
|
132
|
+
panelVisible = true;
|
|
133
|
+
panel.classList.add('visible');
|
|
134
|
+
},
|
|
135
|
+
close: () => {
|
|
136
|
+
panelVisible = false;
|
|
137
|
+
panel.classList.remove('visible');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (window.UplinkLogger?.debug) {
|
|
143
|
+
window.UplinkLogger.debug('Developer: Initialized');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function togglePanel() {
|
|
148
|
+
if (window.UplinkPanels) {
|
|
149
|
+
panelVisible = window.UplinkPanels.toggle('activity');
|
|
150
|
+
} else {
|
|
151
|
+
panelVisible = !panelVisible;
|
|
152
|
+
panel.classList.toggle('visible', panelVisible);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function switchTab(tabName) {
|
|
157
|
+
panel.querySelectorAll('.dev-tab').forEach(t => {
|
|
158
|
+
t.classList.toggle('active', t.dataset.tab === tabName);
|
|
159
|
+
});
|
|
160
|
+
panel.querySelectorAll('.dev-tab-content').forEach(c => {
|
|
161
|
+
c.classList.toggle('active', c.id === 'dev' + tabName.charAt(0).toUpperCase() + tabName.slice(1));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function interceptFetch() {
|
|
166
|
+
// Use FetchUtils hook system instead of monkey-patching window.fetch (H-25)
|
|
167
|
+
// This avoids double-wrapping when both fetch-utils.js and developer.js run
|
|
168
|
+
if (window.UplinkFetch) {
|
|
169
|
+
window.UplinkFetch.registerHook('afterResponse', async (url, options, response, duration) => {
|
|
170
|
+
if (url.startsWith('/api/')) {
|
|
171
|
+
try {
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
logApiCall(url, options, data, duration, response.status);
|
|
174
|
+
if (data.usage) updateTokens(data.usage);
|
|
175
|
+
if (data.toolCalls) logToolCalls(data.toolCalls);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
logApiCall(url, options, null, duration, response.status);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
window.UplinkFetch.registerHook('onError', async (url, options, error) => {
|
|
182
|
+
logError(error, url);
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Fallback: direct monkey-patch if FetchUtils not available
|
|
188
|
+
const originalFetch = window.fetch;
|
|
189
|
+
window.fetch = async function(...args) {
|
|
190
|
+
const url = typeof args[0] === 'string' ? args[0] : args[0].url;
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const response = await originalFetch.apply(this, args);
|
|
195
|
+
|
|
196
|
+
// Clone response to read body without consuming it
|
|
197
|
+
const cloned = response.clone();
|
|
198
|
+
|
|
199
|
+
// Log API calls to our endpoints
|
|
200
|
+
if (url.startsWith('/api/')) {
|
|
201
|
+
const duration = Date.now() - startTime;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const data = await cloned.json();
|
|
205
|
+
logApiCall(url, args[1], data, duration, response.status);
|
|
206
|
+
|
|
207
|
+
// Extract token usage if present
|
|
208
|
+
if (data.usage) {
|
|
209
|
+
updateTokens(data.usage);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract tool calls if present
|
|
213
|
+
if (data.toolCalls) {
|
|
214
|
+
logToolCalls(data.toolCalls);
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Not JSON, just log basic info
|
|
218
|
+
logApiCall(url, args[1], null, duration, response.status);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return response;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
logError(error, url);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function logApiCall(url, options, response, duration, status) {
|
|
231
|
+
const entry = {
|
|
232
|
+
timestamp: new Date().toISOString(),
|
|
233
|
+
url,
|
|
234
|
+
method: options?.method || 'GET',
|
|
235
|
+
status,
|
|
236
|
+
duration,
|
|
237
|
+
response: response ? JSON.stringify(response, null, 2) : null
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
apiLogs.unshift(entry);
|
|
241
|
+
if (apiLogs.length > MAX_LOGS) apiLogs.pop();
|
|
242
|
+
|
|
243
|
+
updateApiDisplay();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function logToolCalls(tools) {
|
|
247
|
+
tools.forEach(tool => {
|
|
248
|
+
toolCalls.unshift({
|
|
249
|
+
timestamp: new Date().toISOString(),
|
|
250
|
+
name: tool.name || tool.function?.name || 'unknown',
|
|
251
|
+
args: tool.arguments || tool.function?.arguments || {},
|
|
252
|
+
result: tool.result
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (toolCalls.length > MAX_LOGS) {
|
|
257
|
+
toolCalls = toolCalls.slice(0, MAX_LOGS);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
updateToolDisplay();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function logError(error, context = '') {
|
|
264
|
+
const msg = error.message || String(error);
|
|
265
|
+
|
|
266
|
+
// "Failed to fetch" = browser-level network error (offline, tab resume, tunnel flaky).
|
|
267
|
+
// Log to console for debugging but don't surface in error panel — not actionable for user.
|
|
268
|
+
// Real server errors (4xx/5xx) still show in the panel.
|
|
269
|
+
if (msg === 'Failed to fetch') {
|
|
270
|
+
console.warn('[Network]', context || 'fetch failed');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
errorLogs.unshift({
|
|
275
|
+
timestamp: new Date().toISOString(),
|
|
276
|
+
message: msg,
|
|
277
|
+
context,
|
|
278
|
+
stack: error.stack
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (errorLogs.length > MAX_LOGS) errorLogs.pop();
|
|
282
|
+
|
|
283
|
+
updateErrorDisplay();
|
|
284
|
+
|
|
285
|
+
// Flash the activity button to indicate error
|
|
286
|
+
const activityBtn = document.getElementById('activityBtn');
|
|
287
|
+
if (activityBtn) {
|
|
288
|
+
activityBtn.classList.add('has-error');
|
|
289
|
+
setTimeout(() => activityBtn.classList.remove('has-error'), 2000);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function updateTokens(usage) {
|
|
294
|
+
tokenUsage = {
|
|
295
|
+
prompt: usage.prompt_tokens || usage.promptTokens || 0,
|
|
296
|
+
completion: usage.completion_tokens || usage.completionTokens || 0,
|
|
297
|
+
total: usage.total_tokens || usage.totalTokens || 0
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
sessionTokens += tokenUsage.total;
|
|
301
|
+
updateTokenDisplay();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function updateTokenDisplay() {
|
|
305
|
+
document.getElementById('tokenPrompt').textContent = tokenUsage.prompt.toLocaleString();
|
|
306
|
+
document.getElementById('tokenCompletion').textContent = tokenUsage.completion.toLocaleString();
|
|
307
|
+
document.getElementById('tokenTotal').textContent = tokenUsage.total.toLocaleString();
|
|
308
|
+
document.getElementById('tokenSession').textContent = sessionTokens.toLocaleString();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updateApiDisplay() {
|
|
312
|
+
const list = document.getElementById('apiLogList');
|
|
313
|
+
if (apiLogs.length === 0) {
|
|
314
|
+
list.innerHTML = '<div class="empty-log">No API calls yet</div>';
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
list.innerHTML = apiLogs.map(log => `
|
|
319
|
+
<div class="api-log-entry">
|
|
320
|
+
<div class="api-log-header">
|
|
321
|
+
<span class="api-method ${escapeHtml(log.method)}">${escapeHtml(log.method)}</span>
|
|
322
|
+
<span class="api-url">${escapeHtml(log.url)}</span>
|
|
323
|
+
<span class="api-status status-${Math.floor(log.status/100)}">${log.status}</span>
|
|
324
|
+
<span class="api-duration">${log.duration}ms</span>
|
|
325
|
+
</div>
|
|
326
|
+
${log.response ? `
|
|
327
|
+
<details class="api-response">
|
|
328
|
+
<summary>Response</summary>
|
|
329
|
+
<pre>${escapeHtml(truncate(log.response, 500))}</pre>
|
|
330
|
+
</details>
|
|
331
|
+
` : ''}
|
|
332
|
+
</div>
|
|
333
|
+
`).join('');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function updateToolDisplay() {
|
|
337
|
+
const list = document.getElementById('toolList');
|
|
338
|
+
if (toolCalls.length === 0) {
|
|
339
|
+
list.innerHTML = '<div class="empty-log">No tool calls yet</div>';
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
list.innerHTML = toolCalls.map(tool => `
|
|
344
|
+
<div class="tool-entry">
|
|
345
|
+
<div class="tool-header">
|
|
346
|
+
<span class="tool-name">🔧 ${escapeHtml(tool.name)}</span>
|
|
347
|
+
<span class="tool-time">${formatTime(tool.timestamp)}</span>
|
|
348
|
+
</div>
|
|
349
|
+
${tool.args ? `
|
|
350
|
+
<details class="tool-args">
|
|
351
|
+
<summary>Arguments</summary>
|
|
352
|
+
<pre>${escapeHtml(JSON.stringify(tool.args, null, 2))}</pre>
|
|
353
|
+
</details>
|
|
354
|
+
` : ''}
|
|
355
|
+
</div>
|
|
356
|
+
`).join('');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function updateErrorDisplay() {
|
|
360
|
+
const list = document.getElementById('errorList');
|
|
361
|
+
if (errorLogs.length === 0) {
|
|
362
|
+
list.innerHTML = '<div class="empty-log">No errors 🎉</div>';
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
list.innerHTML = errorLogs.map(err => `
|
|
367
|
+
<div class="error-entry">
|
|
368
|
+
<div class="error-header">
|
|
369
|
+
<span class="error-message">${escapeHtml(err.message)}</span>
|
|
370
|
+
<span class="error-time">${formatTime(err.timestamp)}</span>
|
|
371
|
+
</div>
|
|
372
|
+
${err.context ? `<div class="error-context">${escapeHtml(err.context)}</div>` : ''}
|
|
373
|
+
</div>
|
|
374
|
+
`).join('');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Utilities
|
|
378
|
+
function escapeHtml(str) {
|
|
379
|
+
if (!str) return '';
|
|
380
|
+
return str.replace(/[&<>"']/g, m => ({
|
|
381
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
382
|
+
})[m]);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function truncate(str, len) {
|
|
386
|
+
if (str.length <= len) return str;
|
|
387
|
+
return str.slice(0, len) + '...\n[truncated]';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function formatTime(iso) {
|
|
391
|
+
return new Date(iso).toLocaleTimeString();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Log a simple tool name (from streaming indicator)
|
|
395
|
+
function logTool(toolName) {
|
|
396
|
+
toolCalls.unshift({
|
|
397
|
+
timestamp: new Date().toISOString(),
|
|
398
|
+
name: toolName,
|
|
399
|
+
args: null,
|
|
400
|
+
result: null,
|
|
401
|
+
streaming: true
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (toolCalls.length > MAX_LOGS) {
|
|
405
|
+
toolCalls = toolCalls.slice(0, MAX_LOGS);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
updateToolDisplay();
|
|
409
|
+
|
|
410
|
+
// Flash the activity button to indicate tool usage
|
|
411
|
+
const activityBtn = document.getElementById('activityBtn');
|
|
412
|
+
if (activityBtn) {
|
|
413
|
+
activityBtn.classList.add('has-tool');
|
|
414
|
+
setTimeout(() => activityBtn.classList.remove('has-tool'), 1000);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Expose for external use
|
|
419
|
+
export const UplinkDeveloper = {
|
|
420
|
+
show: () => { panelVisible = true; panel.classList.add('visible'); },
|
|
421
|
+
hide: () => { panelVisible = false; panel.classList.remove('visible'); },
|
|
422
|
+
logError,
|
|
423
|
+
logTool,
|
|
424
|
+
logToolCall: (name, args, result) => logToolCalls([{ name, arguments: args, result }]),
|
|
425
|
+
updateTokens
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Backward compat: assign to window
|
|
429
|
+
window.UplinkDeveloper = UplinkDeveloper;
|
|
430
|
+
|
|
431
|
+
// Register with core for coordinated initialization
|
|
432
|
+
UplinkCore.registerModule('developer', init);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// ENCRYPTION MODULE
|
|
3
|
+
// Web Crypto API utilities for encrypted storage
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
import { UplinkCore } from './core.js';
|
|
7
|
+
|
|
8
|
+
const SALT_KEY = 'uplink-salt';
|
|
9
|
+
const CRYPTO_ALGO = 'AES-GCM';
|
|
10
|
+
const PBKDF2_ITERATIONS = 600000; // OWASP 2023 recommendation for SHA-256
|
|
11
|
+
const PBKDF2_ITERATIONS_LEGACY = 100000; // Legacy iteration count for migration
|
|
12
|
+
|
|
13
|
+
// Derive encryption key from password
|
|
14
|
+
async function deriveKey(password, salt, iterations = PBKDF2_ITERATIONS) {
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
17
|
+
'raw',
|
|
18
|
+
encoder.encode(password),
|
|
19
|
+
'PBKDF2',
|
|
20
|
+
false,
|
|
21
|
+
['deriveKey']
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return crypto.subtle.deriveKey(
|
|
25
|
+
{
|
|
26
|
+
name: 'PBKDF2',
|
|
27
|
+
salt: salt,
|
|
28
|
+
iterations: iterations,
|
|
29
|
+
hash: 'SHA-256'
|
|
30
|
+
},
|
|
31
|
+
keyMaterial,
|
|
32
|
+
{ name: CRYPTO_ALGO, length: 256 },
|
|
33
|
+
false,
|
|
34
|
+
['encrypt', 'decrypt']
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Encrypt data
|
|
39
|
+
async function encrypt(data, password) {
|
|
40
|
+
let salt = localStorage.getItem(SALT_KEY);
|
|
41
|
+
if (!salt) {
|
|
42
|
+
const newSalt = crypto.getRandomValues(new Uint8Array(16));
|
|
43
|
+
salt = btoa(String.fromCharCode(...newSalt));
|
|
44
|
+
localStorage.setItem(SALT_KEY, salt);
|
|
45
|
+
}
|
|
46
|
+
const saltBytes = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
|
|
47
|
+
|
|
48
|
+
const key = await deriveKey(password, saltBytes);
|
|
49
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
50
|
+
const encoder = new TextEncoder();
|
|
51
|
+
|
|
52
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
53
|
+
{ name: CRYPTO_ALGO, iv: iv },
|
|
54
|
+
key,
|
|
55
|
+
encoder.encode(JSON.stringify(data))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
v: 1, // Version flag for PBKDF2 iteration count
|
|
60
|
+
iv: btoa(String.fromCharCode(...iv)),
|
|
61
|
+
data: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Decrypt data
|
|
66
|
+
async function decrypt(encryptedObj, password) {
|
|
67
|
+
const salt = localStorage.getItem(SALT_KEY);
|
|
68
|
+
if (!salt) throw new Error('No salt found');
|
|
69
|
+
|
|
70
|
+
const saltBytes = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
|
|
71
|
+
|
|
72
|
+
// Determine iteration count based on version (v1 = 600k, undefined = legacy 100k)
|
|
73
|
+
const version = encryptedObj.v;
|
|
74
|
+
const iterations = version === 1 ? PBKDF2_ITERATIONS : PBKDF2_ITERATIONS_LEGACY;
|
|
75
|
+
|
|
76
|
+
const key = await deriveKey(password, saltBytes, iterations);
|
|
77
|
+
const iv = Uint8Array.from(atob(encryptedObj.iv), c => c.charCodeAt(0));
|
|
78
|
+
const data = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0));
|
|
79
|
+
|
|
80
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
81
|
+
{ name: CRYPTO_ALGO, iv: iv },
|
|
82
|
+
key,
|
|
83
|
+
data
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Verify password by attempting decryption
|
|
91
|
+
async function verifyPassword(password) {
|
|
92
|
+
try {
|
|
93
|
+
const encrypted = localStorage.getItem('uplink-history-encrypted');
|
|
94
|
+
if (encrypted) {
|
|
95
|
+
await decrypt(JSON.parse(encrypted), password);
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clear encryption data
|
|
104
|
+
function clearEncryption() {
|
|
105
|
+
localStorage.removeItem('uplink-history-encrypted');
|
|
106
|
+
localStorage.removeItem(SALT_KEY);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Export API
|
|
110
|
+
export const UplinkEncryption = {
|
|
111
|
+
encrypt,
|
|
112
|
+
decrypt,
|
|
113
|
+
verifyPassword,
|
|
114
|
+
clearEncryption,
|
|
115
|
+
SALT_KEY
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export { encrypt, decrypt, verifyPassword, clearEncryption, SALT_KEY };
|
|
119
|
+
|
|
120
|
+
// Backward compat: assign to window
|
|
121
|
+
window.UplinkEncryption = UplinkEncryption;
|
|
122
|
+
|
|
123
|
+
// Register with core if available
|
|
124
|
+
UplinkCore.registerModule('encryption');
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// ERROR MESSAGES MODULE
|
|
3
|
+
// User-friendly error messages
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map technical errors to user-friendly messages
|
|
8
|
+
*/
|
|
9
|
+
const errorMessages = {
|
|
10
|
+
// Network errors
|
|
11
|
+
'Failed to fetch': 'Unable to connect to the server. Check your internet connection.',
|
|
12
|
+
'NetworkError': 'Network error. Please check your connection and try again.',
|
|
13
|
+
'net::ERR_CONNECTION_REFUSED': 'Server is not responding. Is Uplink running?',
|
|
14
|
+
'ECONNREFUSED': 'Cannot connect to the AI gateway. Check your gateway settings.',
|
|
15
|
+
|
|
16
|
+
// Gateway errors
|
|
17
|
+
'Gateway error: 401': 'Authentication failed. Check your gateway token in settings.',
|
|
18
|
+
'Gateway error: 403': 'Access denied. Your gateway token may be invalid.',
|
|
19
|
+
'Gateway error: 404': 'Gateway endpoint not found. Check your gateway URL.',
|
|
20
|
+
'Gateway error: 429': 'Too many requests. Please wait a moment and try again.',
|
|
21
|
+
'Gateway error: 500': 'The AI service encountered an error. Try again in a moment.',
|
|
22
|
+
'Gateway error: 502': 'Gateway is temporarily unavailable. Try again shortly.',
|
|
23
|
+
'Gateway error: 503': 'AI service is overloaded. Please try again later.',
|
|
24
|
+
|
|
25
|
+
// Timeout errors
|
|
26
|
+
'Stream read timed out': 'Response took too long. The AI might be busy with a complex task.',
|
|
27
|
+
'timeout': 'Request timed out. Try a shorter message or try again.',
|
|
28
|
+
'AbortError': 'Request was cancelled.',
|
|
29
|
+
|
|
30
|
+
// TTS errors
|
|
31
|
+
'ElevenLabs error: 401': 'Voice API key is invalid. Check your ElevenLabs settings.',
|
|
32
|
+
'ElevenLabs error: 429': 'Voice quota exceeded. Try again later or check your plan.',
|
|
33
|
+
'No text to speak': 'Nothing to read aloud.',
|
|
34
|
+
|
|
35
|
+
// Whisper/transcription errors
|
|
36
|
+
'Whisper API error': 'Speech recognition failed. Try speaking again.',
|
|
37
|
+
|
|
38
|
+
// WebSocket errors
|
|
39
|
+
'WebSocket': 'Real-time connection lost. Reconnecting...',
|
|
40
|
+
|
|
41
|
+
// Generic
|
|
42
|
+
'Unknown error': 'Something went wrong. Please try again.'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get user-friendly error message
|
|
47
|
+
* @param {Error|string} error - The error object or message
|
|
48
|
+
* @returns {string} User-friendly message
|
|
49
|
+
*/
|
|
50
|
+
export function getFriendlyMessage(error) {
|
|
51
|
+
const message = error?.message || String(error);
|
|
52
|
+
|
|
53
|
+
// Check for exact matches first
|
|
54
|
+
if (errorMessages[message]) {
|
|
55
|
+
return errorMessages[message];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for partial matches
|
|
59
|
+
for (const [key, friendly] of Object.entries(errorMessages)) {
|
|
60
|
+
if (message.includes(key)) {
|
|
61
|
+
return friendly;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for HTTP status codes
|
|
66
|
+
const statusMatch = message.match(/(\d{3})/);
|
|
67
|
+
if (statusMatch) {
|
|
68
|
+
const status = statusMatch[1];
|
|
69
|
+
switch (status) {
|
|
70
|
+
case '400': return 'Invalid request. Please try again.';
|
|
71
|
+
case '401': return 'Authentication required. Check your settings.';
|
|
72
|
+
case '403': return 'Access denied.';
|
|
73
|
+
case '404': return 'Service not found. Check your configuration.';
|
|
74
|
+
case '429': return 'Too many requests. Please wait and try again.';
|
|
75
|
+
case '500': return 'Server error. Please try again later.';
|
|
76
|
+
case '502': return 'Service temporarily unavailable.';
|
|
77
|
+
case '503': return 'Service overloaded. Please try again later.';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If message is already user-friendly (no technical jargon), return it
|
|
82
|
+
if (!message.includes('Error:') &&
|
|
83
|
+
!message.includes('::') &&
|
|
84
|
+
!message.includes('ECONNREFUSED') &&
|
|
85
|
+
message.length < 100) {
|
|
86
|
+
return message;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback
|
|
90
|
+
return 'Something went wrong. Please try again.';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get error with action suggestion
|
|
95
|
+
* @param {Error|string} error - The error
|
|
96
|
+
* @returns {{ message: string, action?: string }}
|
|
97
|
+
*/
|
|
98
|
+
export function getErrorWithAction(error) {
|
|
99
|
+
const message = getFriendlyMessage(error);
|
|
100
|
+
const errorStr = error?.message || String(error);
|
|
101
|
+
|
|
102
|
+
let action = null;
|
|
103
|
+
|
|
104
|
+
if (errorStr.includes('401') || errorStr.includes('403')) {
|
|
105
|
+
action = 'Check Settings';
|
|
106
|
+
} else if (errorStr.includes('ECONNREFUSED') || errorStr.includes('Failed to fetch')) {
|
|
107
|
+
action = 'Check Connection';
|
|
108
|
+
} else if (errorStr.includes('429')) {
|
|
109
|
+
action = 'Wait & Retry';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { message, action };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Export API
|
|
116
|
+
export const UplinkErrors = {
|
|
117
|
+
getFriendlyMessage,
|
|
118
|
+
getErrorWithAction
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Backward compat: assign to window
|
|
122
|
+
window.UplinkErrors = UplinkErrors;
|