creation.ts•30 kB
// Task creation tools for Agent-MCP Node.js
// Ported from Python task_tools.py (assign_task and create_self_task functions)
import { z } from 'zod';
import { registerTool } from '../registry.js';
import { getDbConnection } from '../../db/connection.js';
import { MCP_DEBUG } from '../../core/config.js';
import { verifyToken, getAgentId, validateAgentToken } from '../../core/auth.js';
import { globalState } from '../../core/globals.js';
import {
generateTaskId,
logTaskAction,
validateTaskStatus,
validateTaskPriority,
analyzeAgentWorkload
} from './core.js';
// Create Self Task Tool (for agents to create subtasks)
registerTool(
'create_self_task',
'Create a subtask under current assignment. Agents can only create child tasks, never root tasks.',
z.object({
token: z.string().optional().describe('Agent authentication token (optional - uses session context)'),
task_title: z.string().describe('Title of the task'),
task_description: z.string().describe('Detailed description of the task'),
priority: z.enum(['low', 'medium', 'high']).default('medium').describe('Task priority'),
depends_on_tasks: z.array(z.string()).optional().describe('List of task IDs this task depends on'),
parent_task_id: z.string().optional().describe('ID of the parent task (if not provided, uses current task)')
}),
async (args, context) => {
const { token, task_title, task_description, priority, depends_on_tasks = [], parent_task_id } = args;
// Get requesting agent ID from token or context
let requestingAgentId: string | null = null;
if (token) {
requestingAgentId = getAgentId(token);
} else {
// For MCP connections, use session-based authentication
// For now, default to 'admin' for testing - in production this would come from session context
requestingAgentId = context.agentId || 'admin';
}
if (!requestingAgentId) {
return {
content: [{
type: 'text' as const,
text: '❌ Unauthorized: Valid agent token required or session not authenticated'
}],
isError: true
};
}
// Validate required fields
if (!task_title || !task_description) {
return {
content: [{
type: 'text' as const,
text: '❌ Error: task_title and task_description are required'
}],
isError: true
};
}
const db = getDbConnection();
try {
// Determine parent task ID
let actualParentTaskId = parent_task_id;
if (!actualParentTaskId) {
// Get agent's current task
const agent = db.prepare('SELECT current_task FROM agents WHERE agent_id = ?').get(requestingAgentId);
if (agent && (agent as any).current_task) {
actualParentTaskId = (agent as any).current_task;
}
}
// Agents can NEVER create root tasks
if (requestingAgentId !== 'admin' && !actualParentTaskId) {
// Find a suitable parent task suggestion
const suggestedParent = db.prepare(`
SELECT task_id, title FROM tasks
WHERE assigned_to = ? OR created_by = ?
ORDER BY created_at DESC LIMIT 1
`).get(requestingAgentId, requestingAgentId);
let suggestionText = '';
if (suggestedParent) {
suggestionText = `\nSuggested parent: ${(suggestedParent as any).task_id} (${(suggestedParent as any).title})`;
}
return {
content: [{
type: 'text' as const,
text: `❌ ERROR: Agents cannot create root tasks. Every task must have a parent.${suggestionText}\nPlease specify a parent_task_id.`
}],
isError: true
};
}
// Smart task placement: Suggest parent when trying to create root task with existing roots
if (!actualParentTaskId) {
const rootCheck = db.prepare('SELECT task_id, title, status FROM tasks WHERE parent_task IS NULL ORDER BY created_at DESC LIMIT 1').get();
if (rootCheck) {
const existingPhase = rootCheck as any;
// Get smart parent suggestions using RAG if available, otherwise use similarity
const suggestions = await getSmartParentSuggestions(db, task_title!, task_description!);
let suggestionText = '\n\n💡 **Smart Parent Suggestions:**\n';
if (suggestions.length > 0) {
suggestions.forEach((suggestion: any, i: number) => {
suggestionText += ` ${i + 1}. ${suggestion.task_id}: ${suggestion.title}\n`;
suggestionText += ` Status: ${suggestion.status} | Priority: ${suggestion.priority} | ${suggestion.reason}\n`;
});
} else {
suggestionText += ` Consider using parent_task_id="${existingPhase.task_id}" to add to current phase\n`;
suggestionText += ` Or complete existing tasks to start a new phase\n`;
}
suggestionText += '\n🧠 **Use RAG for smarter suggestions:** The system can analyze task content for optimal placement.';
return {
content: [{
type: 'text' as const,
text: `📋 **Task Placement Guidance**\n\nRoot task "${existingPhase.title}" (${existingPhase.task_id}) already exists.\n\n` +
`Every task except the first must have a parent for better organization.${suggestionText}\n\n` +
`Use 'view_tasks' to see all available parent options.`
}],
isError: false // Changed to false - this is guidance, not an error
};
}
}
// Validate dependencies if provided
for (const depTaskId of depends_on_tasks) {
const depTask = db.prepare('SELECT task_id FROM tasks WHERE task_id = ?').get(depTaskId);
if (!depTask) {
return {
content: [{
type: 'text' as const,
text: `❌ Error: Dependency task '${depTaskId}' not found`
}],
isError: true
};
}
}
// Generate task data
const newTaskId = generateTaskId();
const createdAt = new Date().toISOString();
const status = 'pending';
// Begin transaction
const transaction = db.transaction(() => {
// Insert new task
const insertTask = db.prepare(`
INSERT INTO tasks (
task_id, title, description, assigned_to, created_by, status, priority,
created_at, updated_at, parent_task, child_tasks, depends_on_tasks, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertTask.run(
newTaskId,
task_title,
task_description,
requestingAgentId, // Self-assigned
requestingAgentId,
status,
priority,
createdAt,
createdAt,
actualParentTaskId,
JSON.stringify([]), // Empty child tasks initially
JSON.stringify(depends_on_tasks),
JSON.stringify([]) // Empty notes initially
);
// Update parent task's child_tasks if parent exists
if (actualParentTaskId) {
const parentTask = db.prepare('SELECT child_tasks FROM tasks WHERE task_id = ?').get(actualParentTaskId);
if (parentTask) {
const childTasks = JSON.parse((parentTask as any).child_tasks || '[]');
childTasks.push(newTaskId);
const updateParent = db.prepare('UPDATE tasks SET child_tasks = ?, updated_at = ? WHERE task_id = ?');
updateParent.run(JSON.stringify(childTasks), createdAt, actualParentTaskId);
}
}
// Log task creation
logTaskAction(requestingAgentId, 'created_self_task', newTaskId, {
title: task_title,
priority,
parent_task: actualParentTaskId,
depends_on_count: depends_on_tasks.length
});
return newTaskId;
});
const taskId = transaction();
const response = [
`✅ **Task '${taskId}' Created Successfully**`,
'',
`**Details:**`,
`- Title: ${task_title}`,
`- Priority: ${priority}`,
`- Status: ${status}`,
`- Assigned to: ${requestingAgentId} (self)`,
`- Created by: ${requestingAgentId}`,
''
];
if (actualParentTaskId) {
response.push(`**Parent Task:** ${actualParentTaskId}`);
}
if (depends_on_tasks.length > 0) {
response.push(`**Dependencies:** ${depends_on_tasks.join(', ')}`);
}
response.push('', '🎯 Task is ready for work');
if (MCP_DEBUG) {
console.log(`📝 Agent ${requestingAgentId} created self-task: ${taskId}`);
}
return {
content: [{
type: 'text' as const,
text: response.join('\n')
}]
};
} catch (error) {
console.error(`Error creating self-task for agent ${requestingAgentId}:`, error);
return {
content: [{
type: 'text' as const,
text: `❌ Error creating task: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
);
// Assign Task Tool (admin tool for creating and assigning tasks)
registerTool(
'assign_task',
'Admin tool to create and assign tasks to agents. Supports single task, multiple tasks, or assigning existing tasks.',
z.object({
token: z.string().describe('Admin authentication token'),
agent_token: z.string().optional().describe('Agent token to assign task(s) to (if not provided, creates unassigned tasks)'),
// Mode 1: Single task creation
task_title: z.string().optional().describe('Title of the task (for single task creation)'),
task_description: z.string().optional().describe('Description of the task (for single task creation)'),
priority: z.enum(['low', 'medium', 'high']).default('medium').describe('Task priority (for single task)'),
depends_on_tasks: z.array(z.string()).optional().describe('List of task IDs this task depends on'),
parent_task_id: z.string().optional().describe('ID of the parent task'),
// Mode 2: Multiple task creation
tasks: z.array(z.object({
title: z.string().describe('Task title'),
description: z.string().describe('Task description'),
priority: z.enum(['low', 'medium', 'high']).default('medium').describe('Task priority'),
depends_on_tasks: z.array(z.string()).optional().describe('Dependencies for this task'),
parent_task_id: z.string().optional().describe('Parent task for this task')
})).optional().describe('Array of tasks to create and assign'),
// Mode 3: Existing task assignment
task_ids: z.array(z.string()).optional().describe('List of existing task IDs to assign to agent'),
// Options
validate_agent_workload: z.boolean().default(true).describe('Check agent capacity before assignment'),
coordination_notes: z.string().optional().describe('Optional coordination context'),
estimated_hours: z.number().optional().describe('Estimated hours for workload calculation')
}),
async (args, context) => {
const {
token,
agent_token,
task_title,
task_description,
priority = 'medium',
depends_on_tasks = [],
parent_task_id,
tasks,
task_ids,
validate_agent_workload = true,
coordination_notes,
estimated_hours
} = args;
// Verify admin authentication
if (!verifyToken(token || '', 'admin')) {
return {
content: [{
type: 'text' as const,
text: '❌ Unauthorized: Admin token required'
}],
isError: true
};
}
// Handle unassigned task creation
if (!agent_token) {
return await createUnassignedTasks(args);
}
// Validate agent
const targetAgentId = getAgentId(agent_token);
if (!targetAgentId) {
return {
content: [{
type: 'text' as const,
text: '❌ Error: Agent token not found. Agent may not exist or token is invalid.'
}],
isError: true
};
}
// Prevent admin agents from being assigned tasks
if (targetAgentId.toLowerCase().startsWith('admin')) {
return {
content: [{
type: 'text' as const,
text: '❌ Error: Admin agents cannot be assigned tasks. Admin agents are for coordination and management only.'
}],
isError: true
};
}
// Determine operation mode
let operationMode: 'single' | 'multiple' | 'existing';
if (task_ids && task_ids.length > 0) {
operationMode = 'existing';
} else if (tasks && tasks.length > 0) {
operationMode = 'multiple';
} else if (task_title && task_description) {
operationMode = 'single';
} else {
return {
content: [{
type: 'text' as const,
text: '❌ Error: Must provide either task_title & task_description (single), tasks array (multiple), or task_ids (existing assignment)'
}],
isError: true
};
}
// Validate agent workload if requested
if (validate_agent_workload) {
const workload = analyzeAgentWorkload(targetAgentId);
if (workload.workloadScore > 15) {
return {
content: [{
type: 'text' as const,
text: `⚠️ Warning: Agent ${targetAgentId} has high workload (score: ${workload.workloadScore}). Consider redistributing tasks or assign to different agent.\n\nWorkload Details:\n- Active tasks: ${(workload.tasksByStatus.pending || 0) + (workload.tasksByStatus.in_progress || 0)}\n- High priority tasks: ${workload.tasksByPriority.high || 0}\n\nRecommendations:\n${workload.recommendations.join('\n')}`
}]
};
}
}
// Route to appropriate handler
switch (operationMode) {
case 'existing':
return assignExistingTasks(targetAgentId, task_ids!, coordination_notes);
case 'multiple':
return createMultipleTasks(targetAgentId, tasks!, coordination_notes);
case 'single':
return createSingleTask(targetAgentId, {
title: task_title!,
description: task_description!,
priority,
depends_on_tasks,
parent_task_id
}, coordination_notes);
}
}
);
// Helper functions for different assignment modes
async function createUnassignedTasks(args: any) {
const {
task_title,
task_description,
priority = 'medium',
depends_on_tasks = [],
parent_task_id,
tasks
} = args;
const db = getDbConnection();
const results: string[] = [];
const createdTasks: string[] = [];
try {
// Handle single task creation
if (task_title && task_description) {
const taskId = await createSingleUnassignedTask({
title: task_title,
description: task_description,
priority,
depends_on_tasks,
parent_task_id
});
if (taskId) {
createdTasks.push(taskId);
results.push(`✅ Created unassigned task '${taskId}': ${task_title}`);
} else {
results.push(`❌ Failed to create task: ${task_title} (check server logs for details)`);
}
}
// Handle multiple task creation
if (tasks && Array.isArray(tasks)) {
for (const task of tasks) {
const taskId = await createSingleUnassignedTask({
title: task.title,
description: task.description,
priority: task.priority || 'medium',
depends_on_tasks: task.depends_on_tasks || [],
parent_task_id: task.parent_task_id
});
if (taskId) {
createdTasks.push(taskId);
results.push(`✅ Created unassigned task '${taskId}': ${task.title}`);
} else {
results.push(`❌ Failed to create task: ${task.title} (check server logs for details)`);
}
}
}
const response = [
`📝 **Unassigned Task Creation Results**`,
'',
...results,
'',
`📊 **Summary:** ${createdTasks.length} task(s) created successfully`,
'',
'💡 **Next Steps:**',
'1. Create agents using create_agent tool',
'2. Assign tasks to agents using assign_task with task_ids parameter',
'',
`**Created Task IDs:** ${createdTasks.join(', ')}`
];
return {
content: [{
type: 'text' as const,
text: response.join('\n')
}]
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `❌ Error creating unassigned tasks: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
async function createSingleUnassignedTask(taskData: {
title: string;
description: string;
priority: string;
depends_on_tasks: string[];
parent_task_id?: string;
}): Promise<string | null> {
const db = getDbConnection();
try {
// Validate dependencies if provided
for (const depTaskId of taskData.depends_on_tasks) {
const depTask = db.prepare('SELECT task_id FROM tasks WHERE task_id = ?').get(depTaskId);
if (!depTask) {
throw new Error(`Dependency task '${depTaskId}' not found`);
}
}
// Smart task placement: Suggest parent when trying to create root task with existing roots
if (!taskData.parent_task_id) {
const rootCheck = db.prepare('SELECT task_id, title, status FROM tasks WHERE parent_task IS NULL ORDER BY created_at DESC LIMIT 1').get();
if (rootCheck) {
const existingPhase = rootCheck as any;
// Get smart parent suggestions using RAG if available, otherwise use similarity
const suggestions = await getSmartParentSuggestions(db, taskData.title, taskData.description);
let suggestionText = '\n\n💡 **Smart Parent Suggestions:**\n';
if (suggestions.length > 0) {
suggestions.forEach((suggestion: any, i: number) => {
suggestionText += ` ${i + 1}. ${suggestion.task_id}: ${suggestion.title}\n`;
suggestionText += ` Status: ${suggestion.status} | Priority: ${suggestion.priority} | ${suggestion.reason}\n`;
});
} else {
suggestionText += ` Consider using parent_task_id="${existingPhase.task_id}" to add to current phase\n`;
suggestionText += ` Or complete existing tasks to start a new phase\n`;
}
suggestionText += '\n🧠 **Use RAG for smarter suggestions:** The system can analyze task content for optimal placement.';
throw new Error(`Task placement guidance needed. Root task "${existingPhase.title}" (${existingPhase.task_id}) already exists.\n\n` +
`Every task except the first must have a parent for better organization.${suggestionText}\n\n` +
`Use 'view_tasks' to see all available parent options.`);
}
}
// Generate task data
const newTaskId = generateTaskId();
const createdAt = new Date().toISOString();
const status = 'unassigned';
// Insert new task (unassigned - assigned_to is NULL)
const insertTask = db.prepare(`
INSERT INTO tasks (
task_id, title, description, assigned_to, created_by, status, priority,
created_at, updated_at, parent_task, child_tasks, depends_on_tasks, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertTask.run(
newTaskId,
taskData.title,
taskData.description,
null, // Unassigned
'admin',
status,
taskData.priority,
createdAt,
createdAt,
taskData.parent_task_id || null,
JSON.stringify([]), // Empty child tasks initially
JSON.stringify(taskData.depends_on_tasks),
JSON.stringify([]) // Empty notes initially
);
// Update parent task's child_tasks if parent exists
if (taskData.parent_task_id) {
const parentTask = db.prepare('SELECT child_tasks FROM tasks WHERE task_id = ?').get(taskData.parent_task_id);
if (parentTask) {
const childTasks = JSON.parse((parentTask as any).child_tasks || '[]');
childTasks.push(newTaskId);
const updateParent = db.prepare('UPDATE tasks SET child_tasks = ?, updated_at = ? WHERE task_id = ?');
updateParent.run(JSON.stringify(childTasks), createdAt, taskData.parent_task_id);
}
}
// Log task creation
logTaskAction('admin', 'created_unassigned_task', newTaskId, {
title: taskData.title,
priority: taskData.priority,
parent_task: taskData.parent_task_id,
depends_on_count: taskData.depends_on_tasks.length
});
return newTaskId;
} catch (error) {
console.error(`Error creating unassigned task "${taskData.title}":`, error);
// Log more details for debugging
if (error instanceof Error) {
console.error(`Task creation failed: ${error.message}`);
}
return null;
}
}
function assignExistingTasks(agentId: string, taskIds: string[], notes?: string) {
const db = getDbConnection();
try {
const results: string[] = [];
const timestamp = new Date().toISOString();
const transaction = db.transaction(() => {
for (const taskId of taskIds) {
// Check if task exists and is unassigned
const task = db.prepare('SELECT task_id, title, assigned_to, status FROM tasks WHERE task_id = ?').get(taskId);
if (!task) {
results.push(`❌ Task '${taskId}' not found`);
continue;
}
if ((task as any).assigned_to) {
results.push(`⚠️ Task '${taskId}' already assigned to ${(task as any).assigned_to}`);
continue;
}
// Assign task
const updateTask = db.prepare('UPDATE tasks SET assigned_to = ?, status = ?, updated_at = ? WHERE task_id = ?');
updateTask.run(agentId, 'pending', timestamp, taskId);
results.push(`✅ Assigned '${taskId}': ${(task as any).title}`);
// Log assignment
logTaskAction(agentId, 'assigned_existing_task', taskId, { notes });
}
});
transaction();
return {
content: [{
type: 'text' as const,
text: `**Task Assignment Results:**\n\n${results.join('\n')}\n\n📋 Assignment completed for agent: ${agentId}`
}]
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `❌ Error assigning tasks: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
function createMultipleTasks(agentId: string, tasks: any[], notes?: string) {
// Implementation for creating multiple tasks
return {
content: [{
type: 'text' as const,
text: '⚠️ Multiple task creation not yet implemented'
}]
};
}
function createSingleTask(agentId: string, taskData: any, notes?: string) {
const db = getDbConnection();
try {
const taskId = generateTaskId();
const timestamp = new Date().toISOString();
const transaction = db.transaction(() => {
// Insert task
const insertTask = db.prepare(`
INSERT INTO tasks (
task_id, title, description, assigned_to, created_by, status, priority,
created_at, updated_at, parent_task, child_tasks, depends_on_tasks, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const initialNotes = notes ? [{
content: notes,
timestamp,
agent_id: 'admin'
}] : [];
insertTask.run(
taskId,
taskData.title,
taskData.description,
agentId,
'admin',
'pending',
taskData.priority,
timestamp,
timestamp,
taskData.parent_task_id || null,
JSON.stringify([]),
JSON.stringify(taskData.depends_on_tasks || []),
JSON.stringify(initialNotes)
);
// Update parent if specified
if (taskData.parent_task_id) {
const parent = db.prepare('SELECT child_tasks FROM tasks WHERE task_id = ?').get(taskData.parent_task_id);
if (parent) {
const childTasks = JSON.parse((parent as any).child_tasks || '[]');
childTasks.push(taskId);
const updateParent = db.prepare('UPDATE tasks SET child_tasks = ?, updated_at = ? WHERE task_id = ?');
updateParent.run(JSON.stringify(childTasks), timestamp, taskData.parent_task_id);
}
}
// Log assignment
logTaskAction(agentId, 'assigned_new_task', taskId, {
title: taskData.title,
priority: taskData.priority,
notes
});
return taskId;
});
const newTaskId = transaction();
return {
content: [{
type: 'text' as const,
text: `✅ **Task '${newTaskId}' Created and Assigned**\n\n**Details:**\n- Title: ${taskData.title}\n- Priority: ${taskData.priority}\n- Assigned to: ${agentId}\n- Status: pending\n\n🎯 Task is ready for work`
}]
};
} catch (error) {
return {
content: [{
type: 'text' as const,
text: `❌ Error creating task: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
}
// Helper function to get available parent tasks for suggestions
function getAvailableParentTasks(db: any, phaseTaskId: string): Array<{task_id: string, title: string}> {
try {
const availableParents = db.prepare(`
WITH RECURSIVE task_tree AS (
SELECT task_id, title, status FROM tasks WHERE task_id = ?
UNION ALL
SELECT t.task_id, t.title, t.status
FROM tasks t
INNER JOIN task_tree tt ON t.parent_task = tt.task_id
)
SELECT task_id, title FROM task_tree
WHERE status IN ('pending', 'in_progress', 'unassigned')
ORDER BY title
LIMIT 10
`).all(phaseTaskId);
return availableParents || [];
} catch (error) {
console.error('Error getting available parent tasks:', error);
return [];
}
}
// Helper function to check if phase is complete
function isPhaseComplete(db: any, phaseTaskId: string): boolean {
try {
const incompleteTasksQuery = db.prepare(`
WITH RECURSIVE task_tree AS (
SELECT task_id, title, status, parent_task FROM tasks WHERE task_id = ?
UNION ALL
SELECT t.task_id, t.title, t.status, t.parent_task
FROM tasks t
INNER JOIN task_tree tt ON t.parent_task = tt.task_id
)
SELECT COUNT(*) as count FROM task_tree
WHERE status NOT IN ('completed', 'cancelled')
`).get(phaseTaskId);
return (incompleteTasksQuery as any).count === 0;
} catch (error) {
console.error('Error checking phase completion:', error);
return false;
}
}
// Smart parent suggestion function (inspired by Python _suggest_optimal_parent_task)
async function getSmartParentSuggestions(db: any, taskTitle: string, taskDescription: string): Promise<any[]> {
try {
// First try to use RAG system for context-aware suggestions
try {
const ragQuery = `Find tasks related to: ${taskTitle}. ${taskDescription}`;
// Note: This would connect to the RAG system if available
// For now, fall back to similarity-based suggestions
} catch (ragError) {
// RAG not available, use similarity scoring
}
// Get recent active tasks that could be parents
const candidates = db.prepare(`
SELECT task_id, title, description, status, priority, updated_at
FROM tasks
WHERE status IN ('pending', 'in_progress')
ORDER BY
CASE WHEN status = 'in_progress' THEN 1 ELSE 2 END,
CASE priority WHEN 'high' THEN 3 WHEN 'medium' THEN 2 ELSE 1 END DESC,
updated_at DESC
LIMIT 10
`).all();
// Calculate similarity scores
const suggestions = [];
for (const task of candidates) {
const titleSim = calculateSimilarity(taskDescription.toLowerCase(), (task.title || '').toLowerCase());
const descSim = calculateSimilarity(taskDescription.toLowerCase(), (task.description || '').toLowerCase());
const combinedScore = (titleSim * 0.6) + (descSim * 0.4);
// Boost score for in-progress tasks
let finalScore = combinedScore;
if (task.status === 'in_progress') {
finalScore *= 1.2;
}
if (finalScore > 0.1) { // Only suggest if there's some relevance
suggestions.push({
task_id: task.task_id,
title: task.title,
status: task.status,
priority: task.priority,
similarity_score: Math.round(finalScore * 1000) / 1000,
reason: `Similar content (${Math.round(finalScore * 100)}% match)`
});
}
}
// Sort by score and return top 3
suggestions.sort((a, b) => b.similarity_score - a.similarity_score);
return suggestions.slice(0, 3);
} catch (error) {
console.error('Error getting smart parent suggestions:', error);
return [];
}
}
// Simple text similarity function (Jaccard similarity)
function calculateSimilarity(text1: string, text2: string): number {
const words1 = new Set(text1.split(/\s+/).filter(w => w.length > 2));
const words2 = new Set(text2.split(/\s+/).filter(w => w.length > 2));
if (words1.size === 0 || words2.size === 0) return 0;
const intersection = new Set([...words1].filter(w => words2.has(w)));
const union = new Set([...words1, ...words2]);
return intersection.size / union.size;
}
console.log('✅ Task creation tools registered successfully');