UNPKG

aetherlight-sdk

Version:

ÆtherLight Application Integration SDK - Add voice control to any application with natural language function calling

233 lines (232 loc) 8.94 kB
"use strict"; /** * ÆtherLight Application Integration SDK - WebSocket Client * * DESIGN DECISION: Decorator-based function registration (TypeScript) * WHY: Clean syntax, type-safe, auto-generates function metadata * * REASONING CHAIN: * 1. Developer installs @aetherlight/sdk via npm * 2. Connects to ÆtherLight daemon via WebSocket * 3. Registers functions with decorators/annotations * 4. ÆtherLight automatically handles voice → function calls * 5. Developer receives callbacks with extracted parameters * * PATTERN: Pattern-SDK-001 (Application Integration SDK) * PERFORMANCE: <10ms function invocation latency */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AetherlightClient = void 0; const ws_1 = __importDefault(require("ws")); const events_1 = require("events"); require("reflect-metadata"); /** * ÆtherLight Client - WebSocket connection to ÆtherLight daemon * * DESIGN DECISION: EventEmitter base class for async events * WHY: Enables reactive programming patterns for function invocations */ class AetherlightClient extends events_1.EventEmitter { constructor(config = {}) { super(); this.registeredFunctions = new Map(); this.requestId = 0; this.pendingRequests = new Map(); this.config = { host: config.host ?? 'localhost', port: config.port ?? 9876, autoReconnect: config.autoReconnect ?? true, reconnectInterval: config.reconnectInterval ?? 5000, }; } /** * Connect to ÆtherLight daemon * * DESIGN DECISION: Promise-based connection with auto-reconnect * WHY: Enables await syntax + resilient to daemon restarts */ async connect() { return new Promise((resolve, reject) => { const url = `ws://${this.config.host}:${this.config.port}`; this.ws = new ws_1.default(url); const connectionTimeout = setTimeout(() => { reject(new Error(`Connection timeout after 10s connecting to ${url}`)); }, 10000); this.ws.on('open', () => { clearTimeout(connectionTimeout); this.emit('connected'); console.log(`✅ Connected to ÆtherLight daemon at ${url}`); resolve(); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(message); } catch (error) { console.error('Failed to parse message:', error); } }); this.ws.on('close', () => { this.emit('disconnected'); console.warn('⚠️ Disconnected from ÆtherLight daemon'); if (this.config.autoReconnect) { console.log(`🔄 Reconnecting in ${this.config.reconnectInterval}ms...`); setTimeout(() => { this.connect().catch(err => { console.error('❌ Reconnection failed:', err); }); }, this.config.reconnectInterval); } }); this.ws.on('error', (error) => { clearTimeout(connectionTimeout); console.error('❌ WebSocket error:', error); reject(error); }); }); } /** * Register a class instance with voice-command decorated methods * * DESIGN DECISION: Reflect metadata for automatic parameter extraction * WHY: TypeScript decorators provide compile-time type information * * REASONING CHAIN: * 1. Iterate over instance methods * 2. Check for @Lumina decorator metadata * 3. Extract parameter metadata from @param decorators * 4. Send register_function JSON-RPC request * 5. Store function reference for later invocation */ register(instance) { const prototype = Object.getPrototypeOf(instance); const methods = Object.getOwnPropertyNames(prototype) .filter(name => name !== 'constructor'); for (const methodName of methods) { const metadata = Reflect.getMetadata('voiceCommand', prototype, methodName); if (!metadata) continue; const paramMetadata = Reflect.getMetadata('voiceParams', prototype, methodName) || []; const functionId = `${instance.constructor.name}.${methodName}`; // Register function in ÆtherLight this.sendRequest('register_function', { function_id: functionId, name: methodName, description: metadata.description, parameters: paramMetadata, examples: metadata.examples, tags: metadata.tags || [], }).then(() => { console.log(`✅ Registered function: ${functionId}`); }).catch(err => { console.error(`❌ Failed to register ${functionId}:`, err); }); // Store function reference for invocation this.registeredFunctions.set(functionId, instance[methodName].bind(instance)); } console.log(`📝 Registered ${this.registeredFunctions.size} voice command(s)`); } /** * Disconnect from ÆtherLight daemon */ disconnect() { if (this.ws) { this.ws.close(); this.ws = undefined; } } /** * Handle incoming JSON-RPC message * * DESIGN DECISION: Support both requests and responses * WHY: Bidirectional communication (ÆtherLight can invoke our functions) */ async handleMessage(message) { // Handle response to our request if (message.id && message.result !== undefined) { const pending = this.pendingRequests.get(message.id); if (pending) { this.pendingRequests.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message || 'Unknown error')); } else { pending.resolve(message.result); } } return; } // Handle function invocation from ÆtherLight if (message.method === 'invoke_function') { const { function_id, parameters } = message.params; const func = this.registeredFunctions.get(function_id); if (!func) { this.sendResponse(message.id, null, { code: -32601, message: `Function ${function_id} not found`, }); return; } try { // Invoke function with extracted parameters const result = await func(...Object.values(parameters)); this.sendResponse(message.id, result); this.emit('function_invoked', { function_id, parameters, result }); console.log(`✅ Executed ${function_id}`); } catch (error) { this.sendResponse(message.id, null, { code: -32603, message: error.message || 'Internal error', }); console.error(`❌ Function ${function_id} failed:`, error); } } } /** * Send JSON-RPC 2.0 request */ sendRequest(method, params) { return new Promise((resolve, reject) => { const id = ++this.requestId; const request = { jsonrpc: '2.0', id, method, params, }; this.pendingRequests.set(id, { resolve, reject }); if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) { reject(new Error('WebSocket not connected')); return; } this.ws.send(JSON.stringify(request)); // Timeout after 30 seconds setTimeout(() => { if (this.pendingRequests.has(id)) { this.pendingRequests.delete(id); reject(new Error(`Request timeout for method: ${method}`)); } }, 30000); }); } /** * Send JSON-RPC 2.0 response */ sendResponse(id, result, error) { const response = { jsonrpc: '2.0', id, result, error, }; if (this.ws && this.ws.readyState === ws_1.default.OPEN) { this.ws.send(JSON.stringify(response)); } } } exports.AetherlightClient = AetherlightClient;