aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorA.J. Shulman <Shulman.aj@gmail.com>2025-05-27 14:08:11 -0400
committerA.J. Shulman <Shulman.aj@gmail.com>2025-05-27 14:08:11 -0400
commit656dbe6dc64013215eb312173df398fe4606d788 (patch)
tree05c2d35e5f636091c637779d1c8352c25e9ce7f6 /src/server
parentc3dba47bcda10bbcd72010c177afa8fd301e87e1 (diff)
feat: implement dynamic tool creation with deferred webpack rebuild and AI integration
Added runtime tool registry to Agent.ts for dynamic tool lookup Implemented CreateNewTool agent tool for AI-driven code analysis and tool generation Enabled deferred saving to avoid interrupting AI workflows with immediate rebuilds Introduced user-controlled modal for confirming tool installation and page reload Added REST API and secure server-side persistence for dynamic tools Built TypeScript validation, transpilation, and sandboxed execution for safe tool handling UI enhancements: modal with blur, responsive design, clear messaging Ensured compatibility with Webpack using dynamic require() calls Full error handling, code validation, and secure storage on client and server sides
Diffstat (limited to 'src/server')
-rw-r--r--src/server/api/dynamicTools.ts130
-rw-r--r--src/server/index.ts1
-rw-r--r--src/server/server_Initialization.ts5
3 files changed, 136 insertions, 0 deletions
diff --git a/src/server/api/dynamicTools.ts b/src/server/api/dynamicTools.ts
new file mode 100644
index 000000000..a7b7e1478
--- /dev/null
+++ b/src/server/api/dynamicTools.ts
@@ -0,0 +1,130 @@
+import * as express from 'express';
+import * as fs from 'fs';
+import * as path from 'path';
+
+// Define handler types to match project patterns
+type RouteHandler = (req: express.Request, res: express.Response) => any;
+
+/**
+ * Handles API endpoints for dynamic tools created by the agent
+ */
+export function setupDynamicToolsAPI(app: express.Express): void {
+ // Directory where dynamic tools will be stored
+ const dynamicToolsDir = path.join(process.cwd(), 'src', 'client', 'views', 'nodes', 'chatbot', 'tools', 'dynamic');
+
+ console.log(`Dynamic tools directory path: ${dynamicToolsDir}`);
+
+ // Ensure directory exists
+ if (!fs.existsSync(dynamicToolsDir)) {
+ try {
+ fs.mkdirSync(dynamicToolsDir, { recursive: true });
+ console.log(`Created dynamic tools directory at ${dynamicToolsDir}`);
+ } catch (error) {
+ console.error(`Failed to create dynamic tools directory: ${error}`);
+ }
+ }
+
+ /**
+ * Save a dynamic tool to the server
+ */
+ const saveDynamicTool: RouteHandler = (req, res) => {
+ try {
+ const { toolName, toolCode } = req.body;
+
+ if (!toolName || !toolCode) {
+ return res.status(400).json({
+ success: false,
+ error: 'Missing toolName or toolCode in request body',
+ });
+ }
+
+ // Validate the tool name (should be PascalCase)
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(toolName)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Tool name must be in PascalCase format',
+ });
+ }
+
+ // Create the file path
+ const filePath = path.join(dynamicToolsDir, `${toolName}.ts`);
+
+ // Check if file already exists and is different
+ let existingCode = '';
+ if (fs.existsSync(filePath)) {
+ existingCode = fs.readFileSync(filePath, 'utf8');
+ }
+
+ // Only write if the file doesn't exist or the content is different
+ if (existingCode !== toolCode) {
+ fs.writeFileSync(filePath, toolCode, 'utf8');
+ console.log(`Saved dynamic tool: ${toolName}`);
+ } else {
+ console.log(`Dynamic tool ${toolName} already exists with the same content`);
+ }
+
+ return res.json({ success: true, toolName });
+ } catch (error) {
+ console.error('Error saving dynamic tool:', error);
+ return res.status(500).json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+ };
+
+ /**
+ * Get a list of all available dynamic tools
+ */
+ const getDynamicTools: RouteHandler = (req, res) => {
+ try {
+ // Get all TypeScript files in the dynamic tools directory
+ const files = fs
+ .readdirSync(dynamicToolsDir)
+ .filter(file => file.endsWith('.ts'))
+ .map(file => ({
+ name: path.basename(file, '.ts'),
+ path: path.join('dynamic', file),
+ }));
+
+ return res.json({ success: true, tools: files });
+ } catch (error) {
+ console.error('Error getting dynamic tools:', error);
+ return res.status(500).json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+ };
+
+ /**
+ * Get the code for a specific dynamic tool
+ */
+ const getDynamicTool: RouteHandler = (req, res) => {
+ try {
+ const { toolName } = req.params;
+ const filePath = path.join(dynamicToolsDir, `${toolName}.ts`);
+
+ if (!fs.existsSync(filePath)) {
+ return res.status(404).json({
+ success: false,
+ error: `Tool ${toolName} not found`,
+ });
+ }
+
+ const toolCode = fs.readFileSync(filePath, 'utf8');
+ return res.json({ success: true, toolName, toolCode });
+ } catch (error) {
+ console.error('Error getting dynamic tool:', error);
+ return res.status(500).json({
+ success: false,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ });
+ }
+ };
+
+ // Register routes
+ app.post('/saveDynamicTool', saveDynamicTool);
+ app.get('/getDynamicTools', getDynamicTools);
+ app.get('/getDynamicTool/:toolName', getDynamicTool);
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 3b77359ec..887974ed8 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -2,6 +2,7 @@ import { yellow } from 'colors';
import * as dotenv from 'dotenv';
import * as mobileDetect from 'mobile-detect';
import * as path from 'path';
+import * as express from 'express';
import { logExecution } from './ActionUtilities';
import AssistantManager from './ApiManagers/AssistantManager';
import FlashcardManager from './ApiManagers/FlashcardManager';
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 514e2ce1e..80cf977ee 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -21,6 +21,7 @@ import { Database } from './database';
import { WebSocket } from './websocket';
import axios from 'axios';
import { JSDOM } from 'jsdom';
+import { setupDynamicToolsAPI } from './api/dynamicTools';
/* RouteSetter is a wrapper around the server that prevents the server
from being exposed. */
@@ -210,6 +211,10 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
// app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc)
registerCorsProxy(app); // this adds a /corsproxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies
+
+ // Set up the dynamic tools API
+ setupDynamicToolsAPI(app);
+
isRelease && !SSL.Loaded && SSL.exit();
routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));