commit caae91ec55b044eb1f3a8d9597c6a7d3ad48649e Author: Zhongwei Li Date: Sat Nov 29 18:52:50 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c34e5f4 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "websocket-server-builder", + "description": "Build WebSocket servers for real-time bidirectional communication", + "version": "1.0.0", + "author": { + "name": "Jeremy Longshore", + "email": "[email protected]" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..565b310 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# websocket-server-builder + +Build WebSocket servers for real-time bidirectional communication diff --git a/commands/build-websocket-server.md b/commands/build-websocket-server.md new file mode 100644 index 0000000..e0fa347 --- /dev/null +++ b/commands/build-websocket-server.md @@ -0,0 +1,1255 @@ +--- +description: Build WebSocket servers for real-time bidirectional communication +shortcut: ws +--- + +# Build WebSocket Server + +Automatically generate production-ready WebSocket servers with Socket.IO or native WebSocket implementations featuring room management, authentication, broadcasting, presence tracking, and resilient connection handling for real-time applications. + +## When to Use This Command + +Use `/build-websocket-server` when you need to: +- Build real-time chat applications or messaging systems +- Implement live collaboration features (Google Docs-style) +- Create real-time dashboards with live data updates +- Build multiplayer game servers +- Stream live data feeds (stock prices, sports scores) +- Implement real-time notifications and alerts + +DON'T use this when: +- Simple request-response patterns suffice (use REST) +- Unidirectional server-to-client updates only (consider SSE) +- Very high throughput binary streaming (consider WebRTC) +- Message ordering is critical (consider message queues) + +## Design Decisions + +This command implements **Socket.IO** as the primary approach because: +- Automatic fallback to long-polling for compatibility +- Built-in room and namespace management +- Automatic reconnection with exponential backoff +- Binary data support with automatic serialization +- Event acknowledgments and timeouts +- Extensive middleware ecosystem + +**Alternative considered: Native WebSocket (ws)** +- Lower overhead and better performance +- No automatic fallbacks +- Manual implementation of features +- Recommended for simple, high-performance needs + +**Alternative considered: Server-Sent Events (SSE)** +- Simpler for unidirectional communication +- Works over HTTP/2 +- No bidirectional support +- Recommended for news feeds, notifications + +## Prerequisites + +Before running this command: +1. Choose Socket.IO vs native WebSocket +2. Design event/message protocol +3. Plan authentication strategy +4. Define room/channel structure +5. Determine scaling approach (Redis adapter for multi-server) + +## Implementation Process + +### Step 1: Initialize WebSocket Server +Set up Socket.IO or ws server with proper configuration. + +### Step 2: Implement Authentication +Add middleware for connection authentication and authorization. + +### Step 3: Define Event Handlers +Create handlers for all client events and server broadcasts. + +### Step 4: Add Room Management +Implement room joining, leaving, and broadcasting logic. + +### Step 5: Configure Resilience +Set up reconnection, heartbeat, and error handling. + +## Output Format + +The command generates: +- `websocket/server.js` - Main WebSocket server setup +- `websocket/handlers/` - Event handler modules +- `websocket/middleware/` - Auth and validation middleware +- `websocket/rooms/` - Room management logic +- `websocket/presence/` - User presence tracking +- `websocket/client.js` - Client SDK/library +- `config/websocket.json` - Server configuration +- `tests/websocket/` - Integration tests + +## Code Examples + +### Example 1: Full-Featured Chat Server with Socket.IO + +```javascript +// websocket/server.js - Socket.IO server with all features +const { Server } = require('socket.io'); +const { createAdapter } = require('@socket.io/redis-adapter'); +const { instrument } = require('@socket.io/admin-ui'); +const jwt = require('jsonwebtoken'); +const Redis = require('ioredis'); +const crypto = require('crypto'); + +class WebSocketServer { + constructor(httpServer, config = {}) { + this.config = { + cors: { + origin: config.corsOrigin || ['http://localhost:3000'], + credentials: true + }, + pingTimeout: config.pingTimeout || 60000, + pingInterval: config.pingInterval || 25000, + transports: config.transports || ['websocket', 'polling'], + maxHttpBufferSize: config.maxHttpBufferSize || 1e6, // 1MB + ...config + }; + + this.io = new Server(httpServer, this.config); + this.redis = new Redis(config.redisUrl); + this.pubClient = this.redis; + this.subClient = this.redis.duplicate(); + + this.rooms = new Map(); + this.users = new Map(); + this.presence = new Map(); + + this.setupRedisAdapter(); + this.setupMiddleware(); + this.setupEventHandlers(); + this.setupAdminUI(); + this.startPresenceTracking(); + } + + setupRedisAdapter() { + // Enable horizontal scaling with Redis + this.io.adapter(createAdapter(this.pubClient, this.subClient)); + } + + setupMiddleware() { + // Authentication middleware + this.io.use(async (socket, next) => { + try { + const token = socket.handshake.auth.token; + if (!token) { + return next(new Error('Authentication required')); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await this.validateUser(decoded.userId); + + if (!user) { + return next(new Error('Invalid user')); + } + + // Attach user data to socket + socket.userId = user.id; + socket.user = user; + socket.sessionId = crypto.randomBytes(8).toString('hex'); + + // Rate limiting per user + const rateLimitKey = `ratelimit:${user.id}`; + const requests = await this.redis.incr(rateLimitKey); + + if (requests === 1) { + await this.redis.expire(rateLimitKey, 60); // 1 minute window + } + + if (requests > 100) { // 100 messages per minute + return next(new Error('Rate limit exceeded')); + } + + next(); + } catch (error) { + next(new Error('Authentication failed: ' + error.message)); + } + }); + + // Connection logging middleware + this.io.use((socket, next) => { + console.log(`New connection from ${socket.handshake.address}`); + socket.on('error', (error) => { + console.error(`Socket error for ${socket.userId}:`, error); + }); + next(); + }); + } + + setupEventHandlers() { + this.io.on('connection', (socket) => { + this.handleConnection(socket); + + // Room management + socket.on('join-room', (roomId, callback) => + this.handleJoinRoom(socket, roomId, callback) + ); + + socket.on('leave-room', (roomId, callback) => + this.handleLeaveRoom(socket, roomId, callback) + ); + + // Messaging + socket.on('message', (data, callback) => + this.handleMessage(socket, data, callback) + ); + + socket.on('typing', (data) => + this.handleTyping(socket, data) + ); + + socket.on('stop-typing', (data) => + this.handleStopTyping(socket, data) + ); + + // Presence + socket.on('update-status', (status) => + this.handleStatusUpdate(socket, status) + ); + + // Private messaging + socket.on('private-message', (data, callback) => + this.handlePrivateMessage(socket, data, callback) + ); + + // File sharing + socket.on('upload-start', (data, callback) => + this.handleUploadStart(socket, data, callback) + ); + + socket.on('upload-chunk', (data, callback) => + this.handleUploadChunk(socket, data, callback) + ); + + // Voice/Video calls + socket.on('call-user', (data, callback) => + this.handleCallUser(socket, data, callback) + ); + + socket.on('call-answer', (data) => + this.handleCallAnswer(socket, data) + ); + + socket.on('ice-candidate', (data) => + this.handleIceCandidate(socket, data) + ); + + // Disconnection + socket.on('disconnect', (reason) => + this.handleDisconnection(socket, reason) + ); + }); + } + + async handleConnection(socket) { + console.log(`User ${socket.userId} connected (${socket.sessionId})`); + + // Track user connection + if (!this.users.has(socket.userId)) { + this.users.set(socket.userId, new Set()); + } + this.users.get(socket.userId).add(socket.id); + + // Update presence + await this.updatePresence(socket.userId, 'online'); + + // Send connection success with user data + socket.emit('connected', { + sessionId: socket.sessionId, + userId: socket.userId, + serverTime: Date.now() + }); + + // Rejoin previous rooms from session + const previousRooms = await this.getUserRooms(socket.userId); + for (const roomId of previousRooms) { + socket.join(roomId); + socket.to(roomId).emit('user-joined', { + userId: socket.userId, + user: socket.user, + roomId + }); + } + + // Send pending messages + const pendingMessages = await this.getPendingMessages(socket.userId); + if (pendingMessages.length > 0) { + socket.emit('pending-messages', pendingMessages); + } + } + + async handleJoinRoom(socket, roomId, callback) { + try { + // Validate room access + const hasAccess = await this.validateRoomAccess(socket.userId, roomId); + if (!hasAccess) { + return callback({ error: 'Access denied' }); + } + + // Join the room + socket.join(roomId); + + // Track room membership + if (!this.rooms.has(roomId)) { + this.rooms.set(roomId, new Set()); + } + this.rooms.get(roomId).add(socket.userId); + + // Get room info + const roomInfo = await this.getRoomInfo(roomId); + const members = await this.getRoomMembers(roomId); + + // Notify others in room + socket.to(roomId).emit('user-joined', { + userId: socket.userId, + user: socket.user, + roomId + }); + + // Send room state to joiner + callback({ + success: true, + room: roomInfo, + members: members, + recentMessages: await this.getRecentMessages(roomId) + }); + + // Update user's room list + await this.addUserRoom(socket.userId, roomId); + + } catch (error) { + console.error('Join room error:', error); + callback({ error: error.message }); + } + } + + async handleMessage(socket, data, callback) { + try { + // Validate message + if (!data.roomId || !data.content) { + return callback({ error: 'Invalid message format' }); + } + + // Check room membership + if (!socket.rooms.has(data.roomId)) { + return callback({ error: 'Not in room' }); + } + + // Create message object + const message = { + id: crypto.randomBytes(16).toString('hex'), + roomId: data.roomId, + userId: socket.userId, + user: socket.user, + content: data.content, + type: data.type || 'text', + timestamp: Date.now(), + edited: false, + deleted: false + }; + + // Store message + await this.storeMessage(message); + + // Broadcast to room + this.io.to(data.roomId).emit('new-message', message); + + // Send acknowledgment + callback({ + success: true, + messageId: message.id, + timestamp: message.timestamp + }); + + // Update room activity + await this.updateRoomActivity(data.roomId); + + // Send push notifications to offline users + await this.sendPushNotifications(data.roomId, message, socket.userId); + + } catch (error) { + console.error('Message error:', error); + callback({ error: error.message }); + } + } + + async handleTyping(socket, data) { + if (data.roomId && socket.rooms.has(data.roomId)) { + socket.to(data.roomId).emit('user-typing', { + userId: socket.userId, + user: socket.user, + roomId: data.roomId + }); + + // Auto-stop typing after 3 seconds + setTimeout(() => { + socket.to(data.roomId).emit('user-stopped-typing', { + userId: socket.userId, + roomId: data.roomId + }); + }, 3000); + } + } + + async handlePrivateMessage(socket, data, callback) { + try { + const targetUserId = data.targetUserId; + const targetSockets = this.users.get(targetUserId); + + const message = { + id: crypto.randomBytes(16).toString('hex'), + fromUserId: socket.userId, + fromUser: socket.user, + toUserId: targetUserId, + content: data.content, + timestamp: Date.now(), + read: false + }; + + // Store private message + await this.storePrivateMessage(message); + + if (targetSockets && targetSockets.size > 0) { + // User is online, deliver to all their connections + for (const socketId of targetSockets) { + this.io.to(socketId).emit('private-message', message); + } + message.delivered = true; + } else { + // User is offline, queue for later + await this.queueMessage(targetUserId, message); + message.delivered = false; + } + + callback({ + success: true, + messageId: message.id, + delivered: message.delivered + }); + + } catch (error) { + console.error('Private message error:', error); + callback({ error: error.message }); + } + } + + async handleDisconnection(socket, reason) { + console.log(`User ${socket.userId} disconnected: ${reason}`); + + // Remove socket from user's connections + const userSockets = this.users.get(socket.userId); + if (userSockets) { + userSockets.delete(socket.id); + + // If no more connections, mark as offline + if (userSockets.size === 0) { + this.users.delete(socket.userId); + await this.updatePresence(socket.userId, 'offline'); + + // Notify rooms user was in + for (const roomId of socket.rooms) { + if (roomId !== socket.id) { // Skip default room + socket.to(roomId).emit('user-left', { + userId: socket.userId, + roomId + }); + } + } + } + } + + // Clean up room memberships + for (const [roomId, members] of this.rooms.entries()) { + members.delete(socket.userId); + if (members.size === 0) { + this.rooms.delete(roomId); + } + } + } + + // Presence tracking + startPresenceTracking() { + setInterval(async () => { + for (const [userId, status] of this.presence.entries()) { + // Check if user has active connections + if (!this.users.has(userId)) { + this.presence.set(userId, 'offline'); + await this.broadcastPresenceUpdate(userId, 'offline'); + } + } + }, 30000); // Check every 30 seconds + } + + async updatePresence(userId, status) { + this.presence.set(userId, status); + await this.redis.setex(`presence:${userId}`, 120, status); + await this.broadcastPresenceUpdate(userId, status); + } + + async broadcastPresenceUpdate(userId, status) { + // Get all rooms user is in + const userRooms = await this.getUserRooms(userId); + + for (const roomId of userRooms) { + this.io.to(roomId).emit('presence-update', { + userId, + status, + timestamp: Date.now() + }); + } + } + + // Helper methods + async validateUser(userId) { + // Implement user validation + return { id: userId, name: `User ${userId}` }; + } + + async validateRoomAccess(userId, roomId) { + // Implement room access validation + return true; + } + + async getRoomInfo(roomId) { + // Implement room info retrieval + return { id: roomId, name: `Room ${roomId}` }; + } + + async getRoomMembers(roomId) { + // Implement member list retrieval + return Array.from(this.rooms.get(roomId) || []); + } + + async getRecentMessages(roomId, limit = 50) { + // Implement message history retrieval + return []; + } + + async storeMessage(message) { + // Implement message storage + await this.redis.lpush( + `messages:${message.roomId}`, + JSON.stringify(message) + ); + await this.redis.ltrim(`messages:${message.roomId}`, 0, 999); + } + + async getUserRooms(userId) { + // Implement user room list retrieval + return []; + } + + async getPendingMessages(userId) { + // Implement pending message retrieval + return []; + } + + setupAdminUI() { + // Enable admin UI for monitoring + instrument(this.io, { + auth: { + type: 'basic', + username: process.env.SOCKETIO_ADMIN_USER || 'admin', + password: process.env.SOCKETIO_ADMIN_PASSWORD || 'admin' + }, + readonly: false + }); + } + + // Public methods for external use + async broadcast(event, data) { + this.io.emit(event, data); + } + + async broadcastToRoom(roomId, event, data) { + this.io.to(roomId).emit(event, data); + } + + async sendToUser(userId, event, data) { + const userSockets = this.users.get(userId); + if (userSockets) { + for (const socketId of userSockets) { + this.io.to(socketId).emit(event, data); + } + } + } + + getOnlineUsers() { + return Array.from(this.users.keys()); + } + + getRoomMembers(roomId) { + return Array.from(this.rooms.get(roomId) || []); + } +} + +// Initialize server +const http = require('http'); +const express = require('express'); + +const app = express(); +const server = http.createServer(app); + +const wsServer = new WebSocketServer(server, { + corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], + redisUrl: process.env.REDIS_URL || 'redis://localhost:6379' +}); + +// REST endpoints for WebSocket management +app.get('/ws/stats', (req, res) => { + res.json({ + onlineUsers: wsServer.getOnlineUsers().length, + rooms: wsServer.rooms.size, + connections: wsServer.io.engine.clientsCount + }); +}); + +app.post('/ws/broadcast', express.json(), async (req, res) => { + const { event, data } = req.body; + await wsServer.broadcast(event, data); + res.json({ success: true }); +}); + +server.listen(3000, () => { + console.log('WebSocket server running on port 3000'); +}); + +module.exports = { WebSocketServer }; +``` + +### Example 2: High-Performance Native WebSocket Implementation + +```javascript +// websocket/native-server.js - Pure WebSocket with advanced features +const WebSocket = require('ws'); +const http = require('http'); +const crypto = require('crypto'); +const EventEmitter = require('events'); + +class NativeWebSocketServer extends EventEmitter { + constructor(server, options = {}) { + super(); + + this.wss = new WebSocket.Server({ + server, + perMessageDeflate: options.compression !== false, + maxPayload: options.maxPayload || 10 * 1024 * 1024, // 10MB + clientTracking: true, + ...options + }); + + this.clients = new Map(); + this.rooms = new Map(); + this.messageHandlers = new Map(); + this.heartbeatInterval = options.heartbeatInterval || 30000; + + this.setupEventHandlers(); + this.startHeartbeat(); + } + + setupEventHandlers() { + this.wss.on('connection', (ws, req) => { + this.handleConnection(ws, req); + }); + + this.wss.on('error', (error) => { + console.error('WebSocket server error:', error); + this.emit('error', error); + }); + } + + handleConnection(ws, req) { + // Generate client ID + const clientId = crypto.randomBytes(16).toString('hex'); + + // Parse authentication from URL or headers + const token = this.extractToken(req); + const user = this.authenticateToken(token); + + if (!user) { + ws.close(1008, 'Unauthorized'); + return; + } + + // Setup client + const client = { + id: clientId, + ws: ws, + user: user, + rooms: new Set(), + isAlive: true, + joinedAt: Date.now(), + lastActivity: Date.now(), + messageCount: 0 + }; + + this.clients.set(clientId, client); + + // Send connection acknowledgment + this.send(client, { + type: 'connected', + clientId: clientId, + timestamp: Date.now() + }); + + // Setup client event handlers + ws.on('message', (data) => this.handleMessage(client, data)); + ws.on('pong', () => this.handlePong(client)); + ws.on('close', (code, reason) => this.handleClose(client, code, reason)); + ws.on('error', (error) => this.handleError(client, error)); + + this.emit('connection', client); + } + + handleMessage(client, data) { + try { + // Update activity + client.lastActivity = Date.now(); + client.messageCount++; + + // Parse message + let message; + if (typeof data === 'string') { + message = JSON.parse(data); + } else { + // Handle binary data + message = this.parseBinaryMessage(data); + } + + // Rate limiting + if (client.messageCount > 100) { + const timeDiff = Date.now() - client.joinedAt; + if (timeDiff < 60000) { // Less than 1 minute + this.send(client, { + type: 'error', + error: 'Rate limit exceeded' + }); + client.ws.close(1008, 'Rate limit exceeded'); + return; + } + client.messageCount = 0; + client.joinedAt = Date.now(); + } + + // Route message to handler + const handler = this.messageHandlers.get(message.type); + if (handler) { + handler(client, message); + } else { + this.handleDefaultMessage(client, message); + } + + this.emit('message', client, message); + + } catch (error) { + console.error('Message handling error:', error); + this.send(client, { + type: 'error', + error: 'Invalid message format' + }); + } + } + + handleDefaultMessage(client, message) { + switch (message.type) { + case 'join': + this.joinRoom(client, message.room); + break; + case 'leave': + this.leaveRoom(client, message.room); + break; + case 'broadcast': + this.broadcastToRoom(message.room, message.data, client); + break; + case 'ping': + this.send(client, { type: 'pong', timestamp: Date.now() }); + break; + default: + this.emit('custom-message', client, message); + } + } + + joinRoom(client, roomId) { + if (!roomId) return; + + // Add client to room + if (!this.rooms.has(roomId)) { + this.rooms.set(roomId, new Set()); + } + this.rooms.get(roomId).add(client.id); + client.rooms.add(roomId); + + // Notify room members + this.broadcastToRoom(roomId, { + type: 'user-joined', + userId: client.user.id, + roomId: roomId, + timestamp: Date.now() + }, client); + + // Send confirmation + this.send(client, { + type: 'joined', + roomId: roomId, + members: this.getRoomMembers(roomId) + }); + + this.emit('room-joined', client, roomId); + } + + leaveRoom(client, roomId) { + if (!roomId || !client.rooms.has(roomId)) return; + + // Remove client from room + const room = this.rooms.get(roomId); + if (room) { + room.delete(client.id); + if (room.size === 0) { + this.rooms.delete(roomId); + } + } + client.rooms.delete(roomId); + + // Notify room members + this.broadcastToRoom(roomId, { + type: 'user-left', + userId: client.user.id, + roomId: roomId, + timestamp: Date.now() + }); + + // Send confirmation + this.send(client, { + type: 'left', + roomId: roomId + }); + + this.emit('room-left', client, roomId); + } + + broadcastToRoom(roomId, data, excludeClient = null) { + const room = this.rooms.get(roomId); + if (!room) return; + + const message = typeof data === 'object' ? JSON.stringify(data) : data; + + for (const clientId of room) { + if (excludeClient && clientId === excludeClient.id) continue; + + const client = this.clients.get(clientId); + if (client && client.ws.readyState === WebSocket.OPEN) { + client.ws.send(message); + } + } + } + + broadcast(data, excludeClient = null) { + const message = typeof data === 'object' ? JSON.stringify(data) : data; + + this.clients.forEach((client) => { + if (excludeClient && client.id === excludeClient.id) return; + + if (client.ws.readyState === WebSocket.OPEN) { + client.ws.send(message); + } + }); + } + + send(client, data) { + if (client.ws.readyState === WebSocket.OPEN) { + const message = typeof data === 'object' ? JSON.stringify(data) : data; + client.ws.send(message); + } + } + + handlePong(client) { + client.isAlive = true; + } + + handleClose(client, code, reason) { + console.log(`Client ${client.id} disconnected: ${code} - ${reason}`); + + // Leave all rooms + for (const roomId of client.rooms) { + this.leaveRoom(client, roomId); + } + + // Remove client + this.clients.delete(client.id); + + this.emit('disconnection', client, code, reason); + } + + handleError(client, error) { + console.error(`Client ${client.id} error:`, error); + this.emit('client-error', client, error); + } + + startHeartbeat() { + setInterval(() => { + this.clients.forEach((client) => { + if (!client.isAlive) { + console.log(`Terminating inactive client ${client.id}`); + client.ws.terminate(); + this.clients.delete(client.id); + return; + } + + client.isAlive = false; + client.ws.ping(); + }); + }, this.heartbeatInterval); + } + + // Utility methods + extractToken(req) { + // Extract from query string or authorization header + const url = new URL(req.url, `http://${req.headers.host}`); + return url.searchParams.get('token') || + req.headers.authorization?.replace('Bearer ', ''); + } + + authenticateToken(token) { + // Implement token validation + if (!token) return null; + return { id: 'user123', name: 'Test User' }; + } + + getRoomMembers(roomId) { + const room = this.rooms.get(roomId); + if (!room) return []; + + return Array.from(room).map(clientId => { + const client = this.clients.get(clientId); + return client ? client.user : null; + }).filter(Boolean); + } + + // Public API + registerHandler(type, handler) { + this.messageHandlers.set(type, handler); + } + + getClient(clientId) { + return this.clients.get(clientId); + } + + getClients() { + return Array.from(this.clients.values()); + } + + getRooms() { + return Array.from(this.rooms.keys()); + } + + close() { + clearInterval(this.heartbeatTimer); + this.wss.close(); + } +} + +// Usage +const server = http.createServer(); +const wsServer = new NativeWebSocketServer(server, { + compression: true, + heartbeatInterval: 30000 +}); + +// Register custom message handlers +wsServer.registerHandler('chat', (client, message) => { + wsServer.broadcastToRoom(message.room, { + type: 'chat', + from: client.user.name, + content: message.content, + timestamp: Date.now() + }); +}); + +wsServer.registerHandler('file-upload', (client, message) => { + // Handle file upload + console.log(`File upload from ${client.user.name}:`, message.filename); +}); + +// Event listeners +wsServer.on('connection', (client) => { + console.log(`New connection: ${client.user.name}`); +}); + +wsServer.on('room-joined', (client, roomId) => { + console.log(`${client.user.name} joined room ${roomId}`); +}); + +server.listen(3000, () => { + console.log('Native WebSocket server running on port 3000'); +}); +``` + +### Example 3: Client SDK and Testing + +```javascript +// websocket/client.js - Browser/Node.js client +class WebSocketClient { + constructor(url, options = {}) { + this.url = url; + this.options = options; + this.ws = null; + this.messageHandlers = new Map(); + this.messageQueue = []; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = options.maxReconnectAttempts || 5; + this.reconnectDelay = options.reconnectDelay || 1000; + this.isConnected = false; + } + + connect() { + return new Promise((resolve, reject) => { + try { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.isConnected = true; + this.reconnectAttempts = 0; + + // Send queued messages + while (this.messageQueue.length > 0) { + const message = this.messageQueue.shift(); + this.send(message); + } + + resolve(); + }; + + this.ws.onmessage = (event) => { + this.handleMessage(event.data); + }; + + this.ws.onerror = (error) => { + console.error('WebSocket error:', error); + reject(error); + }; + + this.ws.onclose = (event) => { + console.log('WebSocket closed:', event.code, event.reason); + this.isConnected = false; + this.handleReconnect(); + }; + + } catch (error) { + reject(error); + } + }); + } + + handleMessage(data) { + try { + const message = JSON.parse(data); + const handler = this.messageHandlers.get(message.type); + + if (handler) { + handler(message); + } else { + console.log('Unhandled message:', message); + } + } catch (error) { + console.error('Message parsing error:', error); + } + } + + on(type, handler) { + this.messageHandlers.set(type, handler); + } + + send(data) { + const message = typeof data === 'object' ? JSON.stringify(data) : data; + + if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(message); + } else { + // Queue message for later + this.messageQueue.push(data); + } + } + + handleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('Max reconnection attempts reached'); + return; + } + + this.reconnectAttempts++; + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); + + console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`); + + setTimeout(() => { + this.connect().catch(console.error); + }, delay); + } + + close() { + if (this.ws) { + this.ws.close(); + } + } +} + +// tests/websocket.test.js +const { WebSocketServer } = require('../websocket/server'); +const WebSocketClient = require('../websocket/client'); + +describe('WebSocket Server Tests', () => { + let server; + let client; + + beforeEach(async () => { + server = new WebSocketServer(3001); + await server.start(); + + client = new WebSocketClient('ws://localhost:3001', { + token: 'test-token' + }); + }); + + afterEach(async () => { + client.close(); + await server.close(); + }); + + test('should connect successfully', async () => { + await client.connect(); + expect(client.isConnected).toBe(true); + }); + + test('should join room', async () => { + await client.connect(); + + const joined = new Promise((resolve) => { + client.on('joined', resolve); + }); + + client.send({ type: 'join', room: 'test-room' }); + + const result = await joined; + expect(result.roomId).toBe('test-room'); + }); + + test('should broadcast messages', async () => { + const client2 = new WebSocketClient('ws://localhost:3001'); + await client.connect(); + await client2.connect(); + + // Both join same room + client.send({ type: 'join', room: 'test-room' }); + client2.send({ type: 'join', room: 'test-room' }); + + // Set up message listener + const messageReceived = new Promise((resolve) => { + client2.on('chat', resolve); + }); + + // Send message + client.send({ + type: 'chat', + room: 'test-room', + content: 'Hello World' + }); + + const message = await messageReceived; + expect(message.content).toBe('Hello World'); + }); + + test('should handle reconnection', async () => { + await client.connect(); + + // Force disconnect + server.disconnectClient(client.id); + + // Wait for reconnection + await new Promise(resolve => setTimeout(resolve, 2000)); + + expect(client.isConnected).toBe(true); + expect(client.reconnectAttempts).toBeGreaterThan(0); + }); +}); +``` + +## Error Handling + +| Error | Cause | Solution | +|-------|-------|----------| +| "WebSocket connection failed" | Network issues or wrong URL | Check network and WebSocket URL | +| "Authentication required" | Missing or invalid token | Include valid auth token | +| "Rate limit exceeded" | Too many messages | Implement client-side throttling | +| "Maximum payload size exceeded" | Message too large | Split large messages or use chunking | +| "Connection timeout" | No heartbeat response | Check network stability | + +## Configuration Options + +**Server Options** +- `perMessageDeflate`: Enable compression +- `maxPayload`: Maximum message size +- `pingInterval`: Heartbeat frequency +- `pingTimeout`: Heartbeat timeout +- `transports`: Allowed transports (Socket.IO) + +**Client Options** +- `reconnection`: Enable auto-reconnection +- `reconnectionAttempts`: Max reconnect tries +- `reconnectionDelay`: Initial reconnect delay +- `timeout`: Connection timeout +- `auth`: Authentication data + +## Best Practices + +DO: +- Implement heartbeat/ping-pong for connection health +- Use rooms/namespaces for logical grouping +- Add authentication before accepting connections +- Implement message acknowledgments for critical data +- Use compression for text data +- Monitor connection count and memory usage + +DON'T: +- Send large payloads without chunking +- Store state only in memory without persistence +- Skip authentication for production +- Ignore connection limits +- Use synchronous operations in handlers +- Broadcast sensitive data to all clients + +## Performance Considerations + +- Use binary frames for large data transfers +- Implement message batching for high-frequency updates +- Use Redis adapter for horizontal scaling +- Enable compression for text-heavy payloads +- Limit connection count per IP/user +- Implement connection pooling for database queries + +## Security Considerations + +- Always use WSS (WebSocket Secure) in production +- Validate all incoming messages +- Implement rate limiting per connection +- Use JWT or session-based authentication +- Sanitize user input before broadcasting +- Implement CORS properly +- Monitor for abnormal connection patterns + +## Related Commands + +- `/webhook-handler-creator` - Handle webhooks +- `/real-time-sync` - Implement data synchronization +- `/message-queue-setup` - Configure message queues +- `/pubsub-system` - Build pub/sub architecture +- `/notification-service` - Push notifications + +## Version History + +- v1.0.0 (2024-10): Initial implementation with Socket.IO and native WebSocket +- Planned v1.1.0: Add WebRTC signaling server and video streaming support \ No newline at end of file diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..4a21482 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,97 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jeremylongshore/claude-code-plugins-plus:plugins/api-development/websocket-server-builder", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "f1e4c551348bc9ad6656e442a3b312c518550a85", + "treeHash": "80529f219b7667550df4e566974c5ce80a5575443cba3b8c8c012241177a0f49", + "generatedAt": "2025-11-28T10:18:52.568912Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "websocket-server-builder", + "description": "Build WebSocket servers for real-time bidirectional communication", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "392ae10aeb439ac36398ede716e1d1e66f5c4529633bf17ec2ea095da47dfcde" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "1960a5f2d47aaef61534da08fe02b3adcc4fecd1217f9e8625c89ad4533785cf" + }, + { + "path": "commands/build-websocket-server.md", + "sha256": "55134ffacbd862229789ffe5ac45d876fd6a241a56cb5f81bc9501fd71fa151e" + }, + { + "path": "skills/skill-adapter/references/examples.md", + "sha256": "922bbc3c4ebf38b76f515b5c1998ebde6bf902233e00e2c5a0e9176f975a7572" + }, + { + "path": "skills/skill-adapter/references/best-practices.md", + "sha256": "c8f32b3566252f50daacd346d7045a1060c718ef5cfb07c55a0f2dec5f1fb39e" + }, + { + "path": "skills/skill-adapter/references/README.md", + "sha256": "dd0333d5049c487e017cf189a6a2fdcd52729af1c722d2e68239cfc4d1ad2b37" + }, + { + "path": "skills/skill-adapter/scripts/helper-template.sh", + "sha256": "0881d5660a8a7045550d09ae0acc15642c24b70de6f08808120f47f86ccdf077" + }, + { + "path": "skills/skill-adapter/scripts/validation.sh", + "sha256": "92551a29a7f512d2036e4f1fb46c2a3dc6bff0f7dde4a9f699533e446db48502" + }, + { + "path": "skills/skill-adapter/scripts/README.md", + "sha256": "e9e92b95cdb48fe1363512c6354191e5d48b73512a8d261f4f6181b3bb5971d5" + }, + { + "path": "skills/skill-adapter/assets/test-data.json", + "sha256": "ac17dca3d6e253a5f39f2a2f1b388e5146043756b05d9ce7ac53a0042eee139d" + }, + { + "path": "skills/skill-adapter/assets/template_socketio.py", + "sha256": "4e4813eb9eeac78fa0660891acbe8cc03372538940ec3c28dc610cb0350d25c2" + }, + { + "path": "skills/skill-adapter/assets/README.md", + "sha256": "bd460681d5a2b485922b9dabc46750c09a7c9cf0e4dac663fa34353f1d7abe23" + }, + { + "path": "skills/skill-adapter/assets/template_native_ws.py", + "sha256": "d00dc10c215c0d53125f8528c6ccdccd4b2f966dd02cef15ff3f8ed0e22c8011" + }, + { + "path": "skills/skill-adapter/assets/skill-schema.json", + "sha256": "f5639ba823a24c9ac4fb21444c0717b7aefde1a4993682897f5bf544f863c2cd" + }, + { + "path": "skills/skill-adapter/assets/example_client.html", + "sha256": "19eb22fadc64f533757fc45e3c3db67d321dea349f20e1951a7b8620883d248f" + }, + { + "path": "skills/skill-adapter/assets/config-template.json", + "sha256": "0c2ba33d2d3c5ccb266c0848fc43caa68a2aa6a80ff315d4b378352711f83e1c" + } + ], + "dirSha256": "80529f219b7667550df4e566974c5ce80a5575443cba3b8c8c012241177a0f49" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/README.md b/skills/skill-adapter/assets/README.md new file mode 100644 index 0000000..fd37386 --- /dev/null +++ b/skills/skill-adapter/assets/README.md @@ -0,0 +1,7 @@ +# Assets + +Bundled resources for websocket-server-builder skill + +- [ ] template_socketio.py: Template file for a basic WebSocket server using Socket.IO. +- [ ] template_native_ws.py: Template file for a basic WebSocket server using native WebSockets. +- [ ] example_client.html: Example HTML client for testing the WebSocket server. diff --git a/skills/skill-adapter/assets/config-template.json b/skills/skill-adapter/assets/config-template.json new file mode 100644 index 0000000..16f1712 --- /dev/null +++ b/skills/skill-adapter/assets/config-template.json @@ -0,0 +1,32 @@ +{ + "skill": { + "name": "skill-name", + "version": "1.0.0", + "enabled": true, + "settings": { + "verbose": false, + "autoActivate": true, + "toolRestrictions": true + } + }, + "triggers": { + "keywords": [ + "example-trigger-1", + "example-trigger-2" + ], + "patterns": [] + }, + "tools": { + "allowed": [ + "Read", + "Grep", + "Bash" + ], + "restricted": [] + }, + "metadata": { + "author": "Plugin Author", + "category": "general", + "tags": [] + } +} diff --git a/skills/skill-adapter/assets/example_client.html b/skills/skill-adapter/assets/example_client.html new file mode 100644 index 0000000..0954c7c --- /dev/null +++ b/skills/skill-adapter/assets/example_client.html @@ -0,0 +1,140 @@ + + + + + + WebSocket Client Example + + + +
+

WebSocket Client

+

Connect to WebSocket server at: {{websocket_url}}

+ +
+ +
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/skills/skill-adapter/assets/skill-schema.json b/skills/skill-adapter/assets/skill-schema.json new file mode 100644 index 0000000..8dc154c --- /dev/null +++ b/skills/skill-adapter/assets/skill-schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Claude Skill Configuration", + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "maxLength": 64, + "description": "Skill identifier (lowercase, hyphens only)" + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "What the skill does and when to use it" + }, + "allowed-tools": { + "type": "string", + "description": "Comma-separated list of allowed tools" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "Semantic version (x.y.z)" + } + } +} diff --git a/skills/skill-adapter/assets/template_native_ws.py b/skills/skill-adapter/assets/template_native_ws.py new file mode 100644 index 0000000..f5e5c3f --- /dev/null +++ b/skills/skill-adapter/assets/template_native_ws.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +""" +Template for a basic WebSocket server using native WebSockets. + +This script provides a foundation for building real-time bidirectional +communication applications using Python's built-in `websockets` library. +It includes basic connection handling, message reception, and sending. + +Example usage: +1. Install the `websockets` library: `pip install websockets` +2. Run the script: `python template_native_ws.py` +3. Connect to the server using a WebSocket client (e.g., a browser-based client). +""" + +import asyncio +import websockets +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) + + +async def handle_client(websocket, path): + """ + Handles a single WebSocket client connection. + + Args: + websocket: The WebSocket connection object. + path: The path requested by the client (unused in this example). + """ + try: + logging.info(f"Client connected from {websocket.remote_address}") + async for message in websocket: + logging.info(f"Received message: {message}") + try: + # Process the message (replace with your application logic) + response = f"Server received: {message}" + await websocket.send(response) + logging.info(f"Sent message: {response}") + except Exception as e: + logging.error(f"Error processing message: {e}") + await websocket.send(f"Error: {e}") + + except websockets.exceptions.ConnectionClosedError as e: + logging.info(f"Client disconnected abruptly: {e}") + except websockets.exceptions.ConnectionClosedOK as e: + logging.info(f"Client disconnected gracefully: {e}") + except Exception as e: + logging.error(f"Error handling client: {e}") + finally: + logging.info(f"Connection with {websocket.remote_address} closed.") + + +async def main(): + """ + Starts the WebSocket server. + """ + try: + server = await websockets.serve(handle_client, "localhost", 8765) + logging.info("WebSocket server started on ws://localhost:8765") + await server.wait_closed() + except OSError as e: + logging.error(f"Could not start server: {e}") + except Exception as e: + logging.error(f"Unexpected error: {e}") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Server stopped by keyboard interrupt.") + except Exception as e: + logging.error(f"Unhandled exception during server startup: {e}") \ No newline at end of file diff --git a/skills/skill-adapter/assets/template_socketio.py b/skills/skill-adapter/assets/template_socketio.py new file mode 100644 index 0000000..3abaf93 --- /dev/null +++ b/skills/skill-adapter/assets/template_socketio.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +Template for a basic WebSocket server using Socket.IO. + +This script provides a foundation for building real-time bidirectional +communication applications. It includes error handling, example usage, +and follows PEP 8 style guidelines. +""" + +import os +import logging +import socketio +from aiohttp import web + +# Configure logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + +# Initialize Socket.IO server +sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins='*') + +# Create AIOHTTP application +app = web.Application() + +# Bind Socket.IO to the application +sio.attach(app) + +# Define event handlers +@sio.event +async def connect(sid, environ): + """ + Handles a new client connection. + + Args: + sid (str): Session ID of the client. + environ (dict): Environment variables. + """ + logging.info(f"Client connected: {sid}") + try: + await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) + except Exception as e: + logging.error(f"Error emitting connect message: {e}") + + +@sio.event +async def disconnect(sid): + """ + Handles a client disconnection. + + Args: + sid (str): Session ID of the client. + """ + logging.info(f"Client disconnected: {sid}") + + +@sio.event +async def my_message(sid, message): + """ + Handles a custom message event. + + Args: + sid (str): Session ID of the client. + message (str): The received message. + """ + logging.info(f"Received message from {sid}: {message}") + try: + await sio.emit('my_response', {'data': message}, room=sid) + except Exception as e: + logging.error(f"Error emitting my_response: {e}") + + +@sio.event +async def my_broadcast_event(sid, message): + """ + Handles a broadcast event. + + Args: + sid (str): Session ID of the client. + message (str): The message to broadcast. + """ + logging.info(f"Received broadcast request from {sid}: {message}") + try: + await sio.emit('my_response', {'data': message}) # Broadcast to all clients + except Exception as e: + logging.error(f"Error emitting broadcast message: {e}") + + +@sio.event +async def join_room(sid, room): + """ + Handles a request to join a room. + + Args: + sid (str): Session ID of the client. + room (str): The room to join. + """ + logging.info(f"Client {sid} joining room {room}") + try: + sio.enter_room(sid, room) + await sio.emit('my_response', {'data': 'Entered room: ' + room}, room=sid) + except Exception as e: + logging.error(f"Error joining room: {e}") + + +@sio.event +async def leave_room(sid, room): + """ + Handles a request to leave a room. + + Args: + sid (str): Session ID of the client. + room (str): The room to leave. + """ + logging.info(f"Client {sid} leaving room {room}") + try: + sio.leave_room(sid, room) + await sio.emit('my_response', {'data': 'Left room: ' + room}, room=sid) + except Exception as e: + logging.error(f"Error leaving room: {e}") + + +async def index(request): + """ + Serves the index.html file. + + Args: + request (aiohttp.web.Request): The request object. + + Returns: + aiohttp.web.Response: The response object containing the HTML content. + """ + try: + with open('index.html') as f: + return web.Response(content_type='text/html', text=f.read()) + except FileNotFoundError: + return web.Response(status=404, text="index.html not found") + except Exception as e: + logging.error(f"Error serving index.html: {e}") + return web.Response(status=500, text=f"Internal Server Error: {e}") + +if __name__ == '__main__': + """ + Main entry point of the application. + """ + try: + # Serve static files (e.g., index.html) + app.router.add_get('/', index) + app.router.add_static('/static', './static') # Assuming a 'static' directory + + # Start the web server + port = int(os.environ.get('PORT', 5000)) # Default port 5000 or from environment + logging.info(f"Starting server on port {port}") + web.run_app(app, port=port) + + except Exception as e: + logging.error(f"Failed to start the server: {e}") \ No newline at end of file diff --git a/skills/skill-adapter/assets/test-data.json b/skills/skill-adapter/assets/test-data.json new file mode 100644 index 0000000..f0cd871 --- /dev/null +++ b/skills/skill-adapter/assets/test-data.json @@ -0,0 +1,27 @@ +{ + "testCases": [ + { + "name": "Basic activation test", + "input": "trigger phrase example", + "expected": { + "activated": true, + "toolsUsed": ["Read", "Grep"], + "success": true + } + }, + { + "name": "Complex workflow test", + "input": "multi-step trigger example", + "expected": { + "activated": true, + "steps": 3, + "toolsUsed": ["Read", "Write", "Bash"], + "success": true + } + } + ], + "fixtures": { + "sampleInput": "example data", + "expectedOutput": "processed result" + } +} diff --git a/skills/skill-adapter/references/README.md b/skills/skill-adapter/references/README.md new file mode 100644 index 0000000..fd3878b --- /dev/null +++ b/skills/skill-adapter/references/README.md @@ -0,0 +1,8 @@ +# References + +Bundled resources for websocket-server-builder skill + +- [ ] websocket_best_practices.md: Provides best practices for building secure and scalable WebSocket servers. +- [ ] socketio_api_reference.md: Detailed API reference for Socket.IO, if used. +- [ ] native_ws_api_reference.md: Detailed API reference for native WebSockets, if used. +- [ ] error_handling_guide.md: Guide to handling common WebSocket errors and exceptions. diff --git a/skills/skill-adapter/references/best-practices.md b/skills/skill-adapter/references/best-practices.md new file mode 100644 index 0000000..3505048 --- /dev/null +++ b/skills/skill-adapter/references/best-practices.md @@ -0,0 +1,69 @@ +# Skill Best Practices + +Guidelines for optimal skill usage and development. + +## For Users + +### Activation Best Practices + +1. **Use Clear Trigger Phrases** + - Match phrases from skill description + - Be specific about intent + - Provide necessary context + +2. **Provide Sufficient Context** + - Include relevant file paths + - Specify scope of analysis + - Mention any constraints + +3. **Understand Tool Permissions** + - Check allowed-tools in frontmatter + - Know what the skill can/cannot do + - Request appropriate actions + +### Workflow Optimization + +- Start with simple requests +- Build up to complex workflows +- Verify each step before proceeding +- Use skill consistently for related tasks + +## For Developers + +### Skill Development Guidelines + +1. **Clear Descriptions** + - Include explicit trigger phrases + - Document all capabilities + - Specify limitations + +2. **Proper Tool Permissions** + - Use minimal necessary tools + - Document security implications + - Test with restricted tools + +3. **Comprehensive Documentation** + - Provide usage examples + - Document common pitfalls + - Include troubleshooting guide + +### Maintenance + +- Keep version updated +- Test after tool updates +- Monitor user feedback +- Iterate on descriptions + +## Performance Tips + +- Scope skills to specific domains +- Avoid overlapping trigger phrases +- Keep descriptions under 1024 chars +- Test activation reliability + +## Security Considerations + +- Never include secrets in skill files +- Validate all inputs +- Use read-only tools when possible +- Document security requirements diff --git a/skills/skill-adapter/references/examples.md b/skills/skill-adapter/references/examples.md new file mode 100644 index 0000000..b1d8bd2 --- /dev/null +++ b/skills/skill-adapter/references/examples.md @@ -0,0 +1,70 @@ +# Skill Usage Examples + +This document provides practical examples of how to use this skill effectively. + +## Basic Usage + +### Example 1: Simple Activation + +**User Request:** +``` +[Describe trigger phrase here] +``` + +**Skill Response:** +1. Analyzes the request +2. Performs the required action +3. Returns results + +### Example 2: Complex Workflow + +**User Request:** +``` +[Describe complex scenario] +``` + +**Workflow:** +1. Step 1: Initial analysis +2. Step 2: Data processing +3. Step 3: Result generation +4. Step 4: Validation + +## Advanced Patterns + +### Pattern 1: Chaining Operations + +Combine this skill with other tools: +``` +Step 1: Use this skill for [purpose] +Step 2: Chain with [other tool] +Step 3: Finalize with [action] +``` + +### Pattern 2: Error Handling + +If issues occur: +- Check trigger phrase matches +- Verify context is available +- Review allowed-tools permissions + +## Tips & Best Practices + +- ✅ Be specific with trigger phrases +- ✅ Provide necessary context +- ✅ Check tool permissions match needs +- ❌ Avoid vague requests +- ❌ Don't mix unrelated tasks + +## Common Issues + +**Issue:** Skill doesn't activate +**Solution:** Use exact trigger phrases from description + +**Issue:** Unexpected results +**Solution:** Check input format and context + +## See Also + +- Main SKILL.md for full documentation +- scripts/ for automation helpers +- assets/ for configuration examples diff --git a/skills/skill-adapter/scripts/README.md b/skills/skill-adapter/scripts/README.md new file mode 100644 index 0000000..b537db8 --- /dev/null +++ b/skills/skill-adapter/scripts/README.md @@ -0,0 +1,7 @@ +# Scripts + +Bundled resources for websocket-server-builder skill + +- [ ] init_websocket_server.py: Automates the initial setup of a basic WebSocket server with Socket.IO or native WS. +- [ ] add_route.py: Adds a new route/endpoint to the WebSocket server, handling different message types. +- [ ] test_connection.py: Tests the WebSocket connection to ensure it's functioning correctly. diff --git a/skills/skill-adapter/scripts/helper-template.sh b/skills/skill-adapter/scripts/helper-template.sh new file mode 100755 index 0000000..c4aae90 --- /dev/null +++ b/skills/skill-adapter/scripts/helper-template.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Helper script template for skill automation +# Customize this for your skill's specific needs + +set -e + +function show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --verbose Enable verbose output" + echo "" +} + +# Parse arguments +VERBOSE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Your skill logic here +if [ "$VERBOSE" = true ]; then + echo "Running skill automation..." +fi + +echo "✅ Complete" diff --git a/skills/skill-adapter/scripts/validation.sh b/skills/skill-adapter/scripts/validation.sh new file mode 100755 index 0000000..590af58 --- /dev/null +++ b/skills/skill-adapter/scripts/validation.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Skill validation helper +# Validates skill activation and functionality + +set -e + +echo "🔍 Validating skill..." + +# Check if SKILL.md exists +if [ ! -f "../SKILL.md" ]; then + echo "❌ Error: SKILL.md not found" + exit 1 +fi + +# Validate frontmatter +if ! grep -q "^---$" "../SKILL.md"; then + echo "❌ Error: No frontmatter found" + exit 1 +fi + +# Check required fields +if ! grep -q "^name:" "../SKILL.md"; then + echo "❌ Error: Missing 'name' field" + exit 1 +fi + +if ! grep -q "^description:" "../SKILL.md"; then + echo "❌ Error: Missing 'description' field" + exit 1 +fi + +echo "✅ Skill validation passed"