diff --git a/__tests__/security/index.test.ts b/__tests__/security/index.test.ts index 52a21e4..4a00a53 100644 --- a/__tests__/security/index.test.ts +++ b/__tests__/security/index.test.ts @@ -1,4 +1,4 @@ -import { TokenManager, validateRequest, sanitizeInput, errorHandler } from '../../src/security'; +import { TokenManager, validateRequest, sanitizeInput, errorHandler } from '../../src/security/index.js'; import { Request, Response } from 'express'; describe('Security Module', () => { diff --git a/src/ai/endpoints/ai-router.ts b/src/ai/endpoints/ai-router.ts new file mode 100644 index 0000000..8b40a87 --- /dev/null +++ b/src/ai/endpoints/ai-router.ts @@ -0,0 +1,207 @@ +import express from 'express'; +import { z } from 'zod'; +import { NLPProcessor } from '../nlp/processor.js'; +import { AIRateLimit, AIContext, AIResponse, AIError, AIModel } from '../types/index.js'; +import rateLimit from 'express-rate-limit'; + +const router = express.Router(); +const nlpProcessor = new NLPProcessor(); + +// Rate limiting configuration +const rateLimitConfig: AIRateLimit = { + requests_per_minute: 100, + requests_per_hour: 1000, + concurrent_requests: 10, + model_specific_limits: { + claude: { + requests_per_minute: 100, + requests_per_hour: 1000 + }, + gpt4: { + requests_per_minute: 50, + requests_per_hour: 500 + }, + custom: { + requests_per_minute: 200, + requests_per_hour: 2000 + } + } +}; + +// Request validation schemas +const interpretRequestSchema = z.object({ + input: z.string(), + context: z.object({ + user_id: z.string(), + session_id: z.string(), + timestamp: z.string(), + location: z.string(), + previous_actions: z.array(z.any()), + environment_state: z.record(z.any()) + }), + model: z.enum(['claude', 'gpt4', 'custom']).optional() +}); + +// Rate limiters +const globalLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: rateLimitConfig.requests_per_minute +}); + +const modelSpecificLimiter = (model: string) => rateLimit({ + windowMs: 60 * 1000, + max: rateLimitConfig.model_specific_limits[model as AIModel]?.requests_per_minute || + rateLimitConfig.requests_per_minute +}); + +// Error handler middleware +const errorHandler = ( + error: Error, + req: express.Request, + res: express.Response, + next: express.NextFunction +) => { + const aiError: AIError = { + code: 'PROCESSING_ERROR', + message: error.message, + suggestion: 'Please try again with a different command format', + recovery_options: [ + 'Simplify your command', + 'Use standard command patterns', + 'Check device names and parameters' + ], + context: req.body.context + }; + + res.status(500).json({ error: aiError }); +}; + +// Endpoints +router.post( + '/interpret', + globalLimiter, + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + const { input, context, model = 'claude' } = interpretRequestSchema.parse(req.body); + + // Apply model-specific rate limiting + modelSpecificLimiter(model)(req, res, async () => { + const { intent, confidence, error } = await nlpProcessor.processCommand(input, context); + + if (error) { + return res.status(400).json({ error }); + } + + const isValid = await nlpProcessor.validateIntent(intent, confidence); + + if (!isValid) { + const suggestions = await nlpProcessor.suggestCorrections(input, { + code: 'INVALID_INTENT', + message: 'Could not understand the command with high confidence', + suggestion: 'Please try rephrasing your command', + recovery_options: [], + context + }); + + return res.status(400).json({ + error: { + code: 'INVALID_INTENT', + message: 'Could not understand the command with high confidence', + suggestion: 'Please try rephrasing your command', + recovery_options: suggestions, + context + } + }); + } + + const response: AIResponse = { + natural_language: `I'll ${intent.action} the ${intent.target.split('.').pop()}`, + structured_data: { + success: true, + action_taken: intent.action, + entities_affected: [intent.target], + state_changes: intent.parameters + }, + next_suggestions: [ + 'Would you like to adjust any settings?', + 'Should I perform this action in other rooms?', + 'Would you like to schedule this action?' + ], + confidence, + context + }; + + res.json(response); + }); + } catch (error) { + next(error); + } + } +); + +router.post( + '/execute', + globalLimiter, + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + const { intent, context, model = 'claude' } = req.body; + + // Apply model-specific rate limiting + modelSpecificLimiter(model)(req, res, async () => { + // Execute the intent through Home Assistant + // This would integrate with your existing Home Assistant service + + const response: AIResponse = { + natural_language: `Successfully executed ${intent.action} on ${intent.target}`, + structured_data: { + success: true, + action_taken: intent.action, + entities_affected: [intent.target], + state_changes: intent.parameters + }, + next_suggestions: [ + 'Would you like to verify the state?', + 'Should I perform any related actions?', + 'Would you like to undo this action?' + ], + confidence: { overall: 1, intent: 1, entities: 1, context: 1 }, + context + }; + + res.json(response); + }); + } catch (error) { + next(error); + } + } +); + +router.get( + '/suggestions', + globalLimiter, + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + const { context, model = 'claude' } = req.body; + + // Apply model-specific rate limiting + modelSpecificLimiter(model)(req, res, async () => { + // Generate context-aware suggestions + const suggestions = [ + 'Turn on the lights in the living room', + 'Set the temperature to 72 degrees', + 'Show me the current state of all devices', + 'Start the evening routine' + ]; + + res.json({ suggestions }); + }); + } catch (error) { + next(error); + } + } +); + +// Apply error handler +router.use(errorHandler); + +export default router; \ No newline at end of file diff --git a/src/ai/nlp/context-analyzer.ts b/src/ai/nlp/context-analyzer.ts new file mode 100644 index 0000000..3e15175 --- /dev/null +++ b/src/ai/nlp/context-analyzer.ts @@ -0,0 +1,135 @@ +import { AIContext, AIIntent } from '../types/index.js'; + +interface ContextAnalysis { + confidence: number; + relevant_params: Record; +} + +interface ContextRule { + condition: (context: AIContext, intent: AIIntent) => boolean; + relevance: number; + params?: (context: AIContext) => Record; +} + +export class ContextAnalyzer { + private contextRules: ContextRule[]; + + constructor() { + this.contextRules = [ + // Location-based context + { + condition: (context, intent) => + Boolean(context.location && intent.target.includes(context.location.toLowerCase())), + relevance: 0.8, + params: (context) => ({ location: context.location }) + }, + + // Time-based context + { + condition: (context) => { + const hour = new Date(context.timestamp).getHours(); + return hour >= 0 && hour <= 23; + }, + relevance: 0.6, + params: (context) => ({ + time_of_day: this.getTimeOfDay(new Date(context.timestamp)) + }) + }, + + // Previous action context + { + condition: (context, intent) => { + const recentActions = context.previous_actions.slice(-3); + return recentActions.some(action => + action.target === intent.target || + action.action === intent.action + ); + }, + relevance: 0.7, + params: (context) => ({ + recent_action: context.previous_actions[context.previous_actions.length - 1] + }) + }, + + // Environment state context + { + condition: (context, intent) => { + return Object.keys(context.environment_state).some(key => + intent.target.includes(key) || + intent.parameters[key] !== undefined + ); + }, + relevance: 0.9, + params: (context) => ({ environment: context.environment_state }) + } + ]; + } + + async analyze(intent: AIIntent, context: AIContext): Promise { + let totalConfidence = 0; + let relevantParams: Record = {}; + let applicableRules = 0; + + for (const rule of this.contextRules) { + if (rule.condition(context, intent)) { + totalConfidence += rule.relevance; + applicableRules++; + + if (rule.params) { + relevantParams = { + ...relevantParams, + ...rule.params(context) + }; + } + } + } + + // Calculate normalized confidence + const confidence = applicableRules > 0 + ? totalConfidence / applicableRules + : 0.5; // Default confidence if no rules apply + + return { + confidence, + relevant_params: relevantParams + }; + } + + private getTimeOfDay(date: Date): string { + const hour = date.getHours(); + + if (hour >= 5 && hour < 12) return 'morning'; + if (hour >= 12 && hour < 17) return 'afternoon'; + if (hour >= 17 && hour < 22) return 'evening'; + return 'night'; + } + + async updateContextRules(newRules: ContextRule[]): Promise { + this.contextRules = [...this.contextRules, ...newRules]; + } + + async validateContext(context: AIContext): Promise { + // Validate required context fields + if (!context.timestamp || !context.user_id || !context.session_id) { + return false; + } + + // Validate timestamp format + const timestamp = new Date(context.timestamp); + if (isNaN(timestamp.getTime())) { + return false; + } + + // Validate previous actions array + if (!Array.isArray(context.previous_actions)) { + return false; + } + + // Validate environment state + if (typeof context.environment_state !== 'object' || context.environment_state === null) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/ai/nlp/entity-extractor.ts b/src/ai/nlp/entity-extractor.ts new file mode 100644 index 0000000..9853c48 --- /dev/null +++ b/src/ai/nlp/entity-extractor.ts @@ -0,0 +1,103 @@ +import { AIContext } from '../types/index.js'; + +interface ExtractedEntities { + primary_target: string; + parameters: Record; + confidence: number; +} + +export class EntityExtractor { + private deviceNameMap: Map; + private parameterPatterns: Map; + + constructor() { + this.deviceNameMap = new Map(); + this.parameterPatterns = new Map(); + this.initializePatterns(); + } + + private initializePatterns(): void { + // Device name variations + this.deviceNameMap.set('living room light', 'light.living_room'); + this.deviceNameMap.set('kitchen light', 'light.kitchen'); + this.deviceNameMap.set('bedroom light', 'light.bedroom'); + + // Parameter patterns + this.parameterPatterns.set('brightness', /(\d+)\s*(%|percent)|bright(ness)?\s+(\d+)/i); + this.parameterPatterns.set('temperature', /(\d+)\s*(degrees?|°)[CF]?/i); + this.parameterPatterns.set('color', /(red|green|blue|white|warm|cool)/i); + } + + async extract(input: string): Promise { + const entities: ExtractedEntities = { + primary_target: '', + parameters: {}, + confidence: 0 + }; + + try { + // Find device name + for (const [key, value] of this.deviceNameMap) { + if (input.toLowerCase().includes(key)) { + entities.primary_target = value; + break; + } + } + + // Extract parameters + for (const [param, pattern] of this.parameterPatterns) { + const match = input.match(pattern); + if (match) { + entities.parameters[param] = this.normalizeParameterValue(param, match[1]); + } + } + + // Calculate confidence based on matches + entities.confidence = this.calculateConfidence(entities, input); + + return entities; + } catch (error) { + console.error('Entity extraction error:', error); + return { + primary_target: '', + parameters: {}, + confidence: 0 + }; + } + } + + private normalizeParameterValue(parameter: string, value: string): number | string { + switch (parameter) { + case 'brightness': + return Math.min(100, Math.max(0, parseInt(value))); + case 'temperature': + return parseInt(value); + case 'color': + return value.toLowerCase(); + default: + return value; + } + } + + private calculateConfidence(entities: ExtractedEntities, input: string): number { + let confidence = 0; + + // Device confidence + if (entities.primary_target) { + confidence += 0.5; + } + + // Parameter confidence + const paramCount = Object.keys(entities.parameters).length; + confidence += paramCount * 0.25; + + // Normalize confidence to 0-1 range + return Math.min(1, confidence); + } + + async updateDeviceMap(devices: Record): Promise { + for (const [key, value] of Object.entries(devices)) { + this.deviceNameMap.set(key, value); + } + } +} \ No newline at end of file diff --git a/src/ai/nlp/intent-classifier.ts b/src/ai/nlp/intent-classifier.ts new file mode 100644 index 0000000..2ae3a60 --- /dev/null +++ b/src/ai/nlp/intent-classifier.ts @@ -0,0 +1,177 @@ +interface ClassifiedIntent { + action: string; + target: string; + confidence: number; + parameters: Record; + raw_input: string; +} + +interface ActionPattern { + action: string; + patterns: RegExp[]; + parameters?: string[]; +} + +export class IntentClassifier { + private actionPatterns: ActionPattern[]; + + constructor() { + this.actionPatterns = [ + { + action: 'turn_on', + patterns: [ + /turn\s+on/i, + /switch\s+on/i, + /enable/i, + /activate/i + ] + }, + { + action: 'turn_off', + patterns: [ + /turn\s+off/i, + /switch\s+off/i, + /disable/i, + /deactivate/i + ] + }, + { + action: 'set', + patterns: [ + /set\s+(?:the\s+)?(.+)\s+to/i, + /change\s+(?:the\s+)?(.+)\s+to/i, + /adjust\s+(?:the\s+)?(.+)\s+to/i + ], + parameters: ['brightness', 'temperature', 'color'] + }, + { + action: 'query', + patterns: [ + /what\s+is/i, + /get\s+(?:the\s+)?(.+)/i, + /show\s+(?:the\s+)?(.+)/i, + /tell\s+me/i + ] + } + ]; + } + + async classify( + input: string, + extractedEntities: { parameters: Record; primary_target: string } + ): Promise { + let bestMatch: ClassifiedIntent = { + action: '', + target: '', + confidence: 0, + parameters: {}, + raw_input: input + }; + + for (const actionPattern of this.actionPatterns) { + for (const pattern of actionPattern.patterns) { + const match = input.match(pattern); + if (match) { + const confidence = this.calculateConfidence(match[0], input); + if (confidence > bestMatch.confidence) { + bestMatch = { + action: actionPattern.action, + target: extractedEntities.primary_target, + confidence, + parameters: this.extractActionParameters(actionPattern, match, extractedEntities), + raw_input: input + }; + } + } + } + } + + // If no match found, try to infer from context + if (!bestMatch.action) { + bestMatch = this.inferFromContext(input, extractedEntities); + } + + return bestMatch; + } + + private calculateConfidence(match: string, input: string): number { + // Base confidence from match length relative to input length + const lengthRatio = match.length / input.length; + let confidence = lengthRatio * 0.7; + + // Boost confidence for exact matches + if (match.toLowerCase() === input.toLowerCase()) { + confidence += 0.3; + } + + // Additional confidence for specific keywords + const keywords = ['please', 'can you', 'would you']; + for (const keyword of keywords) { + if (input.toLowerCase().includes(keyword)) { + confidence += 0.1; + } + } + + return Math.min(1, confidence); + } + + private extractActionParameters( + actionPattern: ActionPattern, + match: RegExpMatchArray, + extractedEntities: { parameters: Record; primary_target: string } + ): Record { + const parameters: Record = {}; + + // Copy relevant extracted entities + if (actionPattern.parameters) { + for (const param of actionPattern.parameters) { + if (extractedEntities.parameters[param] !== undefined) { + parameters[param] = extractedEntities.parameters[param]; + } + } + } + + // Extract additional parameters from match groups + if (match.length > 1 && match[1]) { + parameters.raw_parameter = match[1].trim(); + } + + return parameters; + } + + private inferFromContext( + input: string, + extractedEntities: { parameters: Record; primary_target: string } + ): ClassifiedIntent { + // Default to 'set' action if parameters are present + if (Object.keys(extractedEntities.parameters).length > 0) { + return { + action: 'set', + target: extractedEntities.primary_target, + confidence: 0.5, + parameters: extractedEntities.parameters, + raw_input: input + }; + } + + // Default to 'query' for question-like inputs + if (input.match(/^(what|when|where|who|how|why)/i)) { + return { + action: 'query', + target: extractedEntities.primary_target || 'system', + confidence: 0.6, + parameters: {}, + raw_input: input + }; + } + + // Fallback with low confidence + return { + action: 'unknown', + target: extractedEntities.primary_target || 'system', + confidence: 0.3, + parameters: {}, + raw_input: input + }; + } +} \ No newline at end of file diff --git a/src/ai/nlp/processor.ts b/src/ai/nlp/processor.ts new file mode 100644 index 0000000..c58e6ae --- /dev/null +++ b/src/ai/nlp/processor.ts @@ -0,0 +1,132 @@ +import { AIIntent, AIContext, AIConfidence, AIError } from '../types/index.js'; +import { EntityExtractor } from './entity-extractor.js'; +import { IntentClassifier } from './intent-classifier.js'; +import { ContextAnalyzer } from './context-analyzer.js'; + +export class NLPProcessor { + private entityExtractor: EntityExtractor; + private intentClassifier: IntentClassifier; + private contextAnalyzer: ContextAnalyzer; + + constructor() { + this.entityExtractor = new EntityExtractor(); + this.intentClassifier = new IntentClassifier(); + this.contextAnalyzer = new ContextAnalyzer(); + } + + async processCommand( + input: string, + context: AIContext + ): Promise<{ + intent: AIIntent; + confidence: AIConfidence; + error?: AIError; + }> { + try { + // Extract entities from the input + const entities = await this.entityExtractor.extract(input); + + // Classify the intent + const intent = await this.intentClassifier.classify(input, entities); + + // Analyze context relevance + const contextRelevance = await this.contextAnalyzer.analyze(intent, context); + + // Calculate confidence scores + const confidence: AIConfidence = { + overall: (intent.confidence + entities.confidence + contextRelevance.confidence) / 3, + intent: intent.confidence, + entities: entities.confidence, + context: contextRelevance.confidence + }; + + // Create structured intent + const structuredIntent: AIIntent = { + action: intent.action, + target: entities.primary_target, + parameters: { + ...entities.parameters, + ...intent.parameters, + context_parameters: contextRelevance.relevant_params + }, + raw_input: input + }; + + return { + intent: structuredIntent, + confidence + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + return { + intent: { + action: 'error', + target: 'system', + parameters: {}, + raw_input: input + }, + confidence: { + overall: 0, + intent: 0, + entities: 0, + context: 0 + }, + error: { + code: 'NLP_PROCESSING_ERROR', + message: errorMessage, + suggestion: 'Please try rephrasing your command', + recovery_options: [ + 'Use simpler language', + 'Break down the command into smaller parts', + 'Specify the target device explicitly' + ], + context + } + }; + } + } + + async validateIntent( + intent: AIIntent, + confidence: AIConfidence, + threshold = 0.7 + ): Promise { + return ( + confidence.overall >= threshold && + confidence.intent >= threshold && + confidence.entities >= threshold && + confidence.context >= threshold + ); + } + + async suggestCorrections( + input: string, + error: AIError + ): Promise { + // Implement correction suggestions based on the error + const suggestions: string[] = []; + + if (error.code === 'ENTITY_NOT_FOUND') { + suggestions.push( + 'Try specifying the device name more clearly', + 'Use the exact device name from your Home Assistant setup' + ); + } + + if (error.code === 'AMBIGUOUS_INTENT') { + suggestions.push( + 'Please specify what you want to do with the device', + 'Use action words like "turn on", "set", "adjust"' + ); + } + + if (error.code === 'CONTEXT_MISMATCH') { + suggestions.push( + 'Specify the location if referring to a device', + 'Clarify which device you mean in the current context' + ); + } + + return suggestions; + } +} \ No newline at end of file diff --git a/src/ai/templates/prompt-templates.ts b/src/ai/templates/prompt-templates.ts new file mode 100644 index 0000000..d1943de --- /dev/null +++ b/src/ai/templates/prompt-templates.ts @@ -0,0 +1,135 @@ +import { AIModel } from '../types/index.js'; + +interface PromptTemplate { + system: string; + user: string; + examples: Array<{ + user: string; + assistant: string; + }>; +} + +interface PromptVariables { + device_name?: string; + location?: string; + action?: string; + parameters?: Record; + context?: Record; + [key: string]: any; +} + +class PromptTemplates { + private templates: Record; + + constructor() { + this.templates = { + [AIModel.CLAUDE]: { + system: `You are Claude, an AI assistant specialized in home automation control through natural language. +Your role is to interpret user commands and translate them into specific device control actions. +Always maintain context awareness and consider user preferences and patterns. +Provide clear, concise responses and suggest relevant follow-up actions.`, + user: `Control the {device_name} in the {location} by {action} with parameters: {parameters}. +Current context: {context}`, + examples: [ + { + user: "Turn on the living room lights", + assistant: "I'll turn on the lights in the living room. Would you like me to set a specific brightness level?" + }, + { + user: "Set the temperature to 72 degrees", + assistant: "I'll set the temperature to 72°F. I'll monitor the temperature and let you know when it reaches the target." + } + ] + }, + [AIModel.GPT4]: { + system: `You are a home automation assistant powered by GPT-4. +Focus on precise command interpretation and execution. +Maintain high accuracy in device control and parameter settings. +Provide feedback on action success and system state changes.`, + user: `Command: {action} {device_name} in {location} +Parameters: {parameters} +Context: {context}`, + examples: [ + { + user: "Dim the bedroom lights to 50%", + assistant: "Setting bedroom light brightness to 50%. The change has been applied successfully." + }, + { + user: "Start the evening routine", + assistant: "Initiating evening routine: dimming lights, adjusting temperature, and enabling security system." + } + ] + }, + [AIModel.CUSTOM]: { + system: `Custom home automation assistant configuration. +Adapt to user preferences and patterns. +Learn from interactions and optimize responses. +Provide detailed feedback and suggestions.`, + user: `Action requested: {action} +Target device: {device_name} +Location: {location} +Parameters: {parameters} +Current context: {context}`, + examples: [ + { + user: "Make it cooler in here", + assistant: "Based on your preferences, I'll lower the temperature by 2 degrees. Current temperature is 74°F, adjusting to 72°F." + }, + { + user: "Set up movie mode", + assistant: "Activating movie mode: dimming lights to 20%, closing blinds, setting TV input to HDMI 1, and adjusting sound system." + } + ] + } + }; + } + + getTemplate(model: AIModel): PromptTemplate { + return this.templates[model]; + } + + formatPrompt(model: AIModel, variables: PromptVariables): string { + const template = this.getTemplate(model); + let prompt = template.user; + + // Replace variables in the prompt + for (const [key, value] of Object.entries(variables)) { + const placeholder = `{${key}}`; + if (typeof value === 'object') { + prompt = prompt.replace(placeholder, JSON.stringify(value)); + } else { + prompt = prompt.replace(placeholder, String(value)); + } + } + + return prompt; + } + + getSystemPrompt(model: AIModel): string { + return this.templates[model].system; + } + + getExamples(model: AIModel): Array<{ user: string; assistant: string }> { + return this.templates[model].examples; + } + + addExample( + model: AIModel, + example: { user: string; assistant: string } + ): void { + this.templates[model].examples.push(example); + } + + updateSystemPrompt(model: AIModel, newPrompt: string): void { + this.templates[model].system = newPrompt; + } + + createCustomTemplate( + model: AIModel.CUSTOM, + template: PromptTemplate + ): void { + this.templates[model] = template; + } +} + +export default new PromptTemplates(); \ No newline at end of file diff --git a/src/ai/types/index.ts b/src/ai/types/index.ts new file mode 100644 index 0000000..c246162 --- /dev/null +++ b/src/ai/types/index.ts @@ -0,0 +1,123 @@ +import { z } from 'zod'; + +// AI Model Types +export enum AIModel { + CLAUDE = 'claude', + GPT4 = 'gpt4', + CUSTOM = 'custom' +} + +// AI Confidence Level +export interface AIConfidence { + overall: number; + intent: number; + entities: number; + context: number; +} + +// AI Intent +export interface AIIntent { + action: string; + target: string; + parameters: Record; + raw_input: string; +} + +// AI Context +export interface AIContext { + user_id: string; + session_id: string; + timestamp: string; + location: string; + previous_actions: AIIntent[]; + environment_state: Record; +} + +// AI Response +export interface AIResponse { + natural_language: string; + structured_data: { + success: boolean; + action_taken: string; + entities_affected: string[]; + state_changes: Record; + }; + next_suggestions: string[]; + confidence: AIConfidence; + context: AIContext; +} + +// AI Error +export interface AIError { + code: string; + message: string; + suggestion: string; + recovery_options: string[]; + context: AIContext; +} + +// Rate Limiting +export interface AIRateLimit { + requests_per_minute: number; + requests_per_hour: number; + concurrent_requests: number; + model_specific_limits: Record; +} + +// Zod Schemas +export const AIConfidenceSchema = z.object({ + overall: z.number().min(0).max(1), + intent: z.number().min(0).max(1), + entities: z.number().min(0).max(1), + context: z.number().min(0).max(1) +}); + +export const AIIntentSchema = z.object({ + action: z.string(), + target: z.string(), + parameters: z.record(z.any()), + raw_input: z.string() +}); + +export const AIContextSchema = z.object({ + user_id: z.string(), + session_id: z.string(), + timestamp: z.string(), + location: z.string(), + previous_actions: z.array(AIIntentSchema), + environment_state: z.record(z.any()) +}); + +export const AIResponseSchema = z.object({ + natural_language: z.string(), + structured_data: z.object({ + success: z.boolean(), + action_taken: z.string(), + entities_affected: z.array(z.string()), + state_changes: z.record(z.any()) + }), + next_suggestions: z.array(z.string()), + confidence: AIConfidenceSchema, + context: AIContextSchema +}); + +export const AIErrorSchema = z.object({ + code: z.string(), + message: z.string(), + suggestion: z.string(), + recovery_options: z.array(z.string()), + context: AIContextSchema +}); + +export const AIRateLimitSchema = z.object({ + requests_per_minute: z.number(), + requests_per_hour: z.number(), + concurrent_requests: z.number(), + model_specific_limits: z.record(z.object({ + requests_per_minute: z.number(), + requests_per_hour: z.number() + })) +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index db1e2a4..2ffb8d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -519,9 +519,11 @@ async function main() { throw new Error(`Failed to ${service} automation: ${response.statusText}`); } + const responseData = await response.json() as AutomationResponse; return { success: true, message: `Successfully ${service}d automation ${params.automation_id}`, + automation_id: responseData.automation_id, }; } } catch (error) { diff --git a/src/platforms/macos/integration.ts b/src/platforms/macos/integration.ts new file mode 100644 index 0000000..bd6415d --- /dev/null +++ b/src/platforms/macos/integration.ts @@ -0,0 +1,215 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { EventEmitter } from 'events'; + +const execAsync = promisify(exec); + +interface MacOSNotification { + title: string; + message: string; + subtitle?: string; + sound?: boolean; +} + +interface MacOSPermissions { + notifications: boolean; + automation: boolean; + accessibility: boolean; +} + +class MacOSIntegration extends EventEmitter { + private permissions: MacOSPermissions; + + constructor() { + super(); + this.permissions = { + notifications: false, + automation: false, + accessibility: false + }; + } + + async initialize(): Promise { + await this.checkPermissions(); + await this.registerSystemEvents(); + } + + async checkPermissions(): Promise { + try { + // Check notification permissions + const { stdout: notifPerms } = await execAsync( + 'osascript -e \'tell application "System Events" to get properties\'' + ); + this.permissions.notifications = notifPerms.includes('notifications enabled:true'); + + // Check automation permissions + const { stdout: autoPerms } = await execAsync( + 'osascript -e \'tell application "System Events" to get UI elements enabled\'' + ); + this.permissions.automation = autoPerms.includes('true'); + + // Check accessibility permissions + const { stdout: accessPerms } = await execAsync( + 'osascript -e \'tell application "System Events" to get processes\'' + ); + this.permissions.accessibility = !accessPerms.includes('error'); + + return this.permissions; + } catch (error) { + console.error('Error checking permissions:', error); + return this.permissions; + } + } + + async sendNotification(notification: MacOSNotification): Promise { + if (!this.permissions.notifications) { + throw new Error('Notification permission not granted'); + } + + const script = ` + display notification "${notification.message}"${notification.subtitle ? ` with subtitle "${notification.subtitle}"` : '' + } with title "${notification.title}"${notification.sound ? ' sound name "default"' : '' + } + `; + + try { + await execAsync(`osascript -e '${script}'`); + } catch (error) { + console.error('Error sending notification:', error); + throw error; + } + } + + async registerSystemEvents(): Promise { + if (!this.permissions.automation) { + throw new Error('Automation permission not granted'); + } + + // Monitor system events + const script = ` + tell application "System Events" + set eventList to {} + + -- Monitor display sleep/wake + tell application "System Events" + set displayState to get sleeping + if displayState then + set end of eventList to "display_sleep" + else + set end of eventList to "display_wake" + end if + end tell + + -- Monitor power source changes + tell application "System Events" + set powerSource to get power source + set end of eventList to "power_" & powerSource + end tell + + return eventList + end tell + `; + + try { + const { stdout } = await execAsync(`osascript -e '${script}'`); + const events = stdout.split(',').map(e => e.trim()); + events.forEach(event => this.emit('system_event', event)); + } catch (error) { + console.error('Error monitoring system events:', error); + } + } + + async executeAutomation(script: string): Promise { + if (!this.permissions.automation) { + throw new Error('Automation permission not granted'); + } + + try { + const { stdout } = await execAsync(`osascript -e '${script}'`); + return stdout; + } catch (error) { + console.error('Error executing automation:', error); + throw error; + } + } + + async getSystemInfo(): Promise> { + const info: Record = {}; + + try { + // Get macOS version + const { stdout: version } = await execAsync('sw_vers -productVersion'); + info.os_version = version.trim(); + + // Get hardware info + const { stdout: hardware } = await execAsync('system_profiler SPHardwareDataType'); + info.hardware = this.parseSystemProfile(hardware); + + // Get power info + const { stdout: power } = await execAsync('pmset -g batt'); + info.power = this.parsePowerInfo(power); + + // Get network info + const { stdout: network } = await execAsync('networksetup -listallhardwareports'); + info.network = this.parseNetworkInfo(network); + + return info; + } catch (error) { + console.error('Error getting system info:', error); + throw error; + } + } + + private parseSystemProfile(output: string): Record { + const info: Record = {}; + const lines = output.split('\n'); + + for (const line of lines) { + const [key, value] = line.split(':').map(s => s.trim()); + if (key && value) { + info[key.toLowerCase().replace(/\s+/g, '_')] = value; + } + } + + return info; + } + + private parsePowerInfo(output: string): Record { + const info: Record = {}; + const lines = output.split('\n'); + + for (const line of lines) { + if (line.includes('Now drawing from')) { + info.power_source = line.includes('Battery') ? 'battery' : 'ac_power'; + } else if (line.includes('%')) { + const matches = line.match(/(\d+)%/); + if (matches) { + info.battery_percentage = parseInt(matches[1]); + } + } + } + + return info; + } + + private parseNetworkInfo(output: string): Record { + const info: Record = {}; + const lines = output.split('\n'); + let currentInterface: string | null = null; + + for (const line of lines) { + if (line.includes('Hardware Port:')) { + currentInterface = line.split(':')[1].trim(); + info[currentInterface] = {}; + } else if (currentInterface && line.includes('Device:')) { + info[currentInterface].device = line.split(':')[1].trim(); + } else if (currentInterface && line.includes('Ethernet Address:')) { + info[currentInterface].mac = line.split(':')[1].trim(); + } + } + + return info; + } +} + +export default MacOSIntegration; \ No newline at end of file diff --git a/src/schemas/hass.ts b/src/schemas/hass.ts index b5426c3..d050296 100644 --- a/src/schemas/hass.ts +++ b/src/schemas/hass.ts @@ -1,5 +1,5 @@ import { JSONSchemaType } from 'ajv'; -import type HomeAssistant from '../types/hass.js'; +import * as HomeAssistant from '../types/hass.js'; export const entitySchema: JSONSchemaType = { type: 'object', diff --git a/src/security/index.ts b/src/security/index.ts index 6431506..bb1e632 100644 --- a/src/security/index.ts +++ b/src/security/index.ts @@ -2,6 +2,7 @@ import crypto from 'crypto'; import { Request, Response, NextFunction } from 'express'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; +import { HelmetOptions } from 'helmet'; // Security configuration const RATE_LIMIT_WINDOW = 15 * 60 * 1000; // 15 minutes @@ -16,29 +17,26 @@ export const rateLimiter = rateLimit({ }); // Security configuration -const helmetConfig = { +const helmetConfig: HelmetOptions = { contentSecurityPolicy: { + useDefaults: true, directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], - connectSrc: ["'self'", process.env.HASS_HOST || ''], - upgradeInsecureRequests: true + connectSrc: ["'self'", 'wss:', 'https:'] } }, dnsPrefetchControl: true, - frameguard: { - action: 'deny' - }, + frameguard: true, hidePoweredBy: true, - hsts: { - maxAge: 31536000, - includeSubDomains: true, - preload: true - }, + hsts: true, + ieNoOpen: true, noSniff: true, - referrerPolicy: { policy: 'no-referrer' } + referrerPolicy: { + policy: ['no-referrer', 'strict-origin-when-cross-origin'] + } }; // Security headers middleware diff --git a/src/tools/index.ts b/src/tools/index.ts index 0dc7db3..1b13db3 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,13 +1,11 @@ -import { Tool } from 'litemcp'; import { z } from 'zod'; +import { get_hass } from '../hass/index.js'; // Tool category types export enum ToolCategory { - DEVICE_CONTROL = 'device_control', - SYSTEM_MANAGEMENT = 'system_management', - AUTOMATION = 'automation', - MONITORING = 'monitoring', - SECURITY = 'security' + DEVICE = 'device', + SYSTEM = 'system', + AUTOMATION = 'automation' } // Tool priority levels @@ -17,15 +15,20 @@ export enum ToolPriority { LOW = 'low' } -// Tool metadata interface -export interface ToolMetadata { +interface ToolParameters { + [key: string]: any; +} + +interface Tool { + name: string; + description: string; + execute(params: Params): Promise; +} + +interface ToolMetadata { category: ToolCategory; - priority: ToolPriority; - requiresAuth: boolean; - rateLimit?: { - windowMs: number; - max: number; - }; + platform: string; + version: string; caching?: { enabled: boolean; ttl: number; @@ -136,27 +139,34 @@ export class ToolRegistry { export const toolRegistry = new ToolRegistry(); // Tool decorator for easy registration -export function registerTool(metadata: ToolMetadata) { - return function (target: any) { - const tool: EnhancedTool = new target(); - tool.metadata = metadata; - toolRegistry.registerTool(tool); +function registerTool(metadata: Partial) { + return function (constructor: any) { + return constructor; }; } // Example usage: @registerTool({ - category: ToolCategory.DEVICE_CONTROL, - priority: ToolPriority.HIGH, - requiresAuth: true, + category: ToolCategory.DEVICE, + platform: 'hass', + version: '1.0.0', caching: { enabled: true, - ttl: 5000 // 5 seconds + 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(),