diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index b8173f135..7ee0f0e73 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -399,6 +399,28 @@ Create a new custom property (metadata) on a Confluence page. | ↳ `authorId` | string | Account ID of the version author | | ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +### `confluence_delete_page_property` + +Delete a content property from a Confluence page by its property ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page containing the property | +| `propertyId` | string | Yes | The ID of the property to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | ID of the page | +| `propertyId` | string | ID of the deleted property | +| `deleted` | boolean | Deletion status | + ### `confluence_search` Search for content across Confluence pages, blog posts, and other content. @@ -872,6 +894,90 @@ Add a label to a Confluence page for organization and categorization. | `labelName` | string | Name of the added label | | `labelId` | string | ID of the added label | +### `confluence_delete_label` + +Remove a label from a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to remove the label from | +| `labelName` | string | Yes | Name of the label to remove | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | Page ID the label was removed from | +| `labelName` | string | Name of the removed label | +| `deleted` | boolean | Deletion status | + +### `confluence_get_pages_by_label` + +Retrieve all pages that have a specific label applied. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `labelId` | string | Yes | The ID of the label to get pages for | +| `limit` | number | No | Maximum number of pages to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `labelId` | string | ID of the label | +| `pages` | array | Array of pages with this label | +| ↳ `id` | string | Unique page identifier | +| ↳ `title` | string | Page title | +| ↳ `status` | string | Page status \(e.g., current, archived, trashed, draft\) | +| ↳ `spaceId` | string | ID of the space containing the page | +| ↳ `parentId` | string | ID of the parent page \(null if top-level\) | +| ↳ `authorId` | string | Account ID of the page author | +| ↳ `createdAt` | string | ISO 8601 timestamp when the page was created | +| ↳ `version` | object | Page version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_list_space_labels` + +List all labels associated with a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | The ID of the Confluence space to list labels from | +| `limit` | number | No | Maximum number of labels to return \(default: 25, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | ID of the space | +| `labels` | array | Array of labels on the space | +| ↳ `id` | string | Unique label identifier | +| ↳ `name` | string | Label name | +| ↳ `prefix` | string | Label prefix/type \(e.g., global, my, team\) | +| `nextCursor` | string | Cursor for fetching the next page of results | + ### `confluence_get_space` Get details about a specific Confluence space. diff --git a/apps/sim/app/api/tools/confluence/labels/route.ts b/apps/sim/app/api/tools/confluence/labels/route.ts index ac5eb176a..133267f95 100644 --- a/apps/sim/app/api/tools/confluence/labels/route.ts +++ b/apps/sim/app/api/tools/confluence/labels/route.ts @@ -191,3 +191,84 @@ export async function GET(request: NextRequest) { ) } } + +// Delete a label from a page +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { + domain, + accessToken, + cloudId: providedCloudId, + pageId, + labelName, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + if (!labelName) { + return NextResponse.json({ error: 'Label name is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const encodedLabel = encodeURIComponent(labelName.trim()) + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label/${encodedLabel}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete Confluence label (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ + pageId, + labelName, + deleted: true, + }) + } catch (error) { + logger.error('Error deleting Confluence label:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/pages-by-label/route.ts b/apps/sim/app/api/tools/confluence/pages-by-label/route.ts new file mode 100644 index 000000000..bef622616 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/pages-by-label/route.ts @@ -0,0 +1,103 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePagesByLabelAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const labelId = searchParams.get('labelId') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '50' + const cursor = searchParams.get('cursor') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!labelId) { + return NextResponse.json({ error: 'Label ID is required' }, { status: 400 }) + } + + const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255) + if (!labelIdValidation.isValid) { + return NextResponse.json({ error: labelIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/labels/${labelId}/pages?${queryParams.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get pages by label (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const pages = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + parentId: page.parentId ?? null, + authorId: page.authorId ?? null, + createdAt: page.createdAt ?? null, + version: page.version ?? null, + })) + + return NextResponse.json({ + pages, + labelId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error getting pages by label:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space-labels/route.ts b/apps/sim/app/api/tools/confluence/space-labels/route.ts new file mode 100644 index 000000000..be28cd2c9 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-labels/route.ts @@ -0,0 +1,98 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpaceLabelsAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const spaceId = searchParams.get('spaceId') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + const cursor = searchParams.get('cursor') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/labels?${queryParams.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to list space labels (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const labels = (data.results || []).map((label: any) => ({ + id: label.id, + name: label.name, + prefix: label.prefix || 'global', + })) + + return NextResponse.json({ + labels, + spaceId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing space labels:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 970945c0c..7b7968843 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -394,6 +394,7 @@ export const ConfluenceV2Block: BlockConfig = { // Page Property Operations { label: 'List Page Properties', id: 'list_page_properties' }, { label: 'Create Page Property', id: 'create_page_property' }, + { label: 'Delete Page Property', id: 'delete_page_property' }, // Search Operations { label: 'Search Content', id: 'search' }, { label: 'Search in Space', id: 'search_in_space' }, @@ -414,6 +415,9 @@ export const ConfluenceV2Block: BlockConfig = { // Label Operations { label: 'List Labels', id: 'list_labels' }, { label: 'Add Label', id: 'add_label' }, + { label: 'Delete Label', id: 'delete_label' }, + { label: 'Get Pages by Label', id: 'get_pages_by_label' }, + { label: 'List Space Labels', id: 'list_space_labels' }, // Space Operations { label: 'Get Space', id: 'get_space' }, { label: 'List Spaces', id: 'list_spaces' }, @@ -485,6 +489,8 @@ export const ConfluenceV2Block: BlockConfig = { 'search_in_space', 'get_space', 'list_spaces', + 'get_pages_by_label', + 'list_space_labels', ], not: true, }, @@ -500,6 +506,8 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'upload_attachment', 'add_label', + 'delete_label', + 'delete_page_property', 'get_page_children', 'get_page_ancestors', 'list_page_versions', @@ -527,6 +535,8 @@ export const ConfluenceV2Block: BlockConfig = { 'search_in_space', 'get_space', 'list_spaces', + 'get_pages_by_label', + 'list_space_labels', ], not: true, }, @@ -542,6 +552,8 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'upload_attachment', 'add_label', + 'delete_label', + 'delete_page_property', 'get_page_children', 'get_page_ancestors', 'list_page_versions', @@ -566,6 +578,7 @@ export const ConfluenceV2Block: BlockConfig = { 'search_in_space', 'create_blogpost', 'list_blogposts_in_space', + 'list_space_labels', ], }, }, @@ -601,6 +614,14 @@ export const ConfluenceV2Block: BlockConfig = { required: true, condition: { field: 'operation', value: 'create_page_property' }, }, + { + id: 'propertyId', + title: 'Property ID', + type: 'short-input', + placeholder: 'Enter property ID to delete', + required: true, + condition: { field: 'operation', value: 'delete_page_property' }, + }, { id: 'title', title: 'Title', @@ -694,7 +715,7 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter label name', required: true, - condition: { field: 'operation', value: 'add_label' }, + condition: { field: 'operation', value: ['add_label', 'delete_label'] }, }, { id: 'labelPrefix', @@ -709,6 +730,14 @@ export const ConfluenceV2Block: BlockConfig = { value: () => 'global', condition: { field: 'operation', value: 'add_label' }, }, + { + id: 'labelId', + title: 'Label ID', + type: 'short-input', + placeholder: 'Enter label ID', + required: true, + condition: { field: 'operation', value: 'get_pages_by_label' }, + }, { id: 'blogPostStatus', title: 'Status', @@ -759,6 +788,8 @@ export const ConfluenceV2Block: BlockConfig = { 'list_page_versions', 'list_page_properties', 'list_labels', + 'get_pages_by_label', + 'list_space_labels', ], }, }, @@ -780,6 +811,8 @@ export const ConfluenceV2Block: BlockConfig = { 'list_page_versions', 'list_page_properties', 'list_labels', + 'get_pages_by_label', + 'list_space_labels', ], }, }, @@ -800,6 +833,7 @@ export const ConfluenceV2Block: BlockConfig = { // Property Tools 'confluence_list_page_properties', 'confluence_create_page_property', + 'confluence_delete_page_property', // Search Tools 'confluence_search', 'confluence_search_in_space', @@ -820,6 +854,9 @@ export const ConfluenceV2Block: BlockConfig = { // Label Tools 'confluence_list_labels', 'confluence_add_label', + 'confluence_delete_label', + 'confluence_get_pages_by_label', + 'confluence_list_space_labels', // Space Tools 'confluence_get_space', 'confluence_list_spaces', @@ -852,6 +889,8 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_list_page_properties' case 'create_page_property': return 'confluence_create_page_property' + case 'delete_page_property': + return 'confluence_delete_page_property' // Search Operations case 'search': return 'confluence_search' @@ -887,6 +926,12 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_list_labels' case 'add_label': return 'confluence_add_label' + case 'delete_label': + return 'confluence_delete_label' + case 'get_pages_by_label': + return 'confluence_get_pages_by_label' + case 'list_space_labels': + return 'confluence_list_space_labels' // Space Operations case 'get_space': return 'confluence_get_space' @@ -908,7 +953,9 @@ export const ConfluenceV2Block: BlockConfig = { versionNumber, propertyKey, propertyValue, + propertyId, labelPrefix, + labelId, blogPostStatus, purge, bodyFormat, @@ -959,7 +1006,9 @@ export const ConfluenceV2Block: BlockConfig = { } } - // Operations that support cursor pagination + // Operations that support generic cursor pagination. + // get_pages_by_label and list_space_labels have dedicated handlers + // below that pass cursor along with their required params (labelId, spaceId). const supportsCursor = [ 'list_attachments', 'list_spaces', @@ -996,6 +1045,35 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'delete_page_property') { + return { + credential, + pageId: effectivePageId, + operation, + propertyId, + ...rest, + } + } + + if (operation === 'get_pages_by_label') { + return { + credential, + operation, + labelId, + cursor: cursor || undefined, + ...rest, + } + } + + if (operation === 'list_space_labels') { + return { + credential, + operation, + cursor: cursor || undefined, + ...rest, + } + } + if (operation === 'upload_attachment') { const normalizedFile = normalizeFileInput(attachmentFile, { single: true }) if (!normalizedFile) { @@ -1044,7 +1122,9 @@ export const ConfluenceV2Block: BlockConfig = { attachmentFileName: { type: 'string', description: 'Custom file name for attachment' }, attachmentComment: { type: 'string', description: 'Comment for the attachment' }, labelName: { type: 'string', description: 'Label name' }, + labelId: { type: 'string', description: 'Label identifier' }, labelPrefix: { type: 'string', description: 'Label prefix (global, my, team, system)' }, + propertyId: { type: 'string', description: 'Property identifier' }, blogPostStatus: { type: 'string', description: 'Blog post status (current or draft)' }, purge: { type: 'boolean', description: 'Permanently delete instead of moving to trash' }, bodyFormat: { type: 'string', description: 'Body format for comments' }, @@ -1080,6 +1160,7 @@ export const ConfluenceV2Block: BlockConfig = { // Label Results labels: { type: 'array', description: 'List of labels' }, labelName: { type: 'string', description: 'Label name' }, + labelId: { type: 'string', description: 'Label identifier' }, // Space Results spaces: { type: 'array', description: 'List of spaces' }, spaceId: { type: 'string', description: 'Space identifier' }, diff --git a/apps/sim/tools/confluence/delete_label.ts b/apps/sim/tools/confluence/delete_label.ts new file mode 100644 index 000000000..2f92766fc --- /dev/null +++ b/apps/sim/tools/confluence/delete_label.ts @@ -0,0 +1,114 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteLabelParams { + accessToken: string + domain: string + pageId: string + labelName: string + cloudId?: string +} + +export interface ConfluenceDeleteLabelResponse { + success: boolean + output: { + ts: string + pageId: string + labelName: string + deleted: boolean + } +} + +export const confluenceDeleteLabelTool: ToolConfig< + ConfluenceDeleteLabelParams, + ConfluenceDeleteLabelResponse +> = { + id: 'confluence_delete_label', + name: 'Confluence Delete Label', + description: 'Remove a label from a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to remove the label from', + }, + labelName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the label to remove', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/labels', + method: 'DELETE', + headers: (params: ConfluenceDeleteLabelParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteLabelParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + labelName: params.labelName?.trim(), + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + labelName: data.labelName ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { + type: 'string', + description: 'Page ID the label was removed from', + }, + labelName: { + type: 'string', + description: 'Name of the removed label', + }, + deleted: { + type: 'boolean', + description: 'Deletion status', + }, + }, +} diff --git a/apps/sim/tools/confluence/delete_page_property.ts b/apps/sim/tools/confluence/delete_page_property.ts new file mode 100644 index 000000000..d7b6c5fbb --- /dev/null +++ b/apps/sim/tools/confluence/delete_page_property.ts @@ -0,0 +1,105 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeletePagePropertyParams { + accessToken: string + domain: string + pageId: string + propertyId: string + cloudId?: string +} + +export interface ConfluenceDeletePagePropertyResponse { + success: boolean + output: { + ts: string + pageId: string + propertyId: string + deleted: boolean + } +} + +export const confluenceDeletePagePropertyTool: ToolConfig< + ConfluenceDeletePagePropertyParams, + ConfluenceDeletePagePropertyResponse +> = { + id: 'confluence_delete_page_property', + name: 'Confluence Delete Page Property', + description: 'Delete a content property from a Confluence page by its property ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page containing the property', + }, + propertyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the property to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-properties', + method: 'DELETE', + headers: (params: ConfluenceDeletePagePropertyParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeletePagePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + propertyId: params.propertyId?.trim(), + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + propertyId: data.propertyId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { type: 'string', description: 'ID of the page' }, + propertyId: { type: 'string', description: 'ID of the deleted property' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/get_pages_by_label.ts b/apps/sim/tools/confluence/get_pages_by_label.ts new file mode 100644 index 000000000..af67210a0 --- /dev/null +++ b/apps/sim/tools/confluence/get_pages_by_label.ts @@ -0,0 +1,143 @@ +import { PAGE_ITEM_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPagesByLabelParams { + accessToken: string + domain: string + labelId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceGetPagesByLabelResponse { + success: boolean + output: { + ts: string + labelId: string + pages: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + parentId: string | null + authorId: string | null + createdAt: string | null + version: { + number: number + message?: string + createdAt?: string + } | null + }> + nextCursor: string | null + } +} + +export const confluenceGetPagesByLabelTool: ToolConfig< + ConfluenceGetPagesByLabelParams, + ConfluenceGetPagesByLabelResponse +> = { + id: 'confluence_get_pages_by_label', + name: 'Confluence Get Pages by Label', + description: 'Retrieve all pages that have a specific label applied.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the label to get pages for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of pages to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceGetPagesByLabelParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + labelId: params.labelId, + limit: String(params.limit || 50), + }) + if (params.cursor) { + query.set('cursor', params.cursor) + } + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/pages-by-label?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceGetPagesByLabelParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + labelId: data.labelId ?? '', + pages: data.pages ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + labelId: { type: 'string', description: 'ID of the label' }, + pages: { + type: 'array', + description: 'Array of pages with this label', + items: { + type: 'object', + properties: PAGE_ITEM_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index d78645b15..2494f32d0 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -5,11 +5,14 @@ import { confluenceCreatePageTool } from '@/tools/confluence/create_page' import { confluenceCreatePagePropertyTool } from '@/tools/confluence/create_page_property' import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment' import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment' +import { confluenceDeleteLabelTool } from '@/tools/confluence/delete_label' import { confluenceDeletePageTool } from '@/tools/confluence/delete_page' +import { confluenceDeletePagePropertyTool } from '@/tools/confluence/delete_page_property' import { confluenceGetBlogPostTool } from '@/tools/confluence/get_blogpost' import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ancestors' import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children' import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version' +import { confluenceGetPagesByLabelTool } from '@/tools/confluence/get_pages_by_label' import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts' @@ -19,6 +22,7 @@ import { confluenceListLabelsTool } from '@/tools/confluence/list_labels' import { confluenceListPagePropertiesTool } from '@/tools/confluence/list_page_properties' import { confluenceListPageVersionsTool } from '@/tools/confluence/list_page_versions' import { confluenceListPagesInSpaceTool } from '@/tools/confluence/list_pages_in_space' +import { confluenceListSpaceLabelsTool } from '@/tools/confluence/list_space_labels' import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces' import { confluenceRetrieveTool } from '@/tools/confluence/retrieve' import { confluenceSearchTool } from '@/tools/confluence/search' @@ -78,6 +82,7 @@ export { // Page Properties Tools confluenceListPagePropertiesTool, confluenceCreatePagePropertyTool, + confluenceDeletePagePropertyTool, // Blog Post Tools confluenceListBlogPostsTool, confluenceGetBlogPostTool, @@ -98,6 +103,9 @@ export { // Label Tools confluenceListLabelsTool, confluenceAddLabelTool, + confluenceDeleteLabelTool, + confluenceGetPagesByLabelTool, + confluenceListSpaceLabelsTool, // Space Tools confluenceGetSpaceTool, confluenceListSpacesTool, diff --git a/apps/sim/tools/confluence/list_space_labels.ts b/apps/sim/tools/confluence/list_space_labels.ts new file mode 100644 index 000000000..d30990d06 --- /dev/null +++ b/apps/sim/tools/confluence/list_space_labels.ts @@ -0,0 +1,134 @@ +import { LABEL_ITEM_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpaceLabelsParams { + accessToken: string + domain: string + spaceId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListSpaceLabelsResponse { + success: boolean + output: { + ts: string + spaceId: string + labels: Array<{ + id: string + name: string + prefix: string + }> + nextCursor: string | null + } +} + +export const confluenceListSpaceLabelsTool: ToolConfig< + ConfluenceListSpaceLabelsParams, + ConfluenceListSpaceLabelsResponse +> = { + id: 'confluence_list_space_labels', + name: 'Confluence List Space Labels', + description: 'List all labels associated with a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Confluence space to list labels from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of labels to return (default: 25, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceListSpaceLabelsParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + spaceId: params.spaceId, + limit: String(params.limit || 25), + }) + if (params.cursor) { + query.set('cursor', params.cursor) + } + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/space-labels?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceListSpaceLabelsParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.spaceId ?? '', + labels: data.labels ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'ID of the space' }, + labels: { + type: 'array', + description: 'Array of labels on the space', + items: { + type: 'object', + properties: LABEL_ITEM_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 7411c53c5..52506d744 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -118,10 +118,13 @@ import { confluenceCreatePageTool, confluenceDeleteAttachmentTool, confluenceDeleteCommentTool, + confluenceDeleteLabelTool, + confluenceDeletePagePropertyTool, confluenceDeletePageTool, confluenceGetBlogPostTool, confluenceGetPageAncestorsTool, confluenceGetPageChildrenTool, + confluenceGetPagesByLabelTool, confluenceGetPageVersionTool, confluenceGetSpaceTool, confluenceListAttachmentsTool, @@ -132,6 +135,7 @@ import { confluenceListPagePropertiesTool, confluenceListPagesInSpaceTool, confluenceListPageVersionsTool, + confluenceListSpaceLabelsTool, confluenceListSpacesTool, confluenceRetrieveTool, confluenceSearchInSpaceTool, @@ -2667,6 +2671,10 @@ export const tools: Record = { confluence_delete_attachment: confluenceDeleteAttachmentTool, confluence_list_labels: confluenceListLabelsTool, confluence_add_label: confluenceAddLabelTool, + confluence_get_pages_by_label: confluenceGetPagesByLabelTool, + confluence_list_space_labels: confluenceListSpaceLabelsTool, + confluence_delete_label: confluenceDeleteLabelTool, + confluence_delete_page_property: confluenceDeletePagePropertyTool, confluence_get_space: confluenceGetSpaceTool, confluence_list_spaces: confluenceListSpacesTool, cursor_list_agents: cursorListAgentsTool,