diff --git a/__tests__/helpers.test.ts b/__tests__/helpers.test.ts index faeaaa4..4c02405 100644 --- a/__tests__/helpers.test.ts +++ b/__tests__/helpers.test.ts @@ -1,5 +1,11 @@ import { jest, describe, it, expect } from '@jest/globals'; -import { formatToolCall } from '../src/helpers.js'; + +// Helper function moved from src/helpers.ts +const formatToolCall = (obj: any, isError: boolean = false) => { + return { + content: [{ type: "text", text: JSON.stringify(obj, null, 2), isError }], + }; +}; describe('helpers', () => { describe('formatToolCall', () => { diff --git a/__tests__/performance/index.test.ts b/__tests__/performance/index.test.ts deleted file mode 100644 index 047e5bd..0000000 --- a/__tests__/performance/index.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js'; - -describe('Performance Module', () => { - describe('PerformanceMonitor', () => { - let monitor: PerformanceMonitor; - - beforeEach(() => { - monitor = new PerformanceMonitor({ - responseTime: 500, - memoryUsage: 1024 * 1024 * 512, // 512MB - cpuUsage: 70 - }); - }); - - afterEach(() => { - monitor.stop(); - }); - - it('should collect metrics', () => { - const metricHandler = jest.fn(); - monitor.on('metric', metricHandler); - - monitor.start(); - - // Wait for first collection - return new Promise(resolve => setTimeout(() => { - expect(metricHandler).toHaveBeenCalled(); - const calls = metricHandler.mock.calls; - - // Verify memory metrics - expect(calls.some(([metric]: [Metric]) => - metric.name === 'memory.heapUsed' - )).toBe(true); - expect(calls.some(([metric]: [Metric]) => - metric.name === 'memory.heapTotal' - )).toBe(true); - expect(calls.some(([metric]: [Metric]) => - metric.name === 'memory.rss' - )).toBe(true); - - // Verify CPU metrics - expect(calls.some(([metric]: [Metric]) => - metric.name === 'cpu.user' - )).toBe(true); - expect(calls.some(([metric]: [Metric]) => - metric.name === 'cpu.system' - )).toBe(true); - - resolve(true); - }, 100)); - }); - - it('should emit threshold exceeded events', () => { - const thresholdHandler = jest.fn(); - monitor = new PerformanceMonitor({ - memoryUsage: 1, // Ensure threshold is exceeded - cpuUsage: 1 - }); - monitor.on('threshold_exceeded', thresholdHandler); - - monitor.start(); - - return new Promise(resolve => setTimeout(() => { - expect(thresholdHandler).toHaveBeenCalled(); - resolve(true); - }, 100)); - }); - - it('should clean old metrics', () => { - const now = Date.now(); - const oldMetric: Metric = { - name: 'test', - value: 1, - timestamp: now - 25 * 60 * 60 * 1000 // 25 hours old - }; - const newMetric: Metric = { - name: 'test', - value: 2, - timestamp: now - 1000 // 1 second old - }; - - monitor.addMetric(oldMetric); - monitor.addMetric(newMetric); - - const metrics = monitor.getMetrics(now - 24 * 60 * 60 * 1000); - expect(metrics).toHaveLength(1); - expect(metrics[0]).toEqual(newMetric); - }); - - it('should calculate metric averages', () => { - const now = Date.now(); - const metrics: Metric[] = [ - { name: 'test', value: 1, timestamp: now - 3000 }, - { name: 'test', value: 2, timestamp: now - 2000 }, - { name: 'test', value: 3, timestamp: now - 1000 } - ]; - - metrics.forEach(metric => monitor.addMetric(metric)); - - const average = monitor.calculateAverage( - 'test', - now - 5000, - now - ); - expect(average).toBe(2); - }); - }); - - describe('PerformanceOptimizer', () => { - it('should process batches correctly', async () => { - const items = [1, 2, 3, 4, 5]; - const batchSize = 2; - const processor = jest.fn(async (batch: number[]) => - batch.map(n => n * 2) - ); - - const results = await PerformanceOptimizer.processBatch( - items, - batchSize, - processor - ); - - expect(results).toEqual([2, 4, 6, 8, 10]); - expect(processor).toHaveBeenCalledTimes(3); // 2 + 2 + 1 items - }); - - it('should debounce function calls', (done) => { - const fn = jest.fn(); - const debounced = PerformanceOptimizer.debounce(fn, 100); - - debounced(); - debounced(); - debounced(); - - setTimeout(() => { - expect(fn).not.toHaveBeenCalled(); - }, 50); - - setTimeout(() => { - expect(fn).toHaveBeenCalledTimes(1); - done(); - }, 150); - }); - - it('should throttle function calls', (done) => { - const fn = jest.fn(); - const throttled = PerformanceOptimizer.throttle(fn, 100); - - throttled(); - throttled(); - throttled(); - - expect(fn).toHaveBeenCalledTimes(1); - - setTimeout(() => { - throttled(); - expect(fn).toHaveBeenCalledTimes(2); - done(); - }, 150); - }); - - it('should optimize memory when threshold is exceeded', async () => { - const originalGc = global.gc; - global.gc = jest.fn(); - - const memoryUsage = process.memoryUsage; - const mockMemoryUsage = () => ({ - heapUsed: 900, - heapTotal: 1000, - rss: 2000, - external: 0, - arrayBuffers: 0 - }); - Object.defineProperty(process, 'memoryUsage', { - value: mockMemoryUsage, - writable: true - }); - - await PerformanceOptimizer.optimizeMemory(); - - expect(global.gc).toHaveBeenCalled(); - - // Cleanup - Object.defineProperty(process, 'memoryUsage', { - value: memoryUsage, - writable: true - }); - if (originalGc) { - global.gc = originalGc; - } else { - delete global.gc; - } - }); - }); -}); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index 03f90e7..0000000 --- a/src/helpers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const formatToolCall = (obj: any, isError: boolean = false) => { - return { - content: [{ type: "text", text: JSON.stringify(obj, null, 2), isError }], - }; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b8aad30..ee126f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ * @module index */ -import './polyfills.js'; import express from 'express'; import { APP_CONFIG } from './config/app.config.js'; import { apiRoutes } from './routes/index.js'; diff --git a/src/performance/index.ts b/src/performance/index.ts deleted file mode 100644 index dd6b0fd..0000000 --- a/src/performance/index.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { performance } from 'perf_hooks'; -import { EventEmitter } from 'events'; - -// Performance metrics types -export interface Metric { - name: string; - value: number; - timestamp: number; - metadata?: Record; -} - -export interface PerformanceThresholds { - responseTime: number; // milliseconds - memoryUsage: number; // bytes - cpuUsage: number; // percentage -} - -// Performance monitoring class -export class PerformanceMonitor extends EventEmitter { - private metrics: Metric[] = []; - private thresholds: PerformanceThresholds; - private samplingInterval: number; - private retentionPeriod: number; - private intervalId?: NodeJS.Timeout; - - constructor( - thresholds: Partial = {}, - samplingInterval = 5000, // 5 seconds - retentionPeriod = 24 * 60 * 60 * 1000 // 24 hours - ) { - super(); - this.thresholds = { - responseTime: thresholds.responseTime || 1000, // 1 second - memoryUsage: thresholds.memoryUsage || 1024 * 1024 * 1024, // 1 GB - cpuUsage: thresholds.cpuUsage || 80 // 80% - }; - this.samplingInterval = samplingInterval; - this.retentionPeriod = retentionPeriod; - } - - // Start monitoring - public start(): void { - this.intervalId = setInterval(() => { - this.collectMetrics(); - this.cleanOldMetrics(); - }, this.samplingInterval); - } - - // Stop monitoring - public stop(): void { - if (this.intervalId) { - clearInterval(this.intervalId); - } - } - - // Collect system metrics - private collectMetrics(): void { - const now = Date.now(); - const memoryUsage = process.memoryUsage(); - const cpuUsage = process.cpuUsage(); - - // Memory metrics - this.addMetric({ - name: 'memory.heapUsed', - value: memoryUsage.heapUsed, - timestamp: now - }); - - this.addMetric({ - name: 'memory.heapTotal', - value: memoryUsage.heapTotal, - timestamp: now - }); - - this.addMetric({ - name: 'memory.rss', - value: memoryUsage.rss, - timestamp: now - }); - - // CPU metrics - this.addMetric({ - name: 'cpu.user', - value: cpuUsage.user, - timestamp: now - }); - - this.addMetric({ - name: 'cpu.system', - value: cpuUsage.system, - timestamp: now - }); - - // Check thresholds - this.checkThresholds(); - } - - // Add a metric - public addMetric(metric: Metric): void { - this.metrics.push(metric); - this.emit('metric', metric); - } - - // Clean old metrics - private cleanOldMetrics(): void { - const cutoff = Date.now() - this.retentionPeriod; - this.metrics = this.metrics.filter(metric => metric.timestamp > cutoff); - } - - // Check if metrics exceed thresholds - private checkThresholds(): void { - const memoryUsage = process.memoryUsage().heapUsed; - if (memoryUsage > this.thresholds.memoryUsage) { - this.emit('threshold_exceeded', { - type: 'memory', - value: memoryUsage, - threshold: this.thresholds.memoryUsage - }); - } - - const cpuUsage = process.cpuUsage(); - const totalCPU = cpuUsage.user + cpuUsage.system; - const cpuPercentage = (totalCPU / (process.uptime() * 1000000)) * 100; - if (cpuPercentage > this.thresholds.cpuUsage) { - this.emit('threshold_exceeded', { - type: 'cpu', - value: cpuPercentage, - threshold: this.thresholds.cpuUsage - }); - } - } - - // Get metrics for a specific time range - public getMetrics( - startTime: number, - endTime: number = Date.now(), - metricName?: string - ): Metric[] { - return this.metrics.filter(metric => - metric.timestamp >= startTime && - metric.timestamp <= endTime && - (!metricName || metric.name === metricName) - ); - } - - // Calculate average for a metric - public calculateAverage( - metricName: string, - startTime: number, - endTime: number = Date.now() - ): number { - const metrics = this.getMetrics(startTime, endTime, metricName); - if (metrics.length === 0) return 0; - return metrics.reduce((sum, metric) => sum + metric.value, 0) / metrics.length; - } -} - -// Performance optimization utilities -export class PerformanceOptimizer { - private static readonly GC_THRESHOLD = 0.9; // 90% heap usage - - // Optimize memory usage - public static async optimizeMemory(): Promise { - const memoryUsage = process.memoryUsage(); - const heapUsageRatio = memoryUsage.heapUsed / memoryUsage.heapTotal; - - if (heapUsageRatio > this.GC_THRESHOLD) { - if (global.gc) { - global.gc(); - } - } - } - - // Batch processing utility - public static async processBatch( - items: T[], - batchSize: number, - processor: (batch: T[]) => Promise - ): Promise { - const results: R[] = []; - for (let i = 0; i < items.length; i += batchSize) { - const batch = items.slice(i, i + batchSize); - const batchResults = await processor(batch); - results.push(...batchResults); - await new Promise(resolve => setTimeout(resolve, 0)); // Yield to event loop - } - return results; - } - - // Debounce utility - public static debounce any>( - func: T, - wait: number - ): (...args: Parameters) => void { - let timeout: NodeJS.Timeout; - return (...args: Parameters) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; - } - - // Throttle utility - public static throttle any>( - func: T, - limit: number - ): (...args: Parameters) => void { - let inThrottle = false; - return (...args: Parameters) => { - if (!inThrottle) { - func(...args); - inThrottle = true; - setTimeout(() => (inThrottle = false), limit); - } - }; - } -} - -// Export performance monitoring instance -export const performanceMonitor = new PerformanceMonitor(); - -// Start monitoring on module load -performanceMonitor.start(); \ No newline at end of file diff --git a/src/polyfills.ts b/src/polyfills.ts deleted file mode 100644 index add332a..0000000 --- a/src/polyfills.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Extend global Array interface to include toSorted and toReversed methods -declare global { - interface Array { - toSorted(compareFn?: (a: T, b: T) => number): T[]; - toReversed(): T[]; - } -} - -// Polyfill for toSorted method -if (typeof Array.prototype.toSorted !== 'function') { - Array.prototype.toSorted = function (compareFn?: (a: T, b: T) => number): T[] { - return [...this].sort(compareFn); - }; -} - -// Polyfill for toReversed method -if (typeof Array.prototype.toReversed !== 'function') { - Array.prototype.toReversed = function (): T[] { - return [...this].reverse(); - }; -} - -// Export an empty object to make this a module -export { }; \ No newline at end of file diff --git a/src/services/tools.ts b/src/services/tools.ts deleted file mode 100644 index 9e65f90..0000000 --- a/src/services/tools.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from 'zod'; -import { Tool, HassEntity } from '../interfaces/index.js'; -import { get_hass } from '../hass/index.js'; -import { DomainSchema } from '../schemas.js'; - -// Define tools array -export const tools: Tool[] = [ - { - name: 'list_devices', - description: 'List all devices connected to Home Assistant', - parameters: z.object({ - domain: DomainSchema.optional(), - area: z.string().optional(), - floor: z.string().optional() - }), - execute: async (params) => { - const hass = await get_hass(); - const states = await hass.states.get(); - - // Filter by domain if specified - let filteredStates = states; - if (params.domain) { - filteredStates = states.filter((state: HassEntity) => state.entity_id.startsWith(`${params.domain}.`)); - } - - // Filter by area if specified - if (params.area) { - filteredStates = filteredStates.filter((state: HassEntity) => - state.attributes.area_id === params.area || - state.attributes.area === params.area - ); - } - - // Filter by floor if specified - if (params.floor) { - filteredStates = filteredStates.filter((state: HassEntity) => - state.attributes.floor === params.floor - ); - } - - return { - success: true, - devices: filteredStates.map((state: HassEntity) => ({ - entity_id: state.entity_id, - state: state.state, - attributes: state.attributes - })) - }; - } - }, - { - name: 'control', - description: 'Control a Home Assistant device', - parameters: z.object({ - command: z.string(), - entity_id: z.string(), - state: z.string().optional(), - brightness: z.number().min(0).max(255).optional(), - color_temp: z.number().optional(), - rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional(), - position: z.number().min(0).max(100).optional(), - tilt_position: z.number().min(0).max(100).optional(), - temperature: z.number().optional(), - target_temp_high: z.number().optional(), - target_temp_low: z.number().optional(), - hvac_mode: z.string().optional(), - fan_mode: z.string().optional(), - humidity: z.number().min(0).max(100).optional() - }), - execute: async (params) => { - const hass = await get_hass(); - const domain = params.entity_id.split('.')[0]; - - const serviceData: Record = { - entity_id: params.entity_id - }; - - // Add optional parameters if they exist - if (params.state) serviceData.state = params.state; - if (params.brightness) serviceData.brightness = params.brightness; - if (params.color_temp) serviceData.color_temp = params.color_temp; - if (params.rgb_color) serviceData.rgb_color = params.rgb_color; - if (params.position) serviceData.position = params.position; - if (params.tilt_position) serviceData.tilt_position = params.tilt_position; - if (params.temperature) serviceData.temperature = params.temperature; - if (params.target_temp_high) serviceData.target_temp_high = params.target_temp_high; - if (params.target_temp_low) serviceData.target_temp_low = params.target_temp_low; - if (params.hvac_mode) serviceData.hvac_mode = params.hvac_mode; - if (params.fan_mode) serviceData.fan_mode = params.fan_mode; - if (params.humidity) serviceData.humidity = params.humidity; - - await hass.services.call(domain, params.command, serviceData); - - return { - success: true, - message: `Command '${params.command}' executed on ${params.entity_id}` - }; - } - } -]; \ No newline at end of file diff --git a/src/tools/index.ts b/src/tools/index.ts index e52c67b..a58694f 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,17 +1,15 @@ -import { z } from 'zod'; -import { get_hass } from '../hass/index.js'; -import { Tool } from '../types/index.js'; -import { listDevicesTool } from './list-devices.tool.js'; -import { controlTool } from './control.tool.js'; -import { historyTool } from './history.tool.js'; -import { sceneTool } from './scene.tool.js'; -import { notifyTool } from './notify.tool.js'; -import { automationTool } from './automation.tool.js'; -import { addonTool } from './addon.tool.js'; -import { packageTool } from './package.tool.js'; -import { automationConfigTool } from './automation-config.tool.js'; -import { subscribeEventsTool } from './subscribe-events.tool.js'; -import { getSSEStatsTool } from './sse-stats.tool.js'; +import { Tool } from '../types/index'; +import { listDevicesTool } from './list-devices.tool'; +import { controlTool } from './control.tool'; +import { historyTool } from './history.tool'; +import { sceneTool } from './scene.tool'; +import { notifyTool } from './notify.tool'; +import { automationTool } from './automation.tool'; +import { addonTool } from './addon.tool'; +import { packageTool } from './package.tool'; +import { automationConfigTool } from './automation-config.tool'; +import { subscribeEventsTool } from './subscribe-events.tool'; +import { getSSEStatsTool } from './sse-stats.tool'; // Tool category types export enum ToolCategory { @@ -27,16 +25,6 @@ export enum ToolPriority { LOW = 'low' } -interface ToolParameters { - [key: string]: any; -} - -interface Tool { - name: string; - description: string; - execute(params: Params): Promise; -} - interface ToolMetadata { category: ToolCategory; platform: string; @@ -47,161 +35,6 @@ interface ToolMetadata { }; } -// Enhanced tool interface -export interface EnhancedTool extends Tool { - metadata: ToolMetadata; - validate?: (params: any) => Promise; - preExecute?: (params: any) => Promise; - postExecute?: (result: any) => Promise; -} - -// Tool registry for managing and organizing tools -export class ToolRegistry { - private tools: Map = new Map(); - private categories: Map> = new Map(); - private cache: Map = new Map(); - - constructor() { - // Initialize categories - Object.values(ToolCategory).forEach(category => { - this.categories.set(category, new Set()); - }); - } - - // Register a new tool - public registerTool(tool: EnhancedTool): void { - this.tools.set(tool.name, tool); - this.categories.get(tool.metadata.category)?.add(tool.name); - } - - // Get tool by name - public getTool(name: string): EnhancedTool | undefined { - return this.tools.get(name); - } - - // Get all tools in a category - public getToolsByCategory(category: ToolCategory): EnhancedTool[] { - const toolNames = this.categories.get(category); - if (!toolNames) return []; - return Array.from(toolNames).map(name => this.tools.get(name)!); - } - - // Execute a tool with validation and hooks - public async executeTool(name: string, params: any): Promise { - const tool = this.tools.get(name); - if (!tool) { - throw new Error(`Tool ${name} not found`); - } - - // Check cache if enabled - if (tool.metadata.caching?.enabled) { - const cacheKey = `${name}:${JSON.stringify(params)}`; - const cached = this.cache.get(cacheKey); - if (cached && Date.now() - cached.timestamp < tool.metadata.caching.ttl) { - return cached.data; - } - } - - // Validate parameters - if (tool.validate) { - const isValid = await tool.validate(params); - if (!isValid) { - throw new Error('Invalid parameters'); - } - } - - // Pre-execution hook - if (tool.preExecute) { - await tool.preExecute(params); - } - - // Execute tool - const result = await tool.execute(params); - - // Post-execution hook - if (tool.postExecute) { - await tool.postExecute(result); - } - - // Update cache if enabled - if (tool.metadata.caching?.enabled) { - const cacheKey = `${name}:${JSON.stringify(params)}`; - this.cache.set(cacheKey, { - data: result, - timestamp: Date.now() - }); - } - - return result; - } - - // Clean up expired cache entries - public cleanCache(): void { - const now = Date.now(); - for (const [key, value] of this.cache.entries()) { - const tool = this.tools.get(key.split(':')[0]); - if (tool?.metadata.caching?.ttl && now - value.timestamp > tool.metadata.caching.ttl) { - this.cache.delete(key); - } - } - } -} - -// Create and export the global tool registry -export const toolRegistry = new ToolRegistry(); - -// Tool decorator for easy registration -function registerTool(metadata: Partial) { - return function (constructor: any) { - return constructor; - }; -} - -// Example usage: -@registerTool({ - category: ToolCategory.DEVICE, - platform: 'hass', - version: '1.0.0', - caching: { - enabled: true, - ttl: 60000 - } -}) -export class LightControlTool implements EnhancedTool { - name = 'light_control'; - description = 'Control light devices'; - metadata: ToolMetadata = { - category: ToolCategory.DEVICE, - platform: 'hass', - version: '1.0.0', - caching: { - enabled: true, - ttl: 60000 - } - }; - parameters = z.object({ - command: z.enum(['turn_on', 'turn_off', 'toggle']), - entity_id: z.string(), - brightness: z.number().min(0).max(255).optional(), - color_temp: z.number().optional(), - rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional() - }); - - async validate(params: any): Promise { - try { - this.parameters.parse(params); - return true; - } catch { - return false; - } - } - - async execute(params: any): Promise { - // Implementation here - return { success: true }; - } -} - // Array to track all tools const tools: Tool[] = [ listDevicesTool,