/**
* Update Task Tool
*
* Updates an existing task using the CodeRide API
*/
import { z } from 'zod';
import { BaseTool, MCPToolDefinition, ToolAnnotations, AgentInstructions } from '../utils/base-tool.js';
import { SecureApiClient, UpdateTaskApiResponse } from '../utils/secure-api-client.js';
import { logger } from '../utils/logger.js';
// Removed local UpdateTaskResponse as UpdateTaskApiResponse from api-client.ts will be used.
/**
* Schema for the update-task tool input
*/
const UpdateTaskSchema = z.object({
// Required field to identify the task
number: z.string({
required_error: "Task number is required to identify the task",
invalid_type_error: "Task number must be a string"
})
.regex(/^[A-Za-z]{3}-\d+$/, { message: "Task number must be in the format ABC-123 (e.g., CRD-1 or crd-1). Case insensitive." })
.describe("Task number to identify the task to update (case insensitive)"),
// Optional fields that can be updated with security constraints
description: z.string()
.max(2000, "Description cannot exceed 2000 characters")
.optional()
.describe("New task description"),
status: z.enum(['to-do', 'in-progress', 'done'], {
invalid_type_error: "Status must be one of: to-do, in-progress, done"
}).optional().describe("New task status"),
}).strict().refine(
// Ensure at least one field to update is provided
(data) => {
const updateFields = ['description', 'status'];
return updateFields.some(field => field in data);
},
{
message: 'At least one field to update must be provided',
path: ['updateFields']
}
);
/**
* Example input schema for documentation
*/
const exampleInput = {
number: "CRD-1",
status: "in-progress",
description: "Updated task description"
};
/**
* Type for the update-task tool input
*/
type UpdateTaskInput = z.infer<typeof UpdateTaskSchema>;
/**
* Update Task Tool Implementation
*/
export class UpdateTaskTool extends BaseTool<typeof UpdateTaskSchema> {
readonly name = 'update_task';
readonly description = "Updates an existing task's 'description' and/or 'status'. The task is identified by its unique 'number' (e.g., 'CRD-1'). At least one of 'description' or 'status' must be provided for an update.";
readonly zodSchema = UpdateTaskSchema; // Renamed from schema
readonly annotations: ToolAnnotations = {
title: "Update Task",
readOnlyHint: false, // This tool modifies data
destructiveHint: false, // Updates are generally not destructive
idempotentHint: false, // Multiple identical updates might have different outcomes if not designed for idempotency
openWorldHint: true, // Interacts with an external API
};
/**
* Constructor with dependency injection
*/
constructor(apiClient?: SecureApiClient) {
super(apiClient);
}
/**
* Override to require project context for task updates
*/
requiresProjectContext(): boolean {
return true;
}
/**
* Generate agent-specific instructions for task update workflow
*/
generateAgentInstructions(input: any): AgentInstructions {
const isStatusUpdate = input.status !== undefined;
const newStatus = input.status;
const baseInstructions: AgentInstructions = {
immediateActions: [
'Task update completed successfully',
'Verify update was applied correctly'
],
nextRecommendedTools: [],
workflowPhase: 'implementation'
};
// Status-specific guidance
if (isStatusUpdate) {
switch (newStatus) {
case 'in-progress':
baseInstructions.immediateActions = [
'Task status updated to "in-progress"',
'Begin implementation following prompt guidance',
'Track progress and update as needed'
];
baseInstructions.nextRecommendedTools = ['get_prompt'];
baseInstructions.workflowPhase = 'implementation';
baseInstructions.criticalReminders = [
'Task is now active - ensure consistent progress updates',
'Follow prompt instructions precisely during implementation'
];
break;
case 'done':
baseInstructions.immediateActions = [
'Task marked as done',
'Update project knowledge with implementation impacts',
'Update project diagram if architecture changed'
];
baseInstructions.nextRecommendedTools = ['update_project', 'next_task'];
baseInstructions.workflowPhase = 'completion';
baseInstructions.projectUpdateRequired = true;
baseInstructions.criticalReminders = [
'CRITICAL: Update project knowledge and diagram after task completion',
'Document any architectural changes or new patterns',
'Find and start next task in sequence'
];
break;
case 'to-do':
baseInstructions.immediateActions = [
'Task reset to "to-do" status',
'Review task requirements before restarting',
'Ensure project context is current'
];
baseInstructions.nextRecommendedTools = ['get_task', 'get_prompt'];
baseInstructions.workflowPhase = 'analysis';
break;
}
}
// Add workflow correction guidance
baseInstructions.workflowCorrection = {
correctSequence: ['get_project', 'get_task', 'get_prompt', 'update_task'],
redirectMessage: 'Task updates should follow proper workflow sequence for optimal results'
};
return baseInstructions;
}
/**
* Returns the full tool definition conforming to MCP.
*/
getMCPToolDefinition(): MCPToolDefinition {
return {
name: this.name,
description: this.description,
annotations: this.annotations,
inputSchema: {
type: "object",
properties: {
number: {
type: "string",
pattern: "^[A-Za-z]{3}-\\d+$",
description: "The unique identifier for the task to be updated (e.g., 'CRD-1' or 'crd-1'). Case insensitive - will be converted to uppercase."
},
description: {
type: "string",
description: "Optional. The new description for the task. If provided, it will replace the existing task description. (max 2000 characters)"
},
status: {
type: "string",
enum: ["to-do", "in-progress", "done"],
description: "Optional. The new status for the task. Must be one of: 'to-do', 'in-progress', 'done'. If provided, it will update the task's current status."
}
},
required: ["number"], // Zod .refine() handles the "at least one update field" logic at runtime.
additionalProperties: false
}
};
}
/**
* Execute the update-task tool
*/
async execute(input: UpdateTaskInput): Promise<unknown> {
logger.info('Executing update-task tool', input);
try {
// Use the injected API client to update task
if (!this.apiClient) {
throw new Error('API client not available - tool not properly initialized');
}
// Extract task number
const { number, ...updateData } = input;
// Convert task number to uppercase for consistency
const taskNumber = number.toUpperCase();
// Update task using the API endpoint
const url = `/task/number/${taskNumber}`;
logger.debug(`Making PUT request to: ${url}`);
const responseData = await this.apiClient.put<UpdateTaskApiResponse>(url, updateData) as unknown as UpdateTaskApiResponse;
if (!responseData.success) {
const apiErrorMessage = responseData.message || 'API reported update failure without a specific message.';
logger.warn(`Update task API call for ${taskNumber} returned success:false. Message: ${apiErrorMessage}`);
return {
isError: true,
content: [{ type: "text", text: `Update for task ${taskNumber} failed: ${apiErrorMessage}` }]
};
}
// At this point, responseData.success is true
const updatedFieldsList = Object.keys(updateData).join(', ') || 'no specific fields (refresh)'; // Handle case where updateData is empty if API allows
const apiMessage = responseData.message || 'Task successfully updated.';
if (responseData.task) {
return {
number: responseData.task.number,
title: responseData.task.title,
description: responseData.task.description,
status: responseData.task.status,
updateConfirmation: `Task ${responseData.task.number} updated fields: ${updatedFieldsList}. API: ${apiMessage}`
};
} else {
// responseData.success is true, but responseData.task is missing.
logger.warn(`Update task API call for ${taskNumber} succeeded but returned no task data. API message: ${apiMessage}`);
return {
number: taskNumber, // Use input taskNumber as fallback
title: '', // No title info available from response
description: input.description || '', // Fallback to input description if available
status: input.status || '', // Fallback to input status if available
updateConfirmation: `Task ${taskNumber} update reported success by API, but full task details were not returned. Attempted to update fields: ${updatedFieldsList}. API: ${apiMessage}`
};
}
} catch (error) {
let errorMessage = (error instanceof Error) ? error.message : 'An unknown error occurred';
logger.error(`Error in update-task tool: ${errorMessage}`, error instanceof Error ? error : undefined);
// Check if it's a not found error based on API response structure or message
// Note: ApiError from apiClient already provides a safeErrorMessage
if (error instanceof Error && (error as any).status === 404) {
errorMessage = `Task with number '${input.number}' not found.`;
} else if (error instanceof Error && error.message.includes('not found')) { // Fallback for other not found indications
errorMessage = `Task with number '${input.number}' not found or update failed.`;
}
return {
isError: true,
content: [{ type: "text", text: errorMessage }]
};
}
}
}