src / tools / automationTools.ts
src / tools / automationTools.ts
/**
* Automation Tools
* Provides cron job scheduling, task queue management, and workflow execution.
*/
import cronModule from 'node-cron';
const cron = cronModule;
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import { validatePath } from '../pathSecurity';
// Cron jobs storage (in-memory with file persistence)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ScheduledTask = any;
declare global {
var __cron_jobs__: Map<string, ScheduledTask>;
}
if (!global.__cron_jobs__) {
global.__cron_jobs__ = new Map();
}
// Task queue (priority-based)
interface QueuedTask {
id: string;
task: () => Promise<any>;
priority: number; // Higher = more important
added_at: Date;
}
let taskQueue: QueuedTask[] = [];
// Zod schemas for validation
const cronScheduleSchema = z.object({
cron_expr: z.string().min(1),
command: z.string().min(1),
});
const cronDeleteSchema = z.object({
job_id: z.string(),
});
const taskQueueAddSchema = z.object({
task_name: z.string().min(1),
priority: z.number().int().min(0).max(100).optional().default(50),
});
const workflowExecuteSchema = z.object({
steps: z.array(z.any()),
});
/**
* Schedule a recurring cron job.
*/
export async function cron_schedule(cron_expr: string, command: string, workspaceRoot: string = process.cwd()): Promise<{ job_id: string; next_run: string }> {
const validated = cronScheduleSchema.parse({ cron_expr, command });
// Validate cron expression (basic check)
if ((cron as any).validate && !(cron as any).validate(validated.cron_expr)) {
throw new Error(`Invalid cron expression: ${validated.cron_expr}`);
}
const job_id = `cron_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create scheduled task
const task = cron.schedule(validated.cron_expr, async () => {
try {
await executeCommand(validated.command, workspaceRoot);
} catch (error) {
console.error(`Cron job ${job_id} failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
global.__cron_jobs__.set(job_id, task);
// Persist to file
await persistCronJobs(workspaceRoot);
return {
job_id,
next_run: getNextRunTime(validated.cron_expr)
};
}
/**
* List all active cron jobs.
*/
export async function cron_list(): Promise<{ jobs: Array<{ id: string; expr: string; command: string; next_run: string }> }> {
const jobs = [];
for (const [id, task] of global.__cron_jobs__.entries()) {
// Get the cron expression from the task (we need to store it)
// For now, we'll return what we have
jobs.push({
id,
expr: '', // Would need to store this separately
command: '',
next_run: ''
});
}
return { jobs };
}
/**
* Delete a scheduled cron job.
*/
export async function cron_delete(job_id: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean }> {
const validated = cronDeleteSchema.parse({ job_id });
const task = global.__cron_jobs__.get(validated.job_id);
if (!task) {
return { success: false };
}
task.stop();
global.__cron_jobs__.delete(validated.job_id);
// Persist to file
await persistCronJobs(workspaceRoot);
return { success: true };
}
/**
* Add a task to the priority queue.
*/
export async function task_queue_add(task_name: string, priority?: number): Promise<{ queue_id: string; position: number }> {
const validated = taskQueueAddSchema.parse({ task_name, priority });
const queue_id = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const queuedTask: QueuedTask = {
id: queue_id,
task: async () => {
// Execute the task (would need to be passed in separately)
return { success: true };
},
priority: validated.priority || 50,
added_at: new Date()
};
// Insert into queue maintaining priority order
let position = 0;
for (const existing of taskQueue) {
if (existing.priority > queuedTask.priority) {
break;
}
position++;
}
taskQueue.splice(position, 0, queuedTask);
return { queue_id: queue_id, position };
}
/**
* Execute a workflow of steps.
*/
export async function workflow_execute(steps: any[]): Promise<{ results: Array<{ step: number; success: boolean; result?: any; error?: string }>; errors: number }> {
const validated = workflowExecuteSchema.parse({ steps });
const results = [];
let errors = 0;
for (let i = 0; i < validated.steps.length; i++) {
try {
const step = validated.steps[i];
// Execute the step based on its type
let result: any;
if (step.type === 'command') {
result = await executeCommand(step.command);
} else if (step.type === 'file_operation') {
result = await performFileOperation(step.operation, step.path);
} else if (typeof step.execute === 'function') {
result = await step.execute();
} else {
throw new Error(`Unknown step type: ${step.type}`);
}
results.push({ step: i + 1, success: true, result });
} catch (error) {
errors++;
results.push({
step: i + 1,
success: false,
error: error instanceof Error ? error.message : String(error)
});
// Continue to next step even if one fails
}
}
return { results, errors };
}
/**
* Helper function to execute a command.
*/
async function executeCommand(command: string, cwd: string = process.cwd()): Promise<{ stdout: string; stderr: string }> {
const { exec } = await import('child_process');
return new Promise((resolve, reject) => {
exec(command, { cwd }, (error, stdout, stderr) => {
if (error) {
resolve({ stdout: stdout || '', stderr: stderr || error.message });
} else {
resolve({ stdout: stdout || '', stderr: stderr || '' });
}
});
});
}
/**
* Helper function to perform file operations.
*/
async function performFileOperation(operation: string, path_str: string, workspaceRoot: string = process.cwd()): Promise<any> {
const fs = await import('fs/promises');
const safePath = validatePath(workspaceRoot, workspaceRoot, path_str);
switch (operation) {
case 'read':
return await fs.readFile(safePath, 'utf-8');
case 'write':
// Would need content parameter - simplified for now
throw new Error('Write operation requires content parameter');
case 'delete':
await fs.unlink(safePath);
return { success: true };
default:
throw new Error(`Unknown file operation: ${operation}`);
}
}
/**
* Get next run time for a cron expression.
*/
function getNextRunTime(cron_expr: string): string {
// Simple implementation - would need proper cron parsing library
return 'N/A (would require cron-parser library)';
}
/**
* Persist cron jobs to file.
*/
async function persistCronJobs(workspaceRoot: string = process.cwd()): Promise<void> {
const configuredPath = process.env.CRON_STORAGE_PATH;
const storagePath = configuredPath
? validatePath(workspaceRoot, workspaceRoot, configuredPath)
: path.join(workspaceRoot, '.cron_jobs.json');
try {
const jobsData: Array<{ id: string; expr: string; command: string }> = [];
for (const [id, task] of global.__cron_jobs__.entries()) {
// Would need to extract cron expression from task - simplified here
jobsData.push({ id, expr: '', command: '' });
}
await fs.writeFile(storagePath, JSON.stringify(jobsData, null, 2));
} catch (error) {
console.error(`Failed to persist cron jobs: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Automation Tools
* Provides cron job scheduling, task queue management, and workflow execution.
*/
import cronModule from 'node-cron';
const cron = cronModule;
import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import { validatePath } from '../pathSecurity';
// Cron jobs storage (in-memory with file persistence)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ScheduledTask = any;
declare global {
var __cron_jobs__: Map<string, ScheduledTask>;
}
if (!global.__cron_jobs__) {
global.__cron_jobs__ = new Map();
}
// Task queue (priority-based)
interface QueuedTask {
id: string;
task: () => Promise<any>;
priority: number; // Higher = more important
added_at: Date;
}
let taskQueue: QueuedTask[] = [];
// Zod schemas for validation
const cronScheduleSchema = z.object({
cron_expr: z.string().min(1),
command: z.string().min(1),
});
const cronDeleteSchema = z.object({
job_id: z.string(),
});
const taskQueueAddSchema = z.object({
task_name: z.string().min(1),
priority: z.number().int().min(0).max(100).optional().default(50),
});
const workflowExecuteSchema = z.object({
steps: z.array(z.any()),
});
/**
* Schedule a recurring cron job.
*/
export async function cron_schedule(cron_expr: string, command: string, workspaceRoot: string = process.cwd()): Promise<{ job_id: string; next_run: string }> {
const validated = cronScheduleSchema.parse({ cron_expr, command });
// Validate cron expression (basic check)
if ((cron as any).validate && !(cron as any).validate(validated.cron_expr)) {
throw new Error(`Invalid cron expression: ${validated.cron_expr}`);
}
const job_id = `cron_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Create scheduled task
const task = cron.schedule(validated.cron_expr, async () => {
try {
await executeCommand(validated.command, workspaceRoot);
} catch (error) {
console.error(`Cron job ${job_id} failed: ${error instanceof Error ? error.message : String(error)}`);
}
});
global.__cron_jobs__.set(job_id, task);
// Persist to file
await persistCronJobs(workspaceRoot);
return {
job_id,
next_run: getNextRunTime(validated.cron_expr)
};
}
/**
* List all active cron jobs.
*/
export async function cron_list(): Promise<{ jobs: Array<{ id: string; expr: string; command: string; next_run: string }> }> {
const jobs = [];
for (const [id, task] of global.__cron_jobs__.entries()) {
// Get the cron expression from the task (we need to store it)
// For now, we'll return what we have
jobs.push({
id,
expr: '', // Would need to store this separately
command: '',
next_run: ''
});
}
return { jobs };
}
/**
* Delete a scheduled cron job.
*/
export async function cron_delete(job_id: string, workspaceRoot: string = process.cwd()): Promise<{ success: boolean }> {
const validated = cronDeleteSchema.parse({ job_id });
const task = global.__cron_jobs__.get(validated.job_id);
if (!task) {
return { success: false };
}
task.stop();
global.__cron_jobs__.delete(validated.job_id);
// Persist to file
await persistCronJobs(workspaceRoot);
return { success: true };
}
/**
* Add a task to the priority queue.
*/
export async function task_queue_add(task_name: string, priority?: number): Promise<{ queue_id: string; position: number }> {
const validated = taskQueueAddSchema.parse({ task_name, priority });
const queue_id = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const queuedTask: QueuedTask = {
id: queue_id,
task: async () => {
// Execute the task (would need to be passed in separately)
return { success: true };
},
priority: validated.priority || 50,
added_at: new Date()
};
// Insert into queue maintaining priority order
let position = 0;
for (const existing of taskQueue) {
if (existing.priority > queuedTask.priority) {
break;
}
position++;
}
taskQueue.splice(position, 0, queuedTask);
return { queue_id: queue_id, position };
}
/**
* Execute a workflow of steps.
*/
export async function workflow_execute(steps: any[]): Promise<{ results: Array<{ step: number; success: boolean; result?: any; error?: string }>; errors: number }> {
const validated = workflowExecuteSchema.parse({ steps });
const results = [];
let errors = 0;
for (let i = 0; i < validated.steps.length; i++) {
try {
const step = validated.steps[i];
// Execute the step based on its type
let result: any;
if (step.type === 'command') {
result = await executeCommand(step.command);
} else if (step.type === 'file_operation') {
result = await performFileOperation(step.operation, step.path);
} else if (typeof step.execute === 'function') {
result = await step.execute();
} else {
throw new Error(`Unknown step type: ${step.type}`);
}
results.push({ step: i + 1, success: true, result });
} catch (error) {
errors++;
results.push({
step: i + 1,
success: false,
error: error instanceof Error ? error.message : String(error)
});
// Continue to next step even if one fails
}
}
return { results, errors };
}
/**
* Helper function to execute a command.
*/
async function executeCommand(command: string, cwd: string = process.cwd()): Promise<{ stdout: string; stderr: string }> {
const { exec } = await import('child_process');
return new Promise((resolve, reject) => {
exec(command, { cwd }, (error, stdout, stderr) => {
if (error) {
resolve({ stdout: stdout || '', stderr: stderr || error.message });
} else {
resolve({ stdout: stdout || '', stderr: stderr || '' });
}
});
});
}
/**
* Helper function to perform file operations.
*/
async function performFileOperation(operation: string, path_str: string, workspaceRoot: string = process.cwd()): Promise<any> {
const fs = await import('fs/promises');
const safePath = validatePath(workspaceRoot, workspaceRoot, path_str);
switch (operation) {
case 'read':
return await fs.readFile(safePath, 'utf-8');
case 'write':
// Would need content parameter - simplified for now
throw new Error('Write operation requires content parameter');
case 'delete':
await fs.unlink(safePath);
return { success: true };
default:
throw new Error(`Unknown file operation: ${operation}`);
}
}
/**
* Get next run time for a cron expression.
*/
function getNextRunTime(cron_expr: string): string {
// Simple implementation - would need proper cron parsing library
return 'N/A (would require cron-parser library)';
}
/**
* Persist cron jobs to file.
*/
async function persistCronJobs(workspaceRoot: string = process.cwd()): Promise<void> {
const configuredPath = process.env.CRON_STORAGE_PATH;
const storagePath = configuredPath
? validatePath(workspaceRoot, workspaceRoot, configuredPath)
: path.join(workspaceRoot, '.cron_jobs.json');
try {
const jobsData: Array<{ id: string; expr: string; command: string }> = [];
for (const [id, task] of global.__cron_jobs__.entries()) {
// Would need to extract cron expression from task - simplified here
jobsData.push({ id, expr: '', command: '' });
}
await fs.writeFile(storagePath, JSON.stringify(jobsData, null, 2));
} catch (error) {
console.error(`Failed to persist cron jobs: ${error instanceof Error ? error.message : String(error)}`);
}
}