@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
package/server/config.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Module - Environment variables and constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { createLogger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
const log = createLogger('config');
|
|
11
|
+
|
|
12
|
+
// Load environment variables
|
|
13
|
+
config();
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
export const ROOT_DIR = path.dirname(__dirname);
|
|
17
|
+
|
|
18
|
+
// Server
|
|
19
|
+
export const PORT = process.env.PORT || 3456;
|
|
20
|
+
|
|
21
|
+
// Gateway
|
|
22
|
+
export const GATEWAY_URL = process.env.GATEWAY_URL || 'http://127.0.0.1:18789';
|
|
23
|
+
export const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN;
|
|
24
|
+
|
|
25
|
+
// TTS
|
|
26
|
+
export const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
|
|
27
|
+
export const ELEVENLABS_VOICE_ID = process.env.ELEVENLABS_VOICE_ID || 'OPnx2r3vaTlo7wQIPhcM';
|
|
28
|
+
export const TTS_VOICE_NAME = process.env.TTS_VOICE_NAME || 'Assistant';
|
|
29
|
+
|
|
30
|
+
// Session
|
|
31
|
+
export const SESSION_USER = process.env.SESSION_USER || 'uplink-user';
|
|
32
|
+
export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Assistant';
|
|
33
|
+
|
|
34
|
+
// Whisper
|
|
35
|
+
export const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
36
|
+
export const TRANSCRIBE_SCRIPT = process.env.TRANSCRIBE_SCRIPT || '/usr/local/bin/transcribe.sh';
|
|
37
|
+
|
|
38
|
+
// Wake word
|
|
39
|
+
export const WAKE_WORD = (process.env.WAKE_WORD || 'uplink').toLowerCase();
|
|
40
|
+
|
|
41
|
+
// Webhooks
|
|
42
|
+
export const WEBHOOK_TOKEN = process.env.WEBHOOK_TOKEN;
|
|
43
|
+
|
|
44
|
+
// OpenClaw Channel Integration
|
|
45
|
+
export const OPENCLAW_CALLBACK_SECRET = process.env.OPENCLAW_CALLBACK_SECRET || '';
|
|
46
|
+
export const OPENCLAW_WEBHOOK_URL = process.env.OPENCLAW_WEBHOOK_URL || ''; // e.g., http://localhost:18789/uplink-webhook
|
|
47
|
+
export const USE_CHANNEL_WEBHOOK = process.env.USE_CHANNEL_WEBHOOK === 'true'; // Enable to route through channel plugin
|
|
48
|
+
|
|
49
|
+
// CORS
|
|
50
|
+
export const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS
|
|
51
|
+
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
|
|
52
|
+
: ['http://localhost:3456'];
|
|
53
|
+
|
|
54
|
+
// Security: Allowed file types (magic bytes)
|
|
55
|
+
export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
56
|
+
export const ALLOWED_AUDIO_TYPES = ['audio/webm', 'audio/mp4', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'video/webm'];
|
|
57
|
+
export const ALLOWED_VIDEO_TYPES = ['video/webm', 'video/mp4', 'video/quicktime', 'video/x-matroska'];
|
|
58
|
+
|
|
59
|
+
// Reliability
|
|
60
|
+
export const MAX_CONCURRENT_REQUESTS = 3;
|
|
61
|
+
export const REQUEST_TIMEOUT = 300000; // 5 minutes (for longer responses with thinking)
|
|
62
|
+
export const CHANNEL_FETCH_TIMEOUT_MS = 300000; // 5 minutes for initial fetch
|
|
63
|
+
export const STREAM_READ_TIMEOUT_MS = 300000; // 5 minutes between stream chunks (allows for tool use)
|
|
64
|
+
export const RPC_CALL_TIMEOUT_MS = 10000; // 10 seconds for gateway RPC calls
|
|
65
|
+
export const SSE_KEEPALIVE_INTERVAL_MS = 5000; // 5 seconds between SSE keepalive comments
|
|
66
|
+
export const GATEWAY_RESTART_DELAY_MS = 2000; // 2 seconds delay before gateway restart
|
|
67
|
+
export const FILE_EXTRACTION_TIMEOUT_MS = 30000; // 30 seconds for file text extraction
|
|
68
|
+
export const MAX_FILE_EXTRACT_SIZE = 500 * 1024; // 500KB max extracted text
|
|
69
|
+
export const IMAGE_COMPRESSION_THRESHOLD = 500000; // 500KB threshold for image compression
|
|
70
|
+
export const GATEWAY_VALIDATION_TIMEOUT_MS = 5000; // 5 seconds for gateway validation
|
|
71
|
+
export const FILE_CLEANUP_DELAY_MS = 15000; // 15 seconds before cleaning up uploaded files
|
|
72
|
+
export const GATEWAY_HEALTH_CHECK_TIMEOUT_MS = 10000; // 10 seconds for gateway health checks
|
|
73
|
+
export const MIN_RECONNECT_INTERVAL_MS = 1000; // Minimum reconnect interval (1 second)
|
|
74
|
+
|
|
75
|
+
// WebSocket Configuration
|
|
76
|
+
export const WEBSOCKET = {
|
|
77
|
+
heartbeatIntervalMs: 30 * 1000, // 30 seconds (relaxed for mobile/cellular)
|
|
78
|
+
maxMissedPongs: 4, // Close connection after 4 missed pongs (2 min timeout)
|
|
79
|
+
syncDeltaThrottleMs: 150, // Throttle sync deltas to 150ms
|
|
80
|
+
messageRateLimitWindow: 60 * 1000, // 1 minute rate limit window
|
|
81
|
+
maxMessagesPerWindow: 30, // Max 30 messages per minute per client
|
|
82
|
+
maxRateLimitEntries: 500, // Max rate limit entries to prevent unbounded growth
|
|
83
|
+
maxStreamingRequests: 100, // Max active streaming requests
|
|
84
|
+
maxRecentBroadcasts: 500, // Max broadcast dedup cache size
|
|
85
|
+
dedupWindowMs: 5000, // 5 second dedup window for broadcasts
|
|
86
|
+
connectionLimits: {
|
|
87
|
+
maxPerIp: 10, // Max connections per IP
|
|
88
|
+
maxTotal: 100, // Max total connections
|
|
89
|
+
},
|
|
90
|
+
maxMessageSize: 1024 * 1024, // 1MB message size limit
|
|
91
|
+
broadcastCircuitBreaker: {
|
|
92
|
+
threshold: 10, // Open circuit after 10 failures
|
|
93
|
+
resetTimeMs: 30000, // Reset after 30 seconds
|
|
94
|
+
},
|
|
95
|
+
syncBroadcastRateLimit: {
|
|
96
|
+
maxPerSecond: 10, // Max 10 sync broadcasts per second
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Directories
|
|
101
|
+
export const AUDIO_DIR = path.join(ROOT_DIR, 'public', 'audio');
|
|
102
|
+
export const UPLOADS_DIR = path.join(ROOT_DIR, 'uploads');
|
|
103
|
+
export const SHARES_DIR = path.join(ROOT_DIR, 'shared-conversations');
|
|
104
|
+
export const SYNC_DIR = path.join(ROOT_DIR, 'sync-data');
|
|
105
|
+
export const ACTIVITY_FILE = path.join(ROOT_DIR, 'activity.json');
|
|
106
|
+
export const MESSAGES_FILE = path.join(ROOT_DIR, 'messages-sync.json');
|
|
107
|
+
|
|
108
|
+
// Agent media configuration
|
|
109
|
+
// Allowed directories for agent-generated media (security whitelist)
|
|
110
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
111
|
+
export const AGENT_MEDIA_DIRS = [
|
|
112
|
+
// OpenClaw media directory (screenshots, TTS, generated files)
|
|
113
|
+
process.env.OPENCLAW_MEDIA_DIR || path.join(homeDir, '.openclaw', 'media'),
|
|
114
|
+
// Workspace artifacts directory
|
|
115
|
+
path.join(ROOT_DIR, 'artifacts'),
|
|
116
|
+
// Uploads directory (for compatibility)
|
|
117
|
+
UPLOADS_DIR,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Allowed file extensions for agent media (security whitelist)
|
|
121
|
+
export const AGENT_MEDIA_ALLOWED_EXTENSIONS = [
|
|
122
|
+
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', // Images
|
|
123
|
+
'.pdf', // Documents
|
|
124
|
+
'.mp3', '.wav', '.ogg', '.m4a', // Audio
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
// Maximum agent media file size (10MB)
|
|
128
|
+
export const AGENT_MEDIA_MAX_SIZE = 10 * 1024 * 1024;
|
|
129
|
+
|
|
130
|
+
// Cleanup
|
|
131
|
+
export const AUDIO_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
132
|
+
export const UPLOADS_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
133
|
+
|
|
134
|
+
// Limits
|
|
135
|
+
export const MAX_ACTIVITY_ITEMS = 100;
|
|
136
|
+
export const MAX_SYNC_MESSAGES = 50;
|
|
137
|
+
|
|
138
|
+
// Input length validation limits (security)
|
|
139
|
+
export const MAX_INPUT_LENGTHS = {
|
|
140
|
+
MESSAGE: 50000, // Chat messages, webhook messages
|
|
141
|
+
CAPTION: 500, // Image/video captions
|
|
142
|
+
SATELLITE_ID: 100, // Satellite identifier
|
|
143
|
+
ACTION: 100, // Trigger action names
|
|
144
|
+
TITLE: 200, // Notification titles
|
|
145
|
+
BODY: 5000, // Notification bodies
|
|
146
|
+
TYPE: 32, // Activity type
|
|
147
|
+
SUMMARY: 500, // Activity summary
|
|
148
|
+
DETAILS: 5000, // Activity details
|
|
149
|
+
SOURCE: 50, // Webhook source name
|
|
150
|
+
METADATA_KEY: 100, // Metadata object keys
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Warnings
|
|
154
|
+
if (!GATEWAY_TOKEN) {
|
|
155
|
+
log.error('⚠️ GATEWAY_TOKEN not set. Create a .env file with your gateway token.');
|
|
156
|
+
}
|
|
157
|
+
if (!process.env.TRANSCRIBE_SCRIPT) {
|
|
158
|
+
log.warn('⚠️ TRANSCRIBE_SCRIPT not set. Using default:', TRANSCRIBE_SCRIPT);
|
|
159
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Module - Standardized request context for route handlers
|
|
3
|
+
*
|
|
4
|
+
* Provides a clean, consistent API for route modules to access shared
|
|
5
|
+
* functionality without tight coupling to implementation details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { log, fetchWithTimeout } from './utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a standardized request context object for route handlers
|
|
12
|
+
*
|
|
13
|
+
* This replaces the ad-hoc baseDeps pattern with a clean interface that
|
|
14
|
+
* explicitly documents what's available to route handlers.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} options - Context dependencies
|
|
17
|
+
* @param {Object} options.config - Configuration constants
|
|
18
|
+
* @param {Function} options.log - Logging utility
|
|
19
|
+
* @param {Function} options.fetchWithTimeout - HTTP fetch with timeout
|
|
20
|
+
* @param {Function} options.transcribe - Speech-to-text
|
|
21
|
+
* @param {Function} options.chat - Chat function
|
|
22
|
+
* @param {Function} options.chatWithParallelTTS - Chat with parallel TTS
|
|
23
|
+
* @param {Function} options.generateTTS - Text-to-speech generation
|
|
24
|
+
* @param {Function} options.sendMessage - Unified message sending
|
|
25
|
+
* @param {Function} options.saveMessageToSync - Save message to sync file
|
|
26
|
+
* @param {Function} options.broadcastOpenClawPush - Broadcast OpenClaw push messages
|
|
27
|
+
* @param {Function} options.broadcastToAll - Broadcast to all WebSocket clients
|
|
28
|
+
* @param {Map} options.wsClients - WebSocket clients map
|
|
29
|
+
* @param {Function} options.verifyWebhookToken - Webhook auth middleware
|
|
30
|
+
* @param {Function} options.strictLimiter - Rate limiting middleware
|
|
31
|
+
* @param {Function} options.isPrivateIP - SSRF prevention
|
|
32
|
+
* @param {Function} options.loadConfig - Load runtime config
|
|
33
|
+
* @param {Function} options.getClientConfig - Get client-safe config
|
|
34
|
+
* @param {Function} options.saveConfig - Save runtime config
|
|
35
|
+
* @param {Function} options.needsOnboarding - Check if onboarding needed
|
|
36
|
+
* @param {Object} options.audioUpload - Multer audio upload handler
|
|
37
|
+
* @param {Object} options.imageUpload - Multer image upload handler
|
|
38
|
+
* @param {Object} options.videoUpload - Multer video upload handler
|
|
39
|
+
* @param {Object} options.fileUpload - Multer file upload handler
|
|
40
|
+
* @param {string} options.uploadsDir - Uploads directory path
|
|
41
|
+
* @param {string} options.tempDir - Temp directory path
|
|
42
|
+
* @param {Object} options.requestHelpers - Request tracking helpers
|
|
43
|
+
* @returns {Object} Standardized context object
|
|
44
|
+
*/
|
|
45
|
+
export function createRequestContext({
|
|
46
|
+
config,
|
|
47
|
+
log: logger,
|
|
48
|
+
fetchWithTimeout: fetch,
|
|
49
|
+
transcribe,
|
|
50
|
+
chat,
|
|
51
|
+
chatWithParallelTTS,
|
|
52
|
+
generateTTS,
|
|
53
|
+
sendMessage,
|
|
54
|
+
saveMessageToSync,
|
|
55
|
+
broadcastOpenClawPush,
|
|
56
|
+
broadcastToAll,
|
|
57
|
+
wsClients,
|
|
58
|
+
verifyWebhookToken,
|
|
59
|
+
strictLimiter,
|
|
60
|
+
isPrivateIP,
|
|
61
|
+
loadConfig,
|
|
62
|
+
getClientConfig,
|
|
63
|
+
saveConfig,
|
|
64
|
+
needsOnboarding,
|
|
65
|
+
audioUpload,
|
|
66
|
+
imageUpload,
|
|
67
|
+
videoUpload,
|
|
68
|
+
fileUpload,
|
|
69
|
+
uploadsDir,
|
|
70
|
+
tempDir,
|
|
71
|
+
requestHelpers,
|
|
72
|
+
}) {
|
|
73
|
+
return {
|
|
74
|
+
// Configuration
|
|
75
|
+
config,
|
|
76
|
+
|
|
77
|
+
// Core utilities
|
|
78
|
+
log: logger || log,
|
|
79
|
+
fetch: fetch || fetchWithTimeout,
|
|
80
|
+
|
|
81
|
+
// Chat functions
|
|
82
|
+
transcribe,
|
|
83
|
+
chat,
|
|
84
|
+
chatWithParallelTTS,
|
|
85
|
+
generateTTS,
|
|
86
|
+
sendMessage,
|
|
87
|
+
saveMessageToSync,
|
|
88
|
+
|
|
89
|
+
// WebSocket broadcasting
|
|
90
|
+
broadcastOpenClawPush,
|
|
91
|
+
broadcastToAll,
|
|
92
|
+
wsClients,
|
|
93
|
+
|
|
94
|
+
// Middleware
|
|
95
|
+
verifyWebhookToken,
|
|
96
|
+
strictLimiter,
|
|
97
|
+
isPrivateIP,
|
|
98
|
+
|
|
99
|
+
// Config management
|
|
100
|
+
loadConfig,
|
|
101
|
+
getClientConfig,
|
|
102
|
+
saveConfig,
|
|
103
|
+
needsOnboarding,
|
|
104
|
+
|
|
105
|
+
// File uploads
|
|
106
|
+
audioUpload,
|
|
107
|
+
imageUpload,
|
|
108
|
+
videoUpload,
|
|
109
|
+
fileUpload,
|
|
110
|
+
|
|
111
|
+
// Directories
|
|
112
|
+
uploadsDir,
|
|
113
|
+
tempDir,
|
|
114
|
+
|
|
115
|
+
// Request tracking
|
|
116
|
+
requestHelpers,
|
|
117
|
+
|
|
118
|
+
// Backwards compatibility aliases (can be removed in future cleanup)
|
|
119
|
+
// These ensure existing route code doesn't break during migration
|
|
120
|
+
// Remove these once all routes are updated to use the context object directly
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract request helpers from context for backwards compatibility
|
|
126
|
+
* @param {Object} context - Request context
|
|
127
|
+
* @returns {Object} Request helpers
|
|
128
|
+
*/
|
|
129
|
+
export function getRequestHelpers(context) {
|
|
130
|
+
return context.requestHelpers;
|
|
131
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Commands - Route slash commands through the gateway WebSocket RPC
|
|
3
|
+
*
|
|
4
|
+
* The /v1/chat/completions HTTP endpoint doesn't process slash commands.
|
|
5
|
+
* Commands like /compact need the gateway's reply pipeline which only
|
|
6
|
+
* runs through WebSocket RPC (chat.send).
|
|
7
|
+
*
|
|
8
|
+
* Protocol: JSON-RPC over WebSocket
|
|
9
|
+
* 1. Connect to ws://localhost:18789/ws
|
|
10
|
+
* 2. Send connect request with auth token
|
|
11
|
+
* 3. Send chat.send with message and sessionKey
|
|
12
|
+
* 4. Collect response events until lifecycle.end
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import WebSocket from 'ws';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
import { log } from './utils.js';
|
|
18
|
+
import { loadConfig } from './runtime-config.js';
|
|
19
|
+
|
|
20
|
+
const STATIC_GATEWAY_WS_URL = process.env.GATEWAY_WS_URL || 'ws://localhost:18789/ws';
|
|
21
|
+
const STATIC_GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || '';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get gateway WS URL and token dynamically (includes auto-discovered values)
|
|
25
|
+
*/
|
|
26
|
+
async function getGatewayWsConfig() {
|
|
27
|
+
try {
|
|
28
|
+
const config = await loadConfig();
|
|
29
|
+
const gatewayUrl = config.gatewayUrl || 'http://127.0.0.1:18789';
|
|
30
|
+
// Convert HTTP URL to WS URL
|
|
31
|
+
const wsUrl = gatewayUrl.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:') + '/ws';
|
|
32
|
+
return {
|
|
33
|
+
wsUrl: process.env.GATEWAY_WS_URL || wsUrl,
|
|
34
|
+
token: config.gatewayToken || STATIC_GATEWAY_TOKEN,
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return { wsUrl: STATIC_GATEWAY_WS_URL, token: STATIC_GATEWAY_TOKEN };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Gateway slash commands that should be routed via RPC
|
|
42
|
+
const GATEWAY_COMMANDS = new Set([
|
|
43
|
+
'help', 'commands', 'skill', 'status', 'allowlist', 'approve',
|
|
44
|
+
'context', 'tts', 'whoami', 'subagents', 'config', 'debug',
|
|
45
|
+
'usage', 'stop', 'restart', 'activation', 'send', 'reset',
|
|
46
|
+
'new', 'compact', 'think', 'verbose', 'reasoning', 'elevated',
|
|
47
|
+
'exec', 'model', 'models', 'queue', 'bash',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a message is a gateway slash command
|
|
52
|
+
*/
|
|
53
|
+
export function isGatewayCommand(message) {
|
|
54
|
+
if (!message || !message.startsWith('/')) return false;
|
|
55
|
+
const cmd = message.slice(1).split(/\s/)[0].toLowerCase();
|
|
56
|
+
return GATEWAY_COMMANDS.has(cmd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send a JSON-RPC request over a WebSocket
|
|
61
|
+
*/
|
|
62
|
+
function sendRequest(ws, method, params) {
|
|
63
|
+
const id = randomUUID();
|
|
64
|
+
const frame = { type: 'req', id, method, params };
|
|
65
|
+
ws.send(JSON.stringify(frame));
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send a gateway command via WebSocket RPC (chat.send)
|
|
71
|
+
*
|
|
72
|
+
* @param {string} command - The slash command (e.g. "/compact")
|
|
73
|
+
* @param {string} sessionKey - Session key
|
|
74
|
+
* @param {number} timeoutMs - Timeout (default 60s)
|
|
75
|
+
* @returns {Promise<{response: string}>}
|
|
76
|
+
*/
|
|
77
|
+
export async function sendGatewayCommand(command, sessionKey, timeoutMs = 60000) {
|
|
78
|
+
const gw = await getGatewayWsConfig();
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
let ws;
|
|
82
|
+
let response = '';
|
|
83
|
+
let settled = false;
|
|
84
|
+
let connectId = null;
|
|
85
|
+
let chatId = null;
|
|
86
|
+
let timer;
|
|
87
|
+
|
|
88
|
+
const finish = (result) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
try { ws?.close(); } catch (e) {}
|
|
93
|
+
resolve(result);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const fail = (err) => {
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
try { ws?.close(); } catch (e) {}
|
|
101
|
+
reject(err);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
timer = setTimeout(() => {
|
|
105
|
+
finish({ response: response || 'Command sent (timeout).' });
|
|
106
|
+
}, timeoutMs);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
ws = new WebSocket(gw.wsUrl);
|
|
110
|
+
|
|
111
|
+
ws.on('open', () => {
|
|
112
|
+
log('debug', `[GatewayCmd] Connected to ${gw.wsUrl}, sending connect...`);
|
|
113
|
+
// Step 1: Send connect with auth
|
|
114
|
+
// Client ID must be a valid GATEWAY_CLIENT_ID constant
|
|
115
|
+
connectId = sendRequest(ws, 'connect', {
|
|
116
|
+
minProtocol: 3,
|
|
117
|
+
maxProtocol: 3,
|
|
118
|
+
client: {
|
|
119
|
+
id: 'gateway-client',
|
|
120
|
+
displayName: 'Uplink',
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
platform: process.platform,
|
|
123
|
+
mode: 'backend',
|
|
124
|
+
},
|
|
125
|
+
caps: [],
|
|
126
|
+
auth: gw.token ? { token: gw.token } : undefined,
|
|
127
|
+
role: 'operator',
|
|
128
|
+
scopes: ['operator.admin'],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
ws.on('message', (raw) => {
|
|
133
|
+
try {
|
|
134
|
+
const msg = JSON.parse(raw.toString());
|
|
135
|
+
|
|
136
|
+
// Handle RPC responses
|
|
137
|
+
if (msg.type === 'res') {
|
|
138
|
+
if (msg.id === connectId) {
|
|
139
|
+
if (msg.ok) {
|
|
140
|
+
log('debug', '[GatewayCmd] Connected OK, sending chat.send...');
|
|
141
|
+
// Step 2: Send the command via chat.send
|
|
142
|
+
chatId = sendRequest(ws, 'chat.send', {
|
|
143
|
+
sessionKey,
|
|
144
|
+
message: command,
|
|
145
|
+
idempotencyKey: `uplink-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
fail(new Error(`Gateway connect failed: ${msg.error?.message || 'unknown'}`));
|
|
149
|
+
}
|
|
150
|
+
} else if (msg.id === chatId) {
|
|
151
|
+
// chat.send response — the command has been processed
|
|
152
|
+
log('debug', `[GatewayCmd] chat.send response: ok=${msg.ok}`);
|
|
153
|
+
if (!msg.ok) {
|
|
154
|
+
finish({ response: `Error: ${msg.error?.message || 'Command failed'}` });
|
|
155
|
+
}
|
|
156
|
+
// Don't finish yet — wait for the agent events with the actual response
|
|
157
|
+
// But set a shorter timeout since the command was accepted
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
timer = setTimeout(() => {
|
|
160
|
+
finish({ response: response || 'Command executed.' });
|
|
161
|
+
}, 10000);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle events pushed by the gateway
|
|
166
|
+
if (msg.type === 'event') {
|
|
167
|
+
const event = msg.event;
|
|
168
|
+
const payload = msg.payload;
|
|
169
|
+
|
|
170
|
+
// Chat events contain the response
|
|
171
|
+
if (event === 'chat') {
|
|
172
|
+
const state = payload?.state;
|
|
173
|
+
|
|
174
|
+
// Delta events — accumulate text
|
|
175
|
+
if (state === 'delta') {
|
|
176
|
+
const text = payload?.message?.content?.[0]?.text || payload?.delta || '';
|
|
177
|
+
if (text) response += text;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Final event — complete response
|
|
181
|
+
if (state === 'final') {
|
|
182
|
+
const content = payload?.message?.content;
|
|
183
|
+
if (Array.isArray(content)) {
|
|
184
|
+
const text = content
|
|
185
|
+
.filter(c => c.type === 'text')
|
|
186
|
+
.map(c => c.text)
|
|
187
|
+
.join('\n');
|
|
188
|
+
if (text) response = text;
|
|
189
|
+
}
|
|
190
|
+
finish({ response: response || 'Command executed.' });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Non-JSON, skip
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
ws.on('error', (err) => {
|
|
200
|
+
fail(new Error(`Gateway WebSocket error: ${err.message}`));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
ws.on('close', () => {
|
|
204
|
+
finish({ response: response || 'Command executed.' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
} catch (err) {
|
|
208
|
+
fail(err);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|