Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

708 lines
25 KiB
TypeScript

import { isRecordLike } from '@sim/utils/object'
import type {
ClickUpAttachment,
ClickUpChecklist,
ClickUpChecklistItem,
ClickUpComment,
ClickUpCustomField,
ClickUpFolder,
ClickUpList,
ClickUpMember,
ClickUpPriority,
ClickUpSpace,
ClickUpStatus,
ClickUpTag,
ClickUpTask,
ClickUpTimeEntry,
ClickUpUser,
ClickUpWorkspace,
} from '@/tools/clickup/types'
import type { OutputProperty } from '@/tools/types'
export const CLICKUP_API_BASE_URL = 'https://api.clickup.com/api/v2'
/**
* Builds the Authorization header value for ClickUp API requests.
*
* ClickUp documents two credential shapes: OAuth access tokens are sent as
* `Authorization: Bearer <token>`, while personal API tokens (prefixed with
* `pk_`) must be sent bare as `Authorization: <token>` with no scheme.
* This helper detects the personal-token prefix and returns the correct form
* so tools work with both OAuth connections and pasted API tokens.
*
* @param accessToken - OAuth access token or ClickUp personal API token
* @returns The value to use for the `Authorization` header
*/
export function clickupAuthorizationHeader(accessToken: string): string {
return accessToken.startsWith('pk_') ? accessToken : `Bearer ${accessToken}`
}
function getRequiredString(value: unknown, field: string): string {
if (typeof value === 'string' && value.trim().length > 0) {
return value
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
throw new Error(`ClickUp response is missing required field: ${field}`)
}
function getOptionalString(value: unknown): string | null {
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
return null
}
function getOptionalBoolean(value: unknown): boolean | null {
return typeof value === 'boolean' ? value : null
}
function getOptionalNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === 'string' && value.trim().length > 0) {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed
}
}
return null
}
const CLICKUP_USER_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'number', description: 'User ID', nullable: true },
username: { type: 'string', description: 'Username', nullable: true },
email: { type: 'string', description: 'User email', nullable: true },
profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true },
}
export const CLICKUP_TASK_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Task ID' },
customId: { type: 'string', description: 'Custom task ID', nullable: true },
name: { type: 'string', description: 'Task name' },
textContent: { type: 'string', description: 'Plain text content', nullable: true },
description: { type: 'string', description: 'Task description', nullable: true },
markdownDescription: {
type: 'string',
description: 'Task description in Markdown (present when requested)',
nullable: true,
},
status: {
type: 'object',
description: 'Task status',
nullable: true,
properties: {
status: { type: 'string', description: 'Status name', nullable: true },
color: { type: 'string', description: 'Status color', nullable: true },
type: { type: 'string', description: 'Status type (open, closed, custom)', nullable: true },
},
},
archived: { type: 'boolean', description: 'Whether the task is archived' },
creator: {
type: 'object',
description: 'Task creator',
nullable: true,
properties: CLICKUP_USER_OUTPUT_PROPERTIES,
},
assignees: {
type: 'array',
description: 'Users assigned to the task',
items: { type: 'object', properties: CLICKUP_USER_OUTPUT_PROPERTIES },
},
watchers: {
type: 'array',
description: 'Users watching the task',
items: { type: 'object', properties: CLICKUP_USER_OUTPUT_PROPERTIES },
},
tags: {
type: 'array',
description: 'Tags applied to the task',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Tag name', nullable: true },
tagFg: { type: 'string', description: 'Tag foreground color', nullable: true },
tagBg: { type: 'string', description: 'Tag background color', nullable: true },
},
},
},
parent: { type: 'string', description: 'Parent task ID', nullable: true },
priority: {
type: 'object',
description: 'Task priority',
nullable: true,
properties: {
id: { type: 'string', description: 'Priority ID', nullable: true },
priority: { type: 'string', description: 'Priority name', nullable: true },
color: { type: 'string', description: 'Priority color', nullable: true },
},
},
dueDate: { type: 'string', description: 'Due date (Unix ms)', nullable: true },
startDate: { type: 'string', description: 'Start date (Unix ms)', nullable: true },
points: { type: 'number', description: 'Sprint points', nullable: true },
timeEstimate: { type: 'number', description: 'Time estimate in milliseconds', nullable: true },
timeSpent: { type: 'number', description: 'Time tracked in milliseconds', nullable: true },
customFields: {
type: 'json',
description: 'Custom field values on the task (id, name, type, value)',
},
dateCreated: { type: 'string', description: 'Creation timestamp (Unix ms)', nullable: true },
dateUpdated: { type: 'string', description: 'Last update timestamp (Unix ms)', nullable: true },
dateClosed: { type: 'string', description: 'Closed timestamp (Unix ms)', nullable: true },
dateDone: { type: 'string', description: 'Done timestamp (Unix ms)', nullable: true },
list: {
type: 'object',
description: 'List containing the task',
nullable: true,
properties: {
id: { type: 'string', description: 'List ID' },
name: { type: 'string', description: 'List name', nullable: true },
},
},
folder: {
type: 'object',
description: 'Folder containing the task',
nullable: true,
properties: {
id: { type: 'string', description: 'Folder ID' },
name: { type: 'string', description: 'Folder name', nullable: true },
},
},
space: {
type: 'object',
description: 'Space containing the task',
nullable: true,
properties: {
id: { type: 'string', description: 'Space ID' },
name: { type: 'string', description: 'Space name', nullable: true },
},
},
url: { type: 'string', description: 'URL to the task in ClickUp', nullable: true },
subtasks: {
type: 'json',
description: 'Subtasks with the same shape as the task object (present when requested)',
nullable: true,
},
}
export const CLICKUP_MEMBER_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'number', description: 'Member user ID', nullable: true },
username: { type: 'string', description: 'Username', nullable: true },
email: { type: 'string', description: 'User email', nullable: true },
color: { type: 'string', description: 'Profile color', nullable: true },
initials: { type: 'string', description: 'User initials', nullable: true },
profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true },
}
export const CLICKUP_COMMENT_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Comment ID' },
commentText: { type: 'string', description: 'Comment text content', nullable: true },
resolved: { type: 'boolean', description: 'Whether the comment is resolved', nullable: true },
user: {
type: 'object',
description: 'Comment author',
nullable: true,
properties: CLICKUP_USER_OUTPUT_PROPERTIES,
},
assignee: {
type: 'object',
description: 'User the comment is assigned to',
nullable: true,
properties: CLICKUP_USER_OUTPUT_PROPERTIES,
},
date: { type: 'string', description: 'Comment timestamp (Unix ms)', nullable: true },
replyCount: { type: 'string', description: 'Number of replies', nullable: true },
}
export const CLICKUP_LIST_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'List ID' },
name: { type: 'string', description: 'List name', nullable: true },
taskCount: { type: 'string', description: 'Number of tasks in the list', nullable: true },
archived: { type: 'boolean', description: 'Whether the list is archived', nullable: true },
}
export const CLICKUP_FOLDER_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Folder ID' },
name: { type: 'string', description: 'Folder name', nullable: true },
hidden: { type: 'boolean', description: 'Whether the folder is hidden', nullable: true },
taskCount: { type: 'string', description: 'Number of tasks in the folder', nullable: true },
space: {
type: 'object',
description: 'Space containing the folder',
nullable: true,
properties: {
id: { type: 'string', description: 'Space ID' },
name: { type: 'string', description: 'Space name', nullable: true },
},
},
}
export const CLICKUP_TAG_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
name: { type: 'string', description: 'Tag name', nullable: true },
tagFg: { type: 'string', description: 'Tag foreground color', nullable: true },
tagBg: { type: 'string', description: 'Tag background color', nullable: true },
}
export function mapClickUpUser(value: unknown): ClickUpUser | null {
if (!isRecordLike(value)) {
return null
}
return {
id: getOptionalNumber(value.id),
username: getOptionalString(value.username),
email: getOptionalString(value.email),
profilePicture: getOptionalString(value.profilePicture),
}
}
function mapClickUpStatus(value: unknown): ClickUpStatus | null {
if (!isRecordLike(value)) {
return null
}
return {
status: getOptionalString(value.status),
color: getOptionalString(value.color),
type: getOptionalString(value.type),
}
}
function mapClickUpPriority(value: unknown): ClickUpPriority | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return { id: String(value), priority: null, color: null }
}
if (!isRecordLike(value)) {
return null
}
return {
id: getOptionalString(value.id),
priority: getOptionalString(value.priority) ?? getOptionalString(value.name),
color: getOptionalString(value.color),
}
}
export function mapClickUpTag(value: unknown): ClickUpTag | null {
if (!isRecordLike(value)) {
return null
}
return {
name: getOptionalString(value.name),
tagFg: getOptionalString(value.tag_fg),
tagBg: getOptionalString(value.tag_bg),
}
}
function mapIdName(value: unknown): { id: string; name: string | null } | null {
if (!isRecordLike(value)) {
return null
}
const id = getOptionalString(value.id)
if (!id) {
return null
}
return { id, name: getOptionalString(value.name) }
}
export function mapClickUpTask(value: unknown): ClickUpTask {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid task object')
}
const rawAssignees = Array.isArray(value.assignees) ? value.assignees : []
const rawWatchers = Array.isArray(value.watchers) ? value.watchers : []
const rawTags = Array.isArray(value.tags) ? value.tags : []
const rawCustomFields = Array.isArray(value.custom_fields) ? value.custom_fields : []
return {
id: getRequiredString(value.id, 'id'),
customId: getOptionalString(value.custom_id),
name: getRequiredString(value.name, 'name'),
textContent: getOptionalString(value.text_content),
description: getOptionalString(value.description),
markdownDescription: getOptionalString(value.markdown_description),
status: mapClickUpStatus(value.status),
archived: typeof value.archived === 'boolean' ? value.archived : false,
creator: mapClickUpUser(value.creator),
assignees: rawAssignees
.map((assignee) => mapClickUpUser(assignee))
.filter((assignee): assignee is ClickUpUser => assignee !== null),
watchers: rawWatchers
.map((watcher) => mapClickUpUser(watcher))
.filter((watcher): watcher is ClickUpUser => watcher !== null),
tags: rawTags.map((tag) => mapClickUpTag(tag)).filter((tag): tag is ClickUpTag => tag !== null),
parent: getOptionalString(value.parent),
priority: mapClickUpPriority(value.priority),
dueDate: getOptionalString(value.due_date),
startDate: getOptionalString(value.start_date),
points: getOptionalNumber(value.points),
timeEstimate: getOptionalNumber(value.time_estimate),
timeSpent: getOptionalNumber(value.time_spent),
customFields: rawCustomFields.filter(isRecordLike),
dateCreated: getOptionalString(value.date_created),
dateUpdated: getOptionalString(value.date_updated),
dateClosed: getOptionalString(value.date_closed),
dateDone: getOptionalString(value.date_done),
list: mapIdName(value.list),
folder: mapIdName(value.folder),
space: mapIdName(value.space),
url: getOptionalString(value.url),
subtasks: Array.isArray(value.subtasks)
? value.subtasks.map((subtask) => mapClickUpTask(subtask))
: null,
}
}
export function mapClickUpComment(value: unknown): ClickUpComment {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid comment object')
}
return {
id: getRequiredString(value.id, 'id'),
commentText: getOptionalString(value.comment_text),
resolved: getOptionalBoolean(value.resolved),
user: mapClickUpUser(value.user),
assignee: mapClickUpUser(value.assignee),
date: getOptionalString(value.date),
replyCount: getOptionalString(value.reply_count),
}
}
export function mapClickUpWorkspace(value: unknown): ClickUpWorkspace {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid workspace object')
}
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
color: getOptionalString(value.color),
avatar: getOptionalString(value.avatar),
}
}
export function mapClickUpSpace(value: unknown): ClickUpSpace {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid space object')
}
const rawStatuses = Array.isArray(value.statuses) ? value.statuses : []
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
private: getOptionalBoolean(value.private),
archived: getOptionalBoolean(value.archived),
statuses: rawStatuses
.map((status) => mapClickUpStatus(status))
.filter((status): status is ClickUpStatus => status !== null),
}
}
export function mapClickUpFolder(value: unknown): ClickUpFolder {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid folder object')
}
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
hidden: getOptionalBoolean(value.hidden),
taskCount: getOptionalString(value.task_count),
space: mapIdName(value.space),
}
}
export function mapClickUpList(value: unknown): ClickUpList {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid list object')
}
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
taskCount: getOptionalString(value.task_count),
archived: getOptionalBoolean(value.archived),
}
}
export function mapClickUpAttachment(value: unknown): ClickUpAttachment {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid attachment object')
}
return {
id: getRequiredString(value.id, 'id'),
version: getOptionalString(value.version),
title: getOptionalString(value.title),
extension: getOptionalString(value.extension),
url: getOptionalString(value.url),
date: getOptionalNumber(value.date),
thumbnailSmall: getOptionalString(value.thumbnail_small),
thumbnailLarge: getOptionalString(value.thumbnail_large),
}
}
/**
* Maps a member from the task/list member endpoints. The documented shape is a
* flat user object (`{ id, username, email, ... }`); a nested `{ user: {...} }`
* wrapper (the shape the workspace members endpoint uses) is tolerated too.
*/
export function mapClickUpMember(value: unknown): ClickUpMember {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid member object')
}
const source = isRecordLike(value.user) ? value.user : value
return {
id: getOptionalNumber(source.id),
username: getOptionalString(source.username),
email: getOptionalString(source.email),
color: getOptionalString(source.color),
initials: getOptionalString(source.initials),
profilePicture: getOptionalString(source.profilePicture),
}
}
export function mapClickUpCustomField(value: unknown): ClickUpCustomField {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid custom field object')
}
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
type: getOptionalString(value.type),
typeConfig: isRecordLike(value.type_config) ? value.type_config : null,
dateCreated: getOptionalString(value.date_created),
hideFromGuests: getOptionalBoolean(value.hide_from_guests),
}
}
function mapClickUpChecklistItem(value: unknown): ClickUpChecklistItem {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid checklist item object')
}
return {
id: getRequiredString(value.id, 'id'),
name: getOptionalString(value.name),
orderIndex: getOptionalNumber(value.orderindex),
assignee: mapClickUpUser(value.assignee),
resolved: getOptionalBoolean(value.resolved),
parent: getOptionalString(value.parent),
dateCreated: getOptionalString(value.date_created),
children: Array.isArray(value.children)
? value.children.filter((child): child is string => typeof child === 'string')
: [],
}
}
export function mapClickUpChecklist(value: unknown): ClickUpChecklist {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid checklist object')
}
const rawItems = Array.isArray(value.items) ? value.items : []
return {
id: getRequiredString(value.id, 'id'),
taskId: getOptionalString(value.task_id),
name: getOptionalString(value.name),
orderIndex: getOptionalNumber(value.orderindex),
resolved: getOptionalNumber(value.resolved),
unresolved: getOptionalNumber(value.unresolved),
dateCreated: getOptionalString(value.date_created),
items: rawItems.map((item) => mapClickUpChecklistItem(item)),
}
}
export function mapClickUpTimeEntry(value: unknown): ClickUpTimeEntry {
if (!isRecordLike(value)) {
throw new Error('ClickUp returned an invalid time entry object')
}
const rawTags = Array.isArray(value.tags) ? value.tags : []
const rawTaskTags = Array.isArray(value.task_tags) ? value.task_tags : []
const rawLocation = isRecordLike(value.task_location) ? value.task_location : null
return {
id: getRequiredString(value.id, 'id'),
task: mapIdName(value.task),
workspaceId: getOptionalString(value.wid),
user: mapClickUpUser(value.user),
billable: getOptionalBoolean(value.billable),
start: getOptionalString(value.start),
end: getOptionalString(value.end),
duration: getOptionalNumber(value.duration),
description: getOptionalString(value.description),
tags: rawTags.map((tag) => mapClickUpTag(tag)).filter((tag): tag is ClickUpTag => tag !== null),
source: getOptionalString(value.source),
at: getOptionalString(value.at),
taskUrl: getOptionalString(value.task_url),
taskTags: rawTaskTags
.map((tag) => mapClickUpTag(tag))
.filter((tag): tag is ClickUpTag => tag !== null),
taskLocation: rawLocation
? {
listId: getOptionalString(rawLocation.list_id),
folderId: getOptionalString(rawLocation.folder_id),
spaceId: getOptionalString(rawLocation.space_id),
listName: getOptionalString(rawLocation.list_name),
folderName: getOptionalString(rawLocation.folder_name),
spaceName: getOptionalString(rawLocation.space_name),
}
: null,
}
}
const CLICKUP_CHECKLIST_ITEM_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Checklist item ID' },
name: { type: 'string', description: 'Checklist item name', nullable: true },
orderIndex: { type: 'number', description: 'Order of the item in the checklist', nullable: true },
assignee: {
type: 'object',
description: 'User the item is assigned to',
nullable: true,
properties: {
id: { type: 'number', description: 'User ID', nullable: true },
username: { type: 'string', description: 'Username', nullable: true },
email: { type: 'string', description: 'User email', nullable: true },
profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true },
},
},
resolved: { type: 'boolean', description: 'Whether the item is resolved', nullable: true },
parent: { type: 'string', description: 'Parent checklist item ID', nullable: true },
dateCreated: { type: 'string', description: 'Creation timestamp (Unix ms)', nullable: true },
children: {
type: 'array',
description: 'IDs of nested child items',
items: { type: 'string', description: 'A checklist item ID' },
},
}
export const CLICKUP_CHECKLIST_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Checklist ID' },
taskId: {
type: 'string',
description: 'ID of the task the checklist belongs to',
nullable: true,
},
name: { type: 'string', description: 'Checklist name', nullable: true },
orderIndex: { type: 'number', description: 'Order of the checklist on the task', nullable: true },
resolved: { type: 'number', description: 'Number of resolved items', nullable: true },
unresolved: { type: 'number', description: 'Number of unresolved items', nullable: true },
dateCreated: { type: 'string', description: 'Creation timestamp (Unix ms)', nullable: true },
items: {
type: 'array',
description: 'Items in the checklist',
items: { type: 'object', properties: CLICKUP_CHECKLIST_ITEM_OUTPUT_PROPERTIES },
},
}
export const CLICKUP_TIME_ENTRY_OUTPUT_PROPERTIES: Record<string, OutputProperty> = {
id: { type: 'string', description: 'Time entry ID' },
task: {
type: 'object',
description: 'Task the time entry is associated with',
nullable: true,
properties: {
id: { type: 'string', description: 'Task ID' },
name: { type: 'string', description: 'Task name', nullable: true },
},
},
workspaceId: { type: 'string', description: 'Workspace ID', nullable: true },
user: {
type: 'object',
description: 'User the time entry belongs to',
nullable: true,
properties: {
id: { type: 'number', description: 'User ID', nullable: true },
username: { type: 'string', description: 'Username', nullable: true },
email: { type: 'string', description: 'User email', nullable: true },
profilePicture: { type: 'string', description: 'Profile picture URL', nullable: true },
},
},
billable: { type: 'boolean', description: 'Whether the entry is billable', nullable: true },
start: { type: 'string', description: 'Start timestamp (Unix ms)', nullable: true },
end: { type: 'string', description: 'End timestamp (Unix ms)', nullable: true },
duration: {
type: 'number',
description: 'Duration in milliseconds (negative while the timer is running)',
nullable: true,
},
description: { type: 'string', description: 'Time entry description', nullable: true },
tags: {
type: 'array',
description: 'Time entry tags',
items: { type: 'object', properties: CLICKUP_TAG_OUTPUT_PROPERTIES },
},
source: { type: 'string', description: 'Source that created the entry', nullable: true },
at: { type: 'string', description: 'Last update timestamp (Unix ms)', nullable: true },
taskUrl: { type: 'string', description: 'URL to the task in ClickUp', nullable: true },
taskTags: {
type: 'array',
description: 'Tags on the associated task (present when requested)',
items: { type: 'object', properties: CLICKUP_TAG_OUTPUT_PROPERTIES },
},
taskLocation: {
type: 'object',
description: 'Location of the associated task (names present when requested)',
nullable: true,
properties: {
listId: { type: 'string', description: 'List ID', nullable: true },
folderId: { type: 'string', description: 'Folder ID', nullable: true },
spaceId: { type: 'string', description: 'Space ID', nullable: true },
listName: { type: 'string', description: 'List name', nullable: true },
folderName: { type: 'string', description: 'Folder name', nullable: true },
spaceName: { type: 'string', description: 'Space name', nullable: true },
},
},
}
export function extractClickUpErrorMessage(
response: Response,
data: unknown,
fallback: string
): string {
if (isRecordLike(data)) {
const message =
typeof data.err === 'string' && data.err.trim().length > 0
? data.err
: typeof data.error === 'string' && data.error.trim().length > 0
? data.error
: null
const ecode = data.ECODE
if (message) {
return typeof ecode === 'string' && ecode.trim().length > 0
? `${fallback}: ${message} (${ecode})`
: `${fallback}: ${message}`
}
}
if (response.statusText) {
return `${fallback}: ${response.status} ${response.statusText}`
}
return `${fallback}: ${response.status}`
}