import * as z from "zod" import { defineAction } from "../../../automation/actions" import { TRELLO_READ_SCOPE, TRELLO_WRITE_SCOPE } from "./scopes" import { getTrelloApi, normalizeTrelloBoardId, normalizeTrelloCardId, } from "@automate.ax/integration-contracts/trello" import { TRELLO_CARD_SCHEMA, TRELLO_PROVIDER_CARD_SCHEMA, toTrelloCard, } from "@automate.ax/integration-contracts/trello" const TRELLO_ACCOUNT = "trello" const CARD_FIELDS = "id,name,desc,closed,idBoard,idList,url,shortUrl,shortLink,pos,due,start,dueComplete,idMembers,idLabels,labels,dateLastActivity,subscribed" const POSITION_SCHEMA = z.union([ z.enum(["top", "bottom"]), z.number().nonnegative(), ]) const DATE_INPUT_SCHEMA = z.union([ z.date(), z.iso.datetime({ offset: true }).transform((value) => new Date(value)), ]) /** Retrieves one Trello card by ID, short link, or URL. */ export const getTrelloCard = defineAction("Get Trello card") .describe("Retrieves one card by ID, short link, or Trello URL.") .account(TRELLO_ACCOUNT, TRELLO_READ_SCOPE) .input(z.object({ card: z.string().trim().min(1) })) .output(TRELLO_CARD_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toTrelloCard( await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(normalizeTrelloCardId(input.card))}`, { query: { fields: CARD_FIELDS }, responseSchema: TRELLO_PROVIDER_CARD_SCHEMA, }, ), ), ) /** Lists cards in one Trello list. */ export const listTrelloCards = defineAction("List Trello cards") .describe("Lists cards in a Trello list with filtering and date cursors.") .account(TRELLO_ACCOUNT, TRELLO_READ_SCOPE) .input( z.object({ /** Return cards created before this card ID or timestamp. */ before: z.string().min(1).optional(), /** Whether to return open, archived, or all cards. */ filter: z.enum(["open", "closed", "all"]).prefault("open"), /** Maximum cards to return. */ limit: z.number().int().min(1).max(1000).prefault(100), /** Stable Trello list ID. */ listId: z.string().trim().min(1), /** Return cards created since this card ID or timestamp. */ since: z.string().min(1).optional(), }), ) .output(TRELLO_CARD_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getTrelloApi(account.secret).request( `lists/${encodeURIComponent(input.listId)}/cards`, { query: { before: input.before, fields: CARD_FIELDS, filter: input.filter, limit: input.limit, since: input.since, }, responseSchema: TRELLO_PROVIDER_CARD_SCHEMA.array(), }, ) ).map(toTrelloCard), ) /** Lists cards across every list on one Trello board. */ export const listTrelloBoardCards = defineAction("List Trello board cards") .describe("Lists cards across every list on a Trello board.") .account(TRELLO_ACCOUNT, TRELLO_READ_SCOPE) .input( z.object({ /** Return cards created before this card ID or timestamp. */ before: z.string().min(1).optional(), /** Board ID, short link, or standard Trello board URL. */ board: z.string().trim().min(1), /** Whether to return open, archived, or all cards. */ filter: z.enum(["open", "closed", "all"]).prefault("open"), /** Maximum cards to return. */ limit: z.number().int().min(1).max(1000).prefault(100), /** Return cards created since this card ID or timestamp. */ since: z.string().min(1).optional(), }), ) .output(TRELLO_CARD_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ( await getTrelloApi(account.secret).request( `boards/${encodeURIComponent(normalizeTrelloBoardId(input.board))}/cards`, { query: { before: input.before, fields: CARD_FIELDS, filter: input.filter, limit: input.limit, since: input.since, }, responseSchema: TRELLO_PROVIDER_CARD_SCHEMA.array(), }, ) ).map(toTrelloCard), ) /** Searches Trello cards visible to the connected member. */ export const searchTrelloCards = defineAction("Search Trello cards") .describe("Searches card names and descriptions across visible boards.") .account(TRELLO_ACCOUNT, TRELLO_READ_SCOPE) .input( z.object({ /** Board IDs used to limit the search. */ boardIds: z.string().trim().min(1).array().optional(), /** Maximum cards to return per page. */ limit: z.number().int().min(1).max(1000).prefault(50), /** Zero-based Trello search page. */ page: z.number().int().min(0).max(100).prefault(0), /** Permit prefix matches instead of whole-word matches. */ partial: z.boolean().prefault(false), /** Trello search query, including supported search operators. */ query: z.string().trim().min(1), }), ) .output(TRELLO_CARD_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getTrelloApi(account.secret).request("search", { query: { card_fields: CARD_FIELDS, cards_limit: input.limit, cards_page: input.page, idBoards: input.boardIds?.join(","), modelTypes: "cards", partial: input.partial, query: input.query, }, responseSchema: z.union([ TRELLO_PROVIDER_CARD_SCHEMA.array(), z.looseObject({ cards: TRELLO_PROVIDER_CARD_SCHEMA.array().prefault([]), }), ]), }) return (Array.isArray(result) ? result : result.cards).map(toTrelloCard) }) /** Creates a rich Trello card or copies an existing card. */ export const createTrelloCard = defineAction("Create Trello card") .describe("Creates a card with dates, assignees, labels, and copy controls.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ /** Card street address. */ address: z.string().optional(), /** Card description. */ description: z.string().optional(), /** Initial due time. */ dueAt: DATE_INPUT_SCHEMA.optional(), /** Whether the initial due work is complete. */ dueComplete: z.boolean().optional(), /** Label IDs to apply. */ labelIds: z.string().trim().min(1).array().optional(), /** Stable Trello list ID that receives the card. */ listId: z.string().trim().min(1), /** Human-readable location name. */ locationName: z.string().optional(), /** Member IDs to assign. */ memberIds: z.string().trim().min(1).array().optional(), /** Card name. */ name: z.string().trim().min(1), /** Initial card position. */ position: POSITION_SCHEMA.prefault("bottom"), /** Existing card ID or URL to copy. */ sourceCard: z.string().trim().min(1).optional(), /** Source-card content retained when copying. */ keepFromSource: z .enum([ "all", "attachments", "checklists", "comments", "customFields", "due", "labels", "members", "start", "stickers", ]) .array() .min(1) .refine( (values) => values.length === 1 || !values.includes("all"), 'Use "all" by itself or list individual source-card properties.', ) .optional(), /** Initial work start time. */ startAt: DATE_INPUT_SCHEMA.optional(), /** URL Trello should attach to the new card. */ sourceUrl: z.url().optional(), }), ) .output(TRELLO_CARD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toTrelloCard( await getTrelloApi(account.secret).request("cards", { method: "POST", query: { address: input.address, desc: input.description, due: input.dueAt?.toISOString(), dueComplete: input.dueComplete, idCardSource: input.sourceCard ? normalizeTrelloCardId(input.sourceCard) : undefined, idLabels: input.labelIds?.join(","), idList: input.listId, idMembers: input.memberIds?.join(","), keepFromSource: input.keepFromSource?.join(","), locationName: input.locationName, name: input.name, pos: input.position, start: input.startAt?.toISOString(), urlSource: input.sourceUrl, }, responseSchema: TRELLO_PROVIDER_CARD_SCHEMA, }), ), ) /** Updates the mutable fields of a Trello card. */ export const updateTrelloCard = defineAction("Update Trello card") .describe("Updates, moves, archives, schedules, or reassigns a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z .object({ /** Card street address. */ address: z.string().optional(), /** Move the card to this board. */ boardId: z.string().trim().min(1).optional(), /** Card ID, short link, or standard Trello card URL. */ card: z.string().trim().min(1), /** Archive or reopen the card. */ closed: z.boolean().optional(), /** Clear the existing due time. */ clearDue: z.boolean().optional(), /** Clear the existing start time. */ clearStart: z.boolean().optional(), /** Card description. */ description: z.string().optional(), /** New due time. */ dueAt: DATE_INPUT_SCHEMA.optional(), /** Whether work due on the card is complete. */ dueComplete: z.boolean().optional(), /** Replace all applied labels with these label IDs. */ labelIds: z.string().trim().min(1).array().optional(), /** Move the card to this list. */ listId: z.string().trim().min(1).optional(), /** Human-readable location name. */ locationName: z.string().optional(), /** Replace all assigned members with these member IDs. */ memberIds: z.string().trim().min(1).array().optional(), /** New card name. */ name: z.string().trim().min(1).optional(), /** New card position. */ position: POSITION_SCHEMA.optional(), /** New work start time. */ startAt: DATE_INPUT_SCHEMA.optional(), /** Whether the connected member watches the card. */ subscribed: z.boolean().optional(), }) .refine(({ clearDue, dueAt }) => !(clearDue && dueAt), { message: "Provide dueAt or clearDue, not both.", path: ["clearDue"], }) .refine(({ clearStart, startAt }) => !(clearStart && startAt), { message: "Provide startAt or clearStart, not both.", path: ["clearStart"], }) .refine( (input) => input.address !== undefined || input.boardId !== undefined || input.closed !== undefined || input.clearDue !== undefined || input.clearStart !== undefined || input.description !== undefined || input.dueAt !== undefined || input.dueComplete !== undefined || input.labelIds !== undefined || input.listId !== undefined || input.locationName !== undefined || input.memberIds !== undefined || input.name !== undefined || input.position !== undefined || input.startAt !== undefined || input.subscribed !== undefined, "Provide at least one card field to update.", ), ) .output(TRELLO_CARD_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => toTrelloCard( await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(normalizeTrelloCardId(input.card))}`, { method: "PUT", query: { address: input.address, closed: input.closed, desc: input.description, due: input.clearDue ? "null" : input.dueAt?.toISOString(), dueComplete: input.dueComplete, idBoard: input.boardId, idLabels: input.labelIds?.join(","), idList: input.listId, idMembers: input.memberIds?.join(","), locationName: input.locationName, name: input.name, pos: input.position, start: input.clearStart ? "null" : input.startAt?.toISOString(), subscribed: input.subscribed, }, responseSchema: TRELLO_PROVIDER_CARD_SCHEMA, }, ), ), ) /** Permanently deletes a Trello card. */ export const deleteTrelloCard = defineAction("Delete Trello card") .describe("Permanently deletes a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input(z.object({ card: z.string().trim().min(1) })) .output(z.object({ cardId: z.string(), deleted: z.literal(true) })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const cardId = normalizeTrelloCardId(input.card) await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(cardId)}`, { method: "DELETE", responseSchema: z.unknown() }, ) return { cardId, deleted: true as const } }) /** Assigns one Trello member to a card. */ export const addTrelloCardMember = defineAction("Add Trello card member") .describe("Assigns one member to a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ card: z.string().trim().min(1), memberId: z.string().trim().min(1), }), ) .output( z.object({ added: z.literal(true), cardId: z.string(), memberId: z.string(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const cardId = normalizeTrelloCardId(input.card) await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(cardId)}/idMembers`, { method: "POST", query: { value: input.memberId }, responseSchema: z.unknown(), }, ) return { added: true as const, cardId, memberId: input.memberId } }) /** Removes one Trello member from a card. */ export const removeTrelloCardMember = defineAction("Remove Trello card member") .describe("Removes one assigned member from a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ card: z.string().trim().min(1), memberId: z.string().trim().min(1), }), ) .output( z.object({ cardId: z.string(), memberId: z.string(), removed: z.literal(true), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const cardId = normalizeTrelloCardId(input.card) await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(cardId)}/idMembers/${encodeURIComponent(input.memberId)}`, { method: "DELETE", responseSchema: z.unknown() }, ) return { cardId, memberId: input.memberId, removed: true as const } }) /** Applies one existing Trello label to a card. */ export const addTrelloCardLabel = defineAction("Add Trello card label") .describe("Applies one existing board label to a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ card: z.string().trim().min(1), labelId: z.string().trim().min(1), }), ) .output( z.object({ added: z.literal(true), cardId: z.string(), labelId: z.string(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const cardId = normalizeTrelloCardId(input.card) await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(cardId)}/idLabels`, { method: "POST", query: { value: input.labelId }, responseSchema: z.unknown(), }, ) return { added: true as const, cardId, labelId: input.labelId } }) /** Removes one Trello label from a card. */ export const removeTrelloCardLabel = defineAction("Remove Trello card label") .describe("Removes one applied label from a Trello card.") .account(TRELLO_ACCOUNT, TRELLO_WRITE_SCOPE) .input( z.object({ card: z.string().trim().min(1), labelId: z.string().trim().min(1), }), ) .output( z.object({ cardId: z.string(), labelId: z.string(), removed: z.literal(true), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const cardId = normalizeTrelloCardId(input.card) await getTrelloApi(account.secret).request( `cards/${encodeURIComponent(cardId)}/idLabels/${encodeURIComponent(input.labelId)}`, { method: "DELETE", responseSchema: z.unknown() }, ) return { cardId, labelId: input.labelId, removed: true as const } })