UNPKG

@grec0/mcp-s2s-asterisk

Version:

MCP server para asistente telefónico conversacional con Asterisk S2S

427 lines (426 loc) 15.8 kB
/** * @fileoverview Card operations for the MCP Kanban server * * This module provides functions for interacting with cards in the Planka Kanban board, * including creating, retrieving, updating, moving, duplicating, and deleting cards, * as well as managing card stopwatches for time tracking. */ import { z } from "zod"; import { plankaRequest } from "../common/utils.js"; import { PlankaCardSchema } from "../common/types.js"; // Schema definitions /** * Schema for creating a new card * @property {string} listId - The ID of the list to create the card in * @property {string} name - The name of the card * @property {string} [description] - The description of the card * @property {number} [position] - The position of the card in the list (default: 65535) */ export const CreateCardSchema = z.object({ listId: z.string().describe("List ID"), name: z.string().describe("Card name"), description: z.string().optional().describe("Card description"), position: z.number().optional().describe("Card position (default: 65535)"), }); /** * Schema for retrieving cards from a list * @property {string} listId - The ID of the list to get cards from */ export const GetCardsSchema = z.object({ listId: z.string().describe("List ID"), }); /** * Schema for retrieving a specific card * @property {string} id - The ID of the card to retrieve */ export const GetCardSchema = z.object({ id: z.string().describe("Card ID"), }); /** * Schema for updating a card * @property {string} id - The ID of the card to update * @property {string} [name] - The new name for the card * @property {string} [description] - The new description for the card * @property {number} [position] - The new position for the card * @property {string} [dueDate] - The due date for the card (ISO format) * @property {boolean} [isCompleted] - Whether the card is completed */ export const UpdateCardSchema = z.object({ id: z.string().describe("Card ID"), name: z.string().optional().describe("Card name"), description: z.string().optional().describe("Card description"), position: z.number().optional().describe("Card position"), dueDate: z.string().optional().describe("Card due date (ISO format)"), isCompleted: z.boolean().optional().describe("Whether the card is completed"), }); export const MoveCardSchema = z.object({ id: z.string().describe("Card ID"), listId: z.string().describe("Target list ID"), position: z.number().optional().describe("Card position in the target list (default: 65535)"), }); export const DuplicateCardSchema = z.object({ id: z.string().describe("Card ID to duplicate"), position: z.number().optional().describe("Position for the duplicated card (default: 65535)"), }); export const DeleteCardSchema = z.object({ id: z.string().describe("Card ID"), }); // Stopwatch schemas export const StartCardStopwatchSchema = z.object({ id: z.string().describe("Card ID"), }); export const StopCardStopwatchSchema = z.object({ id: z.string().describe("Card ID"), }); export const GetCardStopwatchSchema = z.object({ id: z.string().describe("Card ID"), }); export const ResetCardStopwatchSchema = z.object({ id: z.string().describe("Card ID"), }); // Response schemas const CardsResponseSchema = z.object({ items: z.array(PlankaCardSchema), included: z.record(z.any()).optional(), }); const CardResponseSchema = z.object({ item: PlankaCardSchema, included: z.record(z.any()).optional(), }); // Function implementations /** * Creates a new card in a list * * @param {CreateCardOptions} options - Options for creating the card * @param {string} options.listId - The ID of the list to create the card in * @param {string} options.name - The name of the card * @param {string} [options.description] - The description of the card * @param {number} [options.position] - The position of the card in the list (default: 65535) * @returns {Promise<object>} The created card * @throws {Error} If the card creation fails */ export async function createCard(options) { try { const response = await plankaRequest(`/api/lists/${options.listId}/cards`, { method: "POST", body: { name: options.name, description: options.description, position: options.position, }, }); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } catch (error) { throw new Error(`Failed to create card: ${error instanceof Error ? error.message : String(error)}`); } } /** * Retrieves all cards for a specific list * * @param {string} listId - The ID of the list to get cards from * @returns {Promise<Array<object>>} Array of cards in the list */ export async function getCards(listId) { try { // Get all projects which includes boards const projectsResponse = await plankaRequest(`/api/projects`); if (!projectsResponse || typeof projectsResponse !== "object" || !("included" in projectsResponse) || !projectsResponse.included || typeof projectsResponse.included !== "object") { return []; } const included = projectsResponse.included; // Get all boards if (!("boards" in included) || !Array.isArray(included.boards)) { return []; } const boards = included.boards; // Check each board for cards with the matching list ID for (const board of boards) { if (typeof board !== "object" || board === null || !("id" in board)) { continue; } const boardId = board.id; // Get the board details which includes cards const boardResponse = await plankaRequest(`/api/boards/${boardId}`); if (!boardResponse || typeof boardResponse !== "object" || !("included" in boardResponse) || !boardResponse.included || typeof boardResponse.included !== "object") { continue; } const boardIncluded = boardResponse.included; if (!("cards" in boardIncluded) || !Array.isArray(boardIncluded.cards)) { continue; } const cards = boardIncluded.cards; // Filter cards by list ID const matchingCards = cards.filter((card) => typeof card === "object" && card !== null && "listId" in card && card.listId === listId); if (matchingCards.length > 0) { return matchingCards; } } // If we couldn't find any cards for this list ID return []; } catch (error) { // If all else fails, return an empty array return []; } } /** * Retrieves a specific card by ID * * @param {string} id - The ID of the card to retrieve * @returns {Promise<object>} The requested card */ export async function getCard(id) { const response = await plankaRequest(`/api/cards/${id}`); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } /** * Updates a card's properties * * @param {string} id - The ID of the card to update * @param {Partial<Omit<CreateCardOptions, "listId">>} options - The properties to update * @returns {Promise<object>} The updated card */ export async function updateCard(id, options) { const response = await plankaRequest(`/api/cards/${id}`, { method: "PATCH", body: options, }); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } /** * Moves a card to a different list or position * * @param {string} cardId - The ID of the card to move * @param {string} listId - The ID of the list to move the card to * @param {number} [position=65535] - The position in the target list * @param {string} [boardId] - The ID of the board (if moving between boards) * @param {string} [projectId] - The ID of the project (if moving between projects) * @returns {Promise<object>} The moved card */ export async function moveCard(cardId, listId, position = 65535, boardId, projectId) { try { // Use the PATCH endpoint to update the card with the new list ID and position const response = await plankaRequest(`/api/cards/${cardId}`, { method: "PATCH", body: { listId, position, boardId, projectId, }, }); // Parse and return the updated card const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } catch (error) { throw new Error(`Failed to move card: ${error instanceof Error ? error.message : String(error)}`); } } /** * Duplicates a card in the same list * * @param {string} id - The ID of the card to duplicate * @param {number} [position] - The position for the duplicated card * @returns {Promise<object>} The duplicated card */ export async function duplicateCard(id, position) { try { // First, get the original card to access its name const originalCard = await getCard(id); // Create a new card with "Copy of" prefix const cardName = originalCard ? `Copy of ${originalCard.name}` : ""; // Get the list ID from the original card const listId = originalCard ? originalCard.listId : ""; if (!listId) { throw new Error("Could not determine list ID for card duplication"); } // Create a new card with the same properties but with "Copy of" prefix const newCard = await createCard({ listId, name: cardName, description: originalCard.description || "", position: position || 65535, }); return newCard; } catch (error) { throw new Error(`Failed to duplicate card: ${error instanceof Error ? error.message : String(error)}`); } } /** * Deletes a card by ID * * @param {string} id - The ID of the card to delete * @returns {Promise<{success: boolean}>} Success indicator */ export async function deleteCard(id) { await plankaRequest(`/api/cards/${id}`, { method: "DELETE", }); return { success: true }; } // Stopwatch functions /** * Starts the stopwatch for a card to track time spent * * @param {string} id - The ID of the card to start the stopwatch for * @returns {Promise<object>} The updated card with stopwatch information */ export async function startCardStopwatch(id) { try { // Get the current card to check if a stopwatch is already running const card = await getCard(id); // Calculate the stopwatch object let stopwatch = { startedAt: new Date().toISOString(), total: 0, }; // If there's an existing stopwatch, preserve the total time if (card.stopwatch && card.stopwatch.total) { stopwatch.total = card.stopwatch.total; } // Update the card with the new stopwatch const response = await plankaRequest(`/api/cards/${id}`, { method: "PATCH", body: { stopwatch }, }); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } catch (error) { throw new Error(`Failed to start card stopwatch: ${error instanceof Error ? error.message : String(error)}`); } } /** * Stops the stopwatch for a card * * @param {string} id - The ID of the card to stop the stopwatch for * @returns {Promise<object>} The updated card with stopwatch information */ export async function stopCardStopwatch(id) { try { // Get the current card to calculate elapsed time const card = await getCard(id); // If there's no stopwatch or it's not running, return the card as is if (!card.stopwatch || !card.stopwatch.startedAt) { return card; } // Calculate elapsed time const startedAt = new Date(card.stopwatch.startedAt); const now = new Date(); const elapsedSeconds = Math.floor((now.getTime() - startedAt.getTime()) / 1000); // Calculate the new total time const totalSeconds = (card.stopwatch.total || 0) + elapsedSeconds; // Update the card with the stopped stopwatch (null startedAt but preserved total) const stopwatch = { startedAt: null, total: totalSeconds, }; const response = await plankaRequest(`/api/cards/${id}`, { method: "PATCH", body: { stopwatch }, }); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } catch (error) { throw new Error(`Failed to stop card stopwatch: ${error instanceof Error ? error.message : String(error)}`); } } /** * Gets the current stopwatch time for a card * * @param {string} id - The ID of the card to get the stopwatch time for * @returns {Promise<object>} The card's stopwatch information */ export async function getCardStopwatch(id) { try { const card = await getCard(id); // If there's no stopwatch, return default values if (!card.stopwatch) { return { isRunning: false, total: 0, current: 0, formattedTotal: formatDuration(0), formattedCurrent: formatDuration(0), }; } // Calculate current elapsed time if stopwatch is running let currentElapsed = 0; const isRunning = !!card.stopwatch.startedAt; if (isRunning && card.stopwatch.startedAt) { const startedAt = new Date(card.stopwatch.startedAt); const now = new Date(); currentElapsed = Math.floor((now.getTime() - startedAt.getTime()) / 1000); } return { isRunning, total: card.stopwatch.total || 0, current: currentElapsed, startedAt: card.stopwatch.startedAt, formattedTotal: formatDuration(card.stopwatch.total || 0), formattedCurrent: formatDuration(currentElapsed), }; } catch (error) { throw new Error(`Failed to get card stopwatch: ${error instanceof Error ? error.message : String(error)}`); } } /** * Resets the stopwatch for a card * * @param {string} id - The ID of the card to reset the stopwatch for * @returns {Promise<object>} The updated card with reset stopwatch */ export async function resetCardStopwatch(id) { try { // Set stopwatch to null to clear it const response = await plankaRequest(`/api/cards/${id}`, { method: "PATCH", body: { stopwatch: null }, }); const parsedResponse = CardResponseSchema.parse(response); return parsedResponse.item; } catch (error) { throw new Error(`Failed to reset card stopwatch: ${error instanceof Error ? error.message : String(error)}`); } } /** * Formats a duration in seconds to a human-readable string * * @param {number} seconds - The duration in seconds * @returns {string} Formatted duration string (e.g., "2h 30m 15s") */ function formatDuration(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; let result = ""; if (hours > 0) { result += `${hours}h `; } if (minutes > 0 || hours > 0) { result += `${minutes}m `; } result += `${remainingSeconds}s`; return result.trim(); }