@sschepis/robodev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,310 @@
1
+ // Custom tools management system
2
+ // Handles loading, saving, validating, and managing custom tools
3
+
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { consoleStyler } from '../ui/console-styler.mjs';
7
+
8
+ export class CustomToolsManager {
9
+ constructor() {
10
+ this.customTools = new Map(); // Map of tool_name -> function
11
+ this.customToolSchemas = new Map(); // Map of tool_name -> schema
12
+ this.toolsFilePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../.tools.json');
13
+ }
14
+
15
+ // Load custom tools from .tools.json on startup
16
+ async loadCustomTools() {
17
+ try {
18
+ const fs = await import('fs');
19
+ if (!fs.existsSync(this.toolsFilePath)) {
20
+ consoleStyler.log('tools', `No custom tools file found at ${this.toolsFilePath}`);
21
+ return [];
22
+ }
23
+
24
+ const toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
25
+ let loadedCount = 0;
26
+ const loadedSchemas = [];
27
+
28
+ for (const [toolName, toolDef] of Object.entries(toolsData.tools || {})) {
29
+ try {
30
+ if (this.validateCustomTool(toolDef)) {
31
+ // Convert function string back to executable function
32
+ const toolFunction = new Function('return ' + toolDef.function)();
33
+
34
+ // Add to our maps
35
+ this.customTools.set(toolName, toolFunction);
36
+ this.customToolSchemas.set(toolName, toolDef.schema);
37
+
38
+ // Add schema to return array
39
+ loadedSchemas.push(toolDef.schema);
40
+
41
+ loadedCount++;
42
+ consoleStyler.log('tools', `✓ Loaded custom tool: ${toolName}`);
43
+ } else {
44
+ consoleStyler.log('warning', `✗ Invalid tool: ${toolName}`);
45
+ }
46
+ } catch (error) {
47
+ consoleStyler.log('error', `✗ Failed to load tool ${toolName}: ${error.message}`);
48
+ }
49
+ }
50
+
51
+ if (loadedCount > 0) {
52
+ consoleStyler.log('tools', `Loaded ${loadedCount} custom tools`);
53
+ }
54
+
55
+ return loadedSchemas;
56
+ } catch (error) {
57
+ consoleStyler.log('warning', `Failed to load custom tools: ${error.message}`);
58
+ return [];
59
+ }
60
+ }
61
+
62
+ // Validate a custom tool before loading
63
+ validateCustomTool(toolDef) {
64
+ // Check required fields
65
+ if (!toolDef.schema || !toolDef.function) return false;
66
+
67
+ // Validate schema structure
68
+ if (!toolDef.schema.function || !toolDef.schema.function.name) return false;
69
+
70
+ // Basic security checks - block dangerous patterns
71
+ const dangerousPatterns = [
72
+ /process\.exit\s*\(/,
73
+ /require\s*\(\s*['"]child_process['"]\s*\)/,
74
+ /import\s*\(\s*['"]child_process['"]\s*\)/,
75
+ /eval\s*\(/,
76
+ /Function\s*\(/,
77
+ /fs\.rmSync/,
78
+ /fs\.unlinkSync.*\.\./, // Prevent path traversal deletion
79
+ /rm\s+-rf/,
80
+ /format\s+c:/i
81
+ ];
82
+
83
+ for (const pattern of dangerousPatterns) {
84
+ if (pattern.test(toolDef.function)) {
85
+ consoleStyler.log('error', `SECURITY: Rejected tool with dangerous pattern: ${pattern}`);
86
+ return false;
87
+ }
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ // Save a custom tool to .tools.json
94
+ async saveCustomTool(toolName, toolFunction, toolSchema, category = 'utility') {
95
+ try {
96
+ const fs = await import('fs');
97
+
98
+ // Load existing tools or create new structure
99
+ let toolsData = { version: '1.0.0', tools: {} };
100
+ if (fs.existsSync(this.toolsFilePath)) {
101
+ try {
102
+ toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
103
+ } catch (e) {
104
+ consoleStyler.log('warning', 'Corrupted tools file, creating new one');
105
+ }
106
+ }
107
+
108
+ // Add the new tool
109
+ toolsData.tools[toolName] = {
110
+ schema: toolSchema,
111
+ function: toolFunction.toString(),
112
+ metadata: {
113
+ created_at: new Date().toISOString(),
114
+ category: category,
115
+ usage_count: 0,
116
+ last_used: null
117
+ }
118
+ };
119
+
120
+ // Create backup first
121
+ if (fs.existsSync(this.toolsFilePath)) {
122
+ const backupPath = this.toolsFilePath.replace('.json', '.backup.json');
123
+ fs.copyFileSync(this.toolsFilePath, backupPath);
124
+ }
125
+
126
+ // Save the updated tools
127
+ fs.writeFileSync(this.toolsFilePath, JSON.stringify(toolsData, null, 2), 'utf8');
128
+
129
+ // Add to runtime maps
130
+ this.customTools.set(toolName, toolFunction);
131
+ this.customToolSchemas.set(toolName, toolSchema);
132
+
133
+ consoleStyler.log('tools', `✓ Saved custom tool: ${toolName}`);
134
+ return { success: true, schema: toolSchema };
135
+ } catch (error) {
136
+ consoleStyler.log('error', `✗ Failed to save tool ${toolName}: ${error.message}`);
137
+ return { success: false, error: error.message };
138
+ }
139
+ }
140
+
141
+ // Update tool usage statistics
142
+ async updateToolUsage(toolName) {
143
+ try {
144
+ const fs = await import('fs');
145
+ if (!fs.existsSync(this.toolsFilePath)) return;
146
+
147
+ const toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
148
+ if (toolsData.tools && toolsData.tools[toolName]) {
149
+ toolsData.tools[toolName].metadata.usage_count = (toolsData.tools[toolName].metadata.usage_count || 0) + 1;
150
+ toolsData.tools[toolName].metadata.last_used = new Date().toISOString();
151
+
152
+ fs.writeFileSync(this.toolsFilePath, JSON.stringify(toolsData, null, 2), 'utf8');
153
+ }
154
+ } catch (error) {
155
+ // Ignore usage tracking errors
156
+ }
157
+ }
158
+
159
+ // List custom tools with optional filtering
160
+ async listCustomTools(category = null, showUsage = false) {
161
+ try {
162
+ const fs = await import('fs');
163
+
164
+ if (!fs.existsSync(this.toolsFilePath)) {
165
+ return "No custom tools found.";
166
+ }
167
+
168
+ const toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
169
+ const tools = toolsData.tools || {};
170
+
171
+ let filteredTools = Object.entries(tools);
172
+ if (category) {
173
+ filteredTools = filteredTools.filter(([name, tool]) =>
174
+ tool.metadata.category === category
175
+ );
176
+ }
177
+
178
+ if (filteredTools.length === 0) {
179
+ return category ?
180
+ `No custom tools found in category: ${category}` :
181
+ "No custom tools found.";
182
+ }
183
+
184
+ let output = `Found ${filteredTools.length} custom tool(s):\n\n`;
185
+
186
+ for (const [name, tool] of filteredTools) {
187
+ output += `• **${name}** (${tool.metadata.category})\n`;
188
+ output += ` ${tool.schema.function.description}\n`;
189
+
190
+ if (showUsage) {
191
+ output += ` Used: ${tool.metadata.usage_count || 0} times\n`;
192
+ if (tool.metadata.last_used) {
193
+ output += ` Last used: ${new Date(tool.metadata.last_used).toLocaleString()}\n`;
194
+ }
195
+ }
196
+ output += `\n`;
197
+ }
198
+
199
+ return output.trim();
200
+ } catch (error) {
201
+ return `Error listing tools: ${error.message}`;
202
+ }
203
+ }
204
+
205
+ // Remove a custom tool
206
+ async removeCustomTool(toolName) {
207
+ try {
208
+ const fs = await import('fs');
209
+
210
+ if (!fs.existsSync(this.toolsFilePath)) {
211
+ return { success: false, message: "No custom tools file found." };
212
+ }
213
+
214
+ const toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
215
+
216
+ if (!toolsData.tools || !toolsData.tools[toolName]) {
217
+ return { success: false, message: `Tool '${toolName}' not found.` };
218
+ }
219
+
220
+ // Remove from file
221
+ delete toolsData.tools[toolName];
222
+ fs.writeFileSync(this.toolsFilePath, JSON.stringify(toolsData, null, 2), 'utf8');
223
+
224
+ // Remove from runtime
225
+ this.customTools.delete(toolName);
226
+ this.customToolSchemas.delete(toolName);
227
+
228
+ consoleStyler.log('tools', `✓ Removed custom tool: ${toolName}`);
229
+ return { success: true, message: `✓ Successfully removed tool: ${toolName}` };
230
+ } catch (error) {
231
+ return { success: false, message: `Error removing tool: ${error.message}` };
232
+ }
233
+ }
234
+
235
+ // Export tools to a file
236
+ async exportTools(outputFile = 'exported_tools.json', toolsToExport = null) {
237
+ try {
238
+ const fs = await import('fs');
239
+
240
+ if (!fs.existsSync(this.toolsFilePath)) {
241
+ return { success: false, message: "No custom tools found to export." };
242
+ }
243
+
244
+ const toolsData = JSON.parse(fs.readFileSync(this.toolsFilePath, 'utf8'));
245
+ const allTools = toolsData.tools || {};
246
+
247
+ let exportData = {
248
+ version: toolsData.version || '1.0.0',
249
+ exported_at: new Date().toISOString(),
250
+ tools: {}
251
+ };
252
+
253
+ if (toolsToExport && toolsToExport.length > 0) {
254
+ // Export specific tools
255
+ for (const toolName of toolsToExport) {
256
+ if (allTools[toolName]) {
257
+ exportData.tools[toolName] = allTools[toolName];
258
+ }
259
+ }
260
+ } else {
261
+ // Export all tools
262
+ exportData.tools = allTools;
263
+ }
264
+
265
+ fs.writeFileSync(outputFile, JSON.stringify(exportData, null, 2), 'utf8');
266
+
267
+ const toolCount = Object.keys(exportData.tools).length;
268
+ return {
269
+ success: true,
270
+ message: `✓ Successfully exported ${toolCount} tool(s) to: ${outputFile}`
271
+ };
272
+ } catch (error) {
273
+ return { success: false, message: `Error exporting tools: ${error.message}` };
274
+ }
275
+ }
276
+
277
+ // Check if a tool exists
278
+ hasCustomTool(toolName) {
279
+ return this.customTools.has(toolName);
280
+ }
281
+
282
+ // Get a custom tool function
283
+ getCustomTool(toolName) {
284
+ return this.customTools.get(toolName);
285
+ }
286
+
287
+ // Get all custom tool schemas
288
+ getCustomToolSchemas() {
289
+ return Array.from(this.customToolSchemas.values());
290
+ }
291
+
292
+ // Execute a custom tool
293
+ async executeCustomTool(toolName, args) {
294
+ if (!this.hasCustomTool(toolName)) {
295
+ throw new Error(`Custom tool '${toolName}' not found`);
296
+ }
297
+
298
+ const toolFunction = this.getCustomTool(toolName);
299
+ consoleStyler.log('custom', `Executing custom tool: ${toolName}`);
300
+
301
+ try {
302
+ const result = await toolFunction(...Object.values(args));
303
+ await this.updateToolUsage(toolName);
304
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
305
+ } catch (error) {
306
+ consoleStyler.log('error', `Custom tool error: ${error.message}`);
307
+ throw new Error(`Custom tool error: ${error.message}`);
308
+ }
309
+ }
310
+ }