powr-sdk-api
Version:
Shared API core library for PowrStack projects. Zero dependencies - works with Express, Next.js API routes, and other frameworks. All features are optional and install only what you need.
503 lines (477 loc) • 14.4 kB
JavaScript
"use strict";
const {
getDb
} = require("../services/mongo");
const crypto = require('crypto');
const {
config
} = require('../config');
class ToolsManager {
constructor() {
this.registeredTools = new Map();
this.isInitialized = false;
this.isCentralService = false;
this.isEnabled = false; // Default disabled
}
async initialize(options = {}) {
this.isCentralService = options.isCentralService || false;
this.isEnabled = options.enableTools === true; // Default false unless explicitly enabled
if (!this.isEnabled) {
console.log("🚫 Tools is disabled");
this.isInitialized = true;
return;
}
if (this.isCentralService) {
// Central service: Don't pre-load, load dynamically
console.log("🔄 Central service mode - loading tools dynamically");
this.isInitialized = true;
} else {
// Individual API: Load all available tools
console.log("📦 Loading tools registry...");
try {
const db = await getDb();
const tools = await db.collection("tools").find({}).toArray();
tools.forEach(tool => {
this.registerTool(tool);
});
this.isInitialized = true;
console.log(`✅ Loaded ${tools.length} tools from registry`);
} catch (error) {
console.error("❌ Failed to initialize tools:", error);
this.isInitialized = true; // Still mark as initialized to prevent blocking
}
}
}
// Register a new tool
registerTool(tool) {
var _tool$_id;
const toolId = tool.id || ((_tool$_id = tool._id) === null || _tool$_id === void 0 ? void 0 : _tool$_id.toString());
if (!toolId) {
console.error("❌ Tool must have an ID");
return;
}
this.registeredTools.set(toolId, {
...tool,
registeredAt: new Date(),
isActive: true
});
console.log(`✅ Registered tool: ${tool.name} (${toolId})`);
}
// Get all available tools
getAllTools() {
return Array.from(this.registeredTools.values()).filter(tool => tool.isActive);
}
// Get tool by ID
getTool(toolId) {
return this.registeredTools.get(toolId);
}
// Get tools by category
getToolsByCategory(category) {
return this.getAllTools().filter(tool => tool.category === category);
}
// Get user's configured tools
async getUserTools(userId) {
if (!this.isEnabled) {
return [];
}
try {
const db = await getDb();
const userTools = await db.collection("user_tools").find({
userId
}).toArray();
return userTools;
} catch (error) {
console.error("❌ Failed to get user tools:", error);
return [];
}
}
// Configure tool for user
async configureTool(userId, toolId, config) {
if (!this.isEnabled) {
return {
success: false,
message: "Tools is disabled"
};
}
try {
const db = await getDb();
const tool = this.getTool(toolId);
if (!tool) {
return {
success: false,
message: "Tool not found"
};
}
// Validate configuration against tool schema
if (!this.validateToolConfig(tool, config)) {
return {
success: false,
message: "Invalid configuration"
};
}
// Encrypt sensitive data
const encryptedConfig = this.encryptConfig(config);
const userTool = {
userId,
toolId,
config: encryptedConfig,
enabled: true,
createdAt: new Date(),
updatedAt: new Date()
};
await db.collection("user_tools").updateOne({
userId,
toolId
}, {
$set: userTool
}, {
upsert: true
});
console.log(`✅ Configured tool ${toolId} for user ${userId}`);
return {
success: true,
message: "Tool configured successfully"
};
} catch (error) {
console.error("❌ Failed to configure tool:", error);
return {
success: false,
message: "Failed to configure tool"
};
}
}
// Execute tool action
async executeToolAction(options = {}) {
const {
userId,
toolId,
actionId,
params
} = options;
if (!this.isEnabled) {
return {
success: false,
message: "Tools is disabled"
};
}
try {
const tool = this.getTool(toolId);
if (!tool) {
return {
success: false,
message: "Tool not found"
};
}
// Get user's tool configuration
const db = await getDb();
const userTool = await db.collection("user_tools").findOne({
userId,
toolId
});
if (!userTool || !userTool.enabled) {
return {
success: false,
message: "Tool not configured or disabled"
};
}
// Decrypt configuration
const config = this.decryptConfig(userTool.config);
// Execute the action
const result = await this.executeAction(tool, actionId, params, config);
// Log execution
await this.logToolExecution(userId, toolId, actionId, params, result);
return result;
} catch (error) {
console.error("❌ Failed to execute tool action:", error);
return {
success: false,
message: "Failed to execute tool action"
};
}
}
// Test tool connection
async testToolConnection(userId, toolId) {
if (!this.isEnabled) {
return {
success: false,
message: "Tools is disabled"
};
}
try {
const tool = this.getTool(toolId);
if (!tool) {
return {
success: false,
message: "Tool not found"
};
}
// Get user's tool configuration
const db = await getDb();
const userTool = await db.collection("user_tools").findOne({
userId,
toolId
});
if (!userTool) {
return {
success: false,
message: "Tool not configured"
};
}
// Decrypt configuration
const config = this.decryptConfig(userTool.config);
// Test connection
const result = await this.testConnection(tool, config);
return result;
} catch (error) {
console.error("❌ Failed to test tool connection:", error);
return {
success: false,
message: "Failed to test tool connection"
};
}
}
// Toggle tool for user
async toggleTool(userId, toolId, enabled) {
if (!this.isEnabled) {
return {
success: false,
message: "Tools is disabled"
};
}
try {
const db = await getDb();
await db.collection("user_tools").updateOne({
userId,
toolId
}, {
$set: {
enabled,
updatedAt: new Date()
}
});
console.log(`${enabled ? '✅' : '❌'} Tool ${toolId} ${enabled ? 'enabled' : 'disabled'} for user ${userId}`);
return {
success: true,
message: `Tool ${enabled ? 'enabled' : 'disabled'} successfully`
};
} catch (error) {
console.error("❌ Failed to toggle tool:", error);
return {
success: false,
message: "Failed to toggle tool"
};
}
}
// Helper methods
validateToolConfig(tool, config) {
// Basic validation - can be extended based on tool schema
if (!tool.configSchema) return true;
// Validate required fields
for (const [field, schema] of Object.entries(tool.configSchema)) {
if (schema.required && !config[field]) {
return false;
}
}
return true;
}
encryptConfig(config) {
// Simple encryption - in production, use proper encryption
return Buffer.from(JSON.stringify(config)).toString('base64');
}
decryptConfig(encryptedConfig) {
// Simple decryption - in production, use proper decryption
return JSON.parse(Buffer.from(encryptedConfig, 'base64').toString());
}
async executeAction(tool, actionId, params, config) {
try {
switch (tool.name.toLowerCase()) {
case 'gmail':
return await this.executeGmailAction(actionId, params, config);
case 'weather':
case 'openweather':
return await this.executeWeatherAction(actionId, params, config, tool);
case 'slack':
return await this.executeSlackAction(actionId, params, config);
case 'github':
return await this.executeGitHubAction(actionId, params, config);
default:
return {
success: false,
message: `Tool ${tool.name} not implemented yet`
};
}
} catch (error) {
console.error(`❌ Error executing ${tool.name} action:`, error);
return {
success: false,
message: `Failed to execute ${actionId} on ${tool.name}: ${error.message}`
};
}
}
async executeGmailAction(actionId, params, config) {
const nodemailer = require('nodemailer');
try {
// Create transporter using the exact config from ai-pitcher
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
user: config.email,
pass: config.appPassword
}
});
switch (actionId) {
case 'send-email':
const mailOptions = {
from: config.email,
to: params.to,
subject: params.subject,
text: params.body
};
const result = await transporter.sendMail(mailOptions);
return {
success: true,
message: `Email sent successfully to ${params.to}`,
data: {
messageId: result.messageId,
to: params.to,
subject: params.subject
}
};
default:
return {
success: false,
message: `Unknown Gmail action: ${actionId}`
};
}
} catch (error) {
console.error('❌ Gmail execution error:', error);
return {
success: false,
message: `Failed to send email: ${error.message}`
};
}
}
async executeWeatherAction(actionId, params, config, tool) {
const axios = require('axios');
try {
var _tool$systemConfig;
// Get weather data from system config (database) or fallback to env var
const weatherApiKey = (tool === null || tool === void 0 || (_tool$systemConfig = tool.systemConfig) === null || _tool$systemConfig === void 0 ? void 0 : _tool$systemConfig.apiKey) || process.env.WEATHER_API_KEY || 'demo';
const location = config.location;
let weatherData;
if (weatherApiKey === 'demo') {
// Demo weather data for testing
weatherData = {
weather: [{
main: params.condition || 'Rain',
description: 'light rain'
}]
};
console.log('🌤️ Using demo weather data');
} else {
// Real weather API call
const weatherResponse = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${weatherApiKey}&units=metric`);
weatherData = weatherResponse.data;
}
// Check if condition matches
const currentWeather = weatherData.weather[0].main.toLowerCase();
const targetCondition = ((params === null || params === void 0 ? void 0 : params.condition) || 'rain').toLowerCase();
const operator = (params === null || params === void 0 ? void 0 : params.operator) || 'is';
let conditionMet = false;
switch (operator) {
case 'is':
conditionMet = currentWeather === targetCondition;
break;
case 'is_not':
conditionMet = currentWeather !== targetCondition;
break;
case 'contains':
conditionMet = currentWeather.includes(targetCondition) || weatherData.weather[0].description.toLowerCase().includes(targetCondition);
break;
default:
conditionMet = currentWeather === targetCondition;
}
return {
success: true,
message: `Weather check completed. Current: ${currentWeather}, Required: ${targetCondition}`,
data: {
currentWeather: currentWeather,
requiredCondition: targetCondition,
conditionMet: conditionMet,
location: location
}
};
} catch (error) {
console.error('❌ Weather check error:', error);
return {
success: false,
message: `Failed to check weather: ${error.message}`
};
}
}
async executeSlackAction(actionId, params, config) {
// TODO: Implement Slack actions
return {
success: false,
message: "Slack integration not implemented yet"
};
}
async executeGitHubAction(actionId, params, config) {
// TODO: Implement GitHub actions
return {
success: false,
message: "GitHub integration not implemented yet"
};
}
async testConnection(tool, config) {
// This would test the actual connection based on tool type
// For now, return a mock response
return {
success: true,
message: `Connection to ${tool.name} successful`
};
}
async logToolExecution(userId, toolId, actionId, params, result) {
try {
const db = await getDb();
await db.collection("tool_executions").insertOne({
userId,
toolId,
actionId,
params,
result,
timestamp: new Date()
});
} catch (error) {
console.error("❌ Failed to log tool execution:", error);
}
}
// Get statistics
getStats() {
return {
isEnabled: this.isEnabled,
isInitialized: this.isInitialized,
totalTools: this.registeredTools.size,
activeTools: this.getAllTools().length
};
}
// Enable/disable tools
enable() {
this.isEnabled = true;
console.log("✅ Tools enabled");
}
disable() {
this.isEnabled = false;
console.log("❌ Tools disabled");
}
toggle() {
this.isEnabled = !this.isEnabled;
console.log(`${this.isEnabled ? '✅' : '❌'} Tools ${this.isEnabled ? 'enabled' : 'disabled'}`);
return this.isEnabled;
}
}
// Create and export singleton instance
const toolsManager = new ToolsManager();
module.exports = toolsManager;