mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
feat(confluence): added more confluence endpoints (#3139)
* feat(confluence): added more confluence endpoints * update license * updated * updated docs
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export interface SupportFooterProps {
|
||||
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
|
||||
|
||||
@@ -7,10 +7,10 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
|
||||
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
|
||||
|
||||
const logger = createLogger('nav')
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
parseWorkflowSSEChunk,
|
||||
} from '@/lib/a2a/utils'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
||||
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
type PushNotificationSetParams,
|
||||
type TaskIdParams,
|
||||
} from '@/app/api/a2a/serve/[agentId]/utils'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
const logger = createLogger('A2AServeAPI')
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
const pageId = searchParams.get('pageId')
|
||||
const providedCloudId = searchParams.get('cloudId')
|
||||
const limit = searchParams.get('limit') || '25'
|
||||
const limit = searchParams.get('limit') || '50'
|
||||
const cursor = searchParams.get('cursor')
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
@@ -47,7 +48,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?limit=${limit}`
|
||||
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/pages/${pageId}/attachments?${queryParams.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -77,9 +83,20 @@ export async function GET(request: NextRequest) {
|
||||
fileSize: attachment.fileSize || 0,
|
||||
mediaType: attachment.mediaType || '',
|
||||
downloadUrl: attachment.downloadLink || attachment._links?.download || '',
|
||||
status: attachment.status ?? null,
|
||||
webuiUrl: attachment._links?.webui ?? null,
|
||||
pageId: attachment.pageId ?? null,
|
||||
blogPostId: attachment.blogPostId ?? null,
|
||||
comment: attachment.comment ?? null,
|
||||
version: attachment.version ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ attachments })
|
||||
return NextResponse.json({
|
||||
attachments,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Confluence attachments:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
285
apps/sim/app/api/tools/confluence/blogposts/route.ts
Normal file
285
apps/sim/app/api/tools/confluence/blogposts/route.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
const logger = createLogger('ConfluenceBlogPostsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const getBlogPostSchema = z
|
||||
.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
blogPostId: z.string().min(1, 'Blog post ID is required'),
|
||||
bodyFormat: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
|
||||
return validation.isValid
|
||||
},
|
||||
(data) => {
|
||||
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
|
||||
return { message: validation.error || 'Invalid blog post ID', path: ['blogPostId'] }
|
||||
}
|
||||
)
|
||||
|
||||
const createBlogPostSchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
spaceId: z.string().min(1, 'Space ID is required'),
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
content: z.string().min(1, 'Content is required'),
|
||||
status: z.enum(['current', 'draft']).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* List all blog posts or get a specific blog post
|
||||
*/
|
||||
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 providedCloudId = searchParams.get('cloudId')
|
||||
const limit = searchParams.get('limit') || '25'
|
||||
const status = searchParams.get('status')
|
||||
const sortOrder = searchParams.get('sort')
|
||||
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 })
|
||||
}
|
||||
|
||||
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 (status) {
|
||||
queryParams.append('status', status)
|
||||
}
|
||||
|
||||
if (sortOrder) {
|
||||
queryParams.append('sort', sortOrder)
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts?${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 blog posts (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const blogPosts = (data.results || []).map((post: any) => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
status: post.status ?? null,
|
||||
spaceId: post.spaceId ?? null,
|
||||
authorId: post.authorId ?? null,
|
||||
createdAt: post.createdAt ?? null,
|
||||
version: post.version ?? null,
|
||||
webUrl: post._links?.webui ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
blogPosts,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing blog posts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific blog post by ID
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Check if this is a create or get request
|
||||
if (body.title && body.content && body.spaceId) {
|
||||
// Create blog post
|
||||
const validation = createBlogPostSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
spaceId,
|
||||
title,
|
||||
content,
|
||||
status,
|
||||
} = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts`
|
||||
|
||||
const createBody = {
|
||||
spaceId,
|
||||
status: status || 'current',
|
||||
title,
|
||||
body: {
|
||||
representation: 'storage',
|
||||
value: content,
|
||||
},
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(createBody),
|
||||
})
|
||||
|
||||
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 create blog post (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
spaceId: data.spaceId,
|
||||
webUrl: data._links?.webui ?? null,
|
||||
})
|
||||
}
|
||||
// Get blog post by ID
|
||||
const validation = getBlogPostSchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
blogPostId,
|
||||
bodyFormat,
|
||||
} = validation.data
|
||||
|
||||
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()
|
||||
if (bodyFormat) {
|
||||
queryParams.append('body-format', bodyFormat)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}${queryParams.toString() ? `?${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 blog post (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
version: data.version ?? null,
|
||||
body: data.body ?? null,
|
||||
webUrl: data._links?.webui ?? null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with blog post operation:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,8 @@ export async function GET(request: NextRequest) {
|
||||
const pageId = searchParams.get('pageId')
|
||||
const providedCloudId = searchParams.get('cloudId')
|
||||
const limit = searchParams.get('limit') || '25'
|
||||
const bodyFormat = searchParams.get('bodyFormat') || 'storage'
|
||||
const cursor = searchParams.get('cursor')
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
@@ -130,7 +132,13 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?limit=${limit}`
|
||||
const queryParams = new URLSearchParams()
|
||||
queryParams.append('limit', String(Math.min(Number(limit), 250)))
|
||||
queryParams.append('body-format', bodyFormat)
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?${queryParams.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -154,14 +162,31 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const comments = (data.results || []).map((comment: any) => ({
|
||||
id: comment.id,
|
||||
body: comment.body?.storage?.value || comment.body?.view?.value || '',
|
||||
createdAt: comment.createdAt || '',
|
||||
authorId: comment.authorId || '',
|
||||
}))
|
||||
const comments = (data.results || []).map((comment: any) => {
|
||||
const bodyValue = comment.body?.storage?.value || comment.body?.view?.value || ''
|
||||
return {
|
||||
id: comment.id,
|
||||
body: {
|
||||
value: bodyValue,
|
||||
representation: bodyFormat,
|
||||
},
|
||||
createdAt: comment.createdAt || '',
|
||||
authorId: comment.authorId || '',
|
||||
status: comment.status ?? null,
|
||||
title: comment.title ?? null,
|
||||
pageId: comment.pageId ?? null,
|
||||
blogPostId: comment.blogPostId ?? null,
|
||||
parentCommentId: comment.parentCommentId ?? null,
|
||||
version: comment.version ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ comments })
|
||||
return NextResponse.json({
|
||||
comments,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Confluence comments:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
cloudId: providedCloudId,
|
||||
pageId,
|
||||
labelName,
|
||||
prefix: labelPrefix,
|
||||
} = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
@@ -52,12 +53,14 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label`
|
||||
|
||||
const body = {
|
||||
prefix: 'global',
|
||||
name: labelName,
|
||||
}
|
||||
const body = [
|
||||
{
|
||||
prefix: labelPrefix || 'global',
|
||||
name: labelName,
|
||||
},
|
||||
]
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -82,7 +85,14 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json({ ...data, pageId, labelName })
|
||||
const addedLabel = data.results?.[0] || data[0] || data
|
||||
return NextResponse.json({
|
||||
id: addedLabel.id ?? '',
|
||||
name: addedLabel.name ?? labelName,
|
||||
prefix: addedLabel.prefix ?? labelPrefix ?? 'global',
|
||||
pageId,
|
||||
labelName,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error adding Confluence label:', error)
|
||||
return NextResponse.json(
|
||||
@@ -105,6 +115,8 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
const pageId = searchParams.get('pageId')
|
||||
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 })
|
||||
@@ -130,7 +142,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
|
||||
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/pages/${pageId}/labels?${queryParams.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -160,7 +177,12 @@ export async function GET(request: NextRequest) {
|
||||
prefix: label.prefix || 'global',
|
||||
}))
|
||||
|
||||
return NextResponse.json({ labels })
|
||||
return NextResponse.json({
|
||||
labels,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Confluence labels:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
96
apps/sim/app/api/tools/confluence/page-ancestors/route.ts
Normal file
96
apps/sim/app/api/tools/confluence/page-ancestors/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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('ConfluencePageAncestorsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Get ancestors (parent pages) of a specific Confluence page.
|
||||
* Uses GET /wiki/api/v2/pages/{id}/ancestors
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 25 } = body
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
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 queryParams = new URLSearchParams()
|
||||
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/ancestors?${queryParams.toString()}`
|
||||
|
||||
logger.info(`Fetching ancestors for page ${pageId}`)
|
||||
|
||||
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 page ancestors (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const ancestors = (data.results || []).map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
status: page.status ?? null,
|
||||
spaceId: page.spaceId ?? null,
|
||||
webUrl: page._links?.webui ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
ancestors,
|
||||
pageId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting page ancestors:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
104
apps/sim/app/api/tools/confluence/page-children/route.ts
Normal file
104
apps/sim/app/api/tools/confluence/page-children/route.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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('ConfluencePageChildrenAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Get child pages of a specific Confluence page.
|
||||
* Uses GET /wiki/api/v2/pages/{id}/children
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
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 queryParams = new URLSearchParams()
|
||||
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/children?${queryParams.toString()}`
|
||||
|
||||
logger.info(`Fetching child pages for page ${pageId}`)
|
||||
|
||||
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 child pages (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const children = (data.results || []).map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
status: page.status ?? null,
|
||||
spaceId: page.spaceId ?? null,
|
||||
childPosition: page.childPosition ?? null,
|
||||
webUrl: page._links?.webui ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
children,
|
||||
parentId: pageId,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting child pages:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
365
apps/sim/app/api/tools/confluence/page-properties/route.ts
Normal file
365
apps/sim/app/api/tools/confluence/page-properties/route.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
|
||||
const logger = createLogger('ConfluencePagePropertiesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const createPropertySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
key: z.string().min(1, 'Property key is required'),
|
||||
value: z.any(),
|
||||
})
|
||||
|
||||
const updatePropertySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
propertyId: z.string().min(1, 'Property ID is required'),
|
||||
key: z.string().min(1, 'Property key is required'),
|
||||
value: z.any(),
|
||||
versionNumber: z.number().min(1, 'Version number is required'),
|
||||
})
|
||||
|
||||
const deletePropertySchema = z.object({
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
propertyId: z.string().min(1, 'Property ID is required'),
|
||||
})
|
||||
|
||||
/**
|
||||
* List all content properties on a page.
|
||||
*/
|
||||
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 pageId = searchParams.get('pageId')
|
||||
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 (!pageId) {
|
||||
return NextResponse.json({ error: 'Page ID 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 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/pages/${pageId}/properties?${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 page properties (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const properties = (data.results || []).map((prop: any) => ({
|
||||
id: prop.id,
|
||||
key: prop.key,
|
||||
value: prop.value ?? null,
|
||||
version: prop.version ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
properties,
|
||||
pageId,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing page properties:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new content property on a page.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = createPropertySchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId, key, value } = validation.data
|
||||
|
||||
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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ key, value }),
|
||||
})
|
||||
|
||||
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 create page property (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json({
|
||||
id: data.id,
|
||||
key: data.key,
|
||||
value: data.value,
|
||||
version: data.version,
|
||||
pageId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error creating page property:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a content property on a page.
|
||||
*/
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
const validation = updatePropertySchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: providedCloudId,
|
||||
pageId,
|
||||
propertyId,
|
||||
key,
|
||||
value,
|
||||
versionNumber,
|
||||
} = validation.data
|
||||
|
||||
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||
if (!pageIdValidation.isValid) {
|
||||
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
|
||||
if (!propertyIdValidation.isValid) {
|
||||
return NextResponse.json({ error: propertyIdValidation.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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key,
|
||||
value,
|
||||
version: { number: versionNumber },
|
||||
}),
|
||||
})
|
||||
|
||||
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 update page property (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return NextResponse.json({
|
||||
id: data.id,
|
||||
key: data.key,
|
||||
value: data.value,
|
||||
version: data.version,
|
||||
pageId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error updating page property:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a content property 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 body = await request.json()
|
||||
|
||||
const validation = deletePropertySchema.safeParse(body)
|
||||
if (!validation.success) {
|
||||
const firstError = validation.error.errors[0]
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId, propertyId } = validation.data
|
||||
|
||||
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
|
||||
if (!pageIdValidation.isValid) {
|
||||
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
|
||||
if (!propertyIdValidation.isValid) {
|
||||
return NextResponse.json({ error: propertyIdValidation.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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}`
|
||||
|
||||
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 page property (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
return NextResponse.json({ propertyId, pageId, deleted: true })
|
||||
} catch (error) {
|
||||
logger.error('Error deleting page property:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
151
apps/sim/app/api/tools/confluence/page-versions/route.ts
Normal file
151
apps/sim/app/api/tools/confluence/page-versions/route.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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('ConfluencePageVersionsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* List all versions of a page or get a specific version.
|
||||
* Uses GET /wiki/api/v2/pages/{id}/versions
|
||||
* and GET /wiki/api/v2/pages/{page-id}/versions/{version-number}
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
pageId,
|
||||
versionNumber,
|
||||
cloudId: providedCloudId,
|
||||
limit = 50,
|
||||
cursor,
|
||||
} = body
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
// If versionNumber is provided, get specific version
|
||||
if (versionNumber !== undefined && versionNumber !== null) {
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}`
|
||||
|
||||
logger.info(`Fetching version ${versionNumber} for page ${pageId}`)
|
||||
|
||||
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 page version (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
version: {
|
||||
number: data.number,
|
||||
message: data.message ?? null,
|
||||
minorEdit: data.minorEdit ?? false,
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
},
|
||||
pageId,
|
||||
})
|
||||
}
|
||||
// List all versions
|
||||
const queryParams = new URLSearchParams()
|
||||
queryParams.append('limit', String(Math.min(limit, 250)))
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions?${queryParams.toString()}`
|
||||
|
||||
logger.info(`Fetching versions for page ${pageId}`)
|
||||
|
||||
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 page versions (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const versions = (data.results || []).map((version: any) => ({
|
||||
number: version.number,
|
||||
message: version.message ?? null,
|
||||
minorEdit: version.minorEdit ?? false,
|
||||
authorId: version.authorId ?? null,
|
||||
createdAt: version.createdAt ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
versions,
|
||||
pageId,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error with page versions:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ const deletePageSchema = z
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
cloudId: z.string().optional(),
|
||||
pageId: z.string().min(1, 'Page ID is required'),
|
||||
purge: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?expand=body.storage,body.view,body.atlas_doc_format`
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -130,16 +131,18 @@ export async function POST(request: NextRequest) {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
body: {
|
||||
view: {
|
||||
value:
|
||||
data.body?.storage?.value ||
|
||||
data.body?.view?.value ||
|
||||
data.body?.atlas_doc_format?.value ||
|
||||
data.content || // try alternative fields
|
||||
data.description ||
|
||||
`Content for page ${data.title}`, // fallback content
|
||||
storage: {
|
||||
value: data.body?.storage?.value ?? null,
|
||||
representation: 'storage',
|
||||
},
|
||||
},
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
parentId: data.parentId ?? null,
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
version: data.version ?? null,
|
||||
_links: data._links ?? null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching Confluence page:', error)
|
||||
@@ -274,7 +277,7 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: firstError.message }, { status: 400 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data
|
||||
const { domain, accessToken, cloudId: providedCloudId, pageId, purge } = validation.data
|
||||
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
@@ -283,7 +286,12 @@ export async function DELETE(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}`
|
||||
const queryParams = new URLSearchParams()
|
||||
if (purge) {
|
||||
queryParams.append('purge', 'true')
|
||||
}
|
||||
const queryString = queryParams.toString()
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -32,7 +32,6 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Use provided cloudId or fetch it if not provided
|
||||
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
@@ -40,7 +39,6 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
// Build the URL with query parameters
|
||||
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages`
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
@@ -57,7 +55,6 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info(`Fetching Confluence pages from: ${url}`)
|
||||
|
||||
// Make the request to Confluence API with OAuth Bearer token
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -79,7 +76,6 @@ export async function POST(request: NextRequest) {
|
||||
} catch (e) {
|
||||
logger.error('Could not parse error response as JSON:', e)
|
||||
|
||||
// Try to get the response text for more context
|
||||
try {
|
||||
const text = await response.text()
|
||||
logger.error('Response text:', text)
|
||||
|
||||
120
apps/sim/app/api/tools/confluence/search-in-space/route.ts
Normal file
120
apps/sim/app/api/tools/confluence/search-in-space/route.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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('ConfluenceSearchInSpaceAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Search for content within a specific Confluence space using CQL.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
spaceKey,
|
||||
query,
|
||||
cloudId: providedCloudId,
|
||||
limit = 25,
|
||||
contentType,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!spaceKey) {
|
||||
return NextResponse.json({ error: 'Space key is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const spaceKeyValidation = validateAlphanumericId(spaceKey, 'spaceKey', 255)
|
||||
if (!spaceKeyValidation.isValid) {
|
||||
return NextResponse.json({ error: spaceKeyValidation.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 escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
|
||||
|
||||
let cql = `space = "${escapeCqlValue(spaceKey)}"`
|
||||
|
||||
if (query) {
|
||||
cql += ` AND text ~ "${escapeCqlValue(query)}"`
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
cql += ` AND type = "${escapeCqlValue(contentType)}"`
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
cql,
|
||||
limit: String(Math.min(limit, 250)),
|
||||
})
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/search?${searchParams.toString()}`
|
||||
|
||||
logger.info(`Searching in space ${spaceKey} with CQL: ${cql}`)
|
||||
|
||||
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 search in space (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const results = (data.results || []).map((result: any) => ({
|
||||
id: result.content?.id ?? result.id,
|
||||
title: result.content?.title ?? result.title,
|
||||
type: result.content?.type ?? result.type,
|
||||
status: result.content?.status ?? null,
|
||||
url: result.url ?? result._links?.webui ?? '',
|
||||
excerpt: result.excerpt ?? '',
|
||||
lastModified: result.lastModified ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
results,
|
||||
spaceKey,
|
||||
totalSize: data.totalSize ?? results.length,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error searching in space:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,10 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
cql: `text ~ "${query}"`,
|
||||
cql: `text ~ "${escapeCqlValue(query)}"`,
|
||||
limit: limit.toString(),
|
||||
})
|
||||
|
||||
@@ -70,13 +72,27 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const results = (data.results || []).map((result: any) => ({
|
||||
id: result.content?.id || result.id,
|
||||
title: result.content?.title || result.title,
|
||||
type: result.content?.type || result.type,
|
||||
url: result.url || result._links?.webui || '',
|
||||
excerpt: result.excerpt || '',
|
||||
}))
|
||||
const results = (data.results || []).map((result: any) => {
|
||||
const spaceData = result.resultGlobalContainer || result.content?.space
|
||||
return {
|
||||
id: result.content?.id || result.id,
|
||||
title: result.content?.title || result.title,
|
||||
type: result.content?.type || result.type,
|
||||
url: result.url || result._links?.webui || '',
|
||||
excerpt: result.excerpt || '',
|
||||
status: result.content?.status ?? null,
|
||||
spaceKey: result.resultGlobalContainer?.key ?? result.content?.space?.key ?? null,
|
||||
space: spaceData
|
||||
? {
|
||||
id: spaceData.id ?? null,
|
||||
key: spaceData.key ?? null,
|
||||
name: spaceData.name ?? spaceData.title ?? null,
|
||||
}
|
||||
: null,
|
||||
lastModified: result.lastModified ?? result.content?.history?.lastUpdated?.when ?? null,
|
||||
entityType: result.entityType ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({ results })
|
||||
} catch (error) {
|
||||
|
||||
124
apps/sim/app/api/tools/confluence/space-blogposts/route.ts
Normal file
124
apps/sim/app/api/tools/confluence/space-blogposts/route.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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('ConfluenceSpaceBlogPostsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* List all blog posts in a specific Confluence space.
|
||||
* Uses GET /wiki/api/v2/spaces/{id}/blogposts
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
spaceId,
|
||||
cloudId: providedCloudId,
|
||||
limit = 25,
|
||||
status,
|
||||
bodyFormat,
|
||||
cursor,
|
||||
} = body
|
||||
|
||||
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(limit, 250)))
|
||||
|
||||
if (status) {
|
||||
queryParams.append('status', status)
|
||||
}
|
||||
|
||||
if (bodyFormat) {
|
||||
queryParams.append('body-format', bodyFormat)
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/blogposts?${queryParams.toString()}`
|
||||
|
||||
logger.info(`Fetching blog posts in space ${spaceId}`)
|
||||
|
||||
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 blog posts in space (${response.status})`
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const blogPosts = (data.results || []).map((post: any) => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
status: post.status ?? null,
|
||||
spaceId: post.spaceId ?? null,
|
||||
authorId: post.authorId ?? null,
|
||||
createdAt: post.createdAt ?? null,
|
||||
version: post.version ?? null,
|
||||
body: post.body ?? null,
|
||||
webUrl: post._links?.webui ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
blogPosts,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing blog posts in space:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
125
apps/sim/app/api/tools/confluence/space-pages/route.ts
Normal file
125
apps/sim/app/api/tools/confluence/space-pages/route.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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('ConfluenceSpacePagesAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* List all pages in a specific Confluence space.
|
||||
* Uses GET /wiki/api/v2/spaces/{id}/pages
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
spaceId,
|
||||
cloudId: providedCloudId,
|
||||
limit = 50,
|
||||
status,
|
||||
bodyFormat,
|
||||
cursor,
|
||||
} = body
|
||||
|
||||
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(limit, 250)))
|
||||
|
||||
if (status) {
|
||||
queryParams.append('status', status)
|
||||
}
|
||||
|
||||
if (bodyFormat) {
|
||||
queryParams.append('body-format', bodyFormat)
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/pages?${queryParams.toString()}`
|
||||
|
||||
logger.info(`Fetching pages in space ${spaceId}`)
|
||||
|
||||
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 pages in space (${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,
|
||||
body: page.body ?? null,
|
||||
webUrl: page._links?.webui ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
pages,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing pages in space:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = searchParams.get('accessToken')
|
||||
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 })
|
||||
@@ -37,7 +38,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=${limit}`
|
||||
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?${queryParams.toString()}`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -67,9 +73,18 @@ export async function GET(request: NextRequest) {
|
||||
key: space.key,
|
||||
type: space.type,
|
||||
status: space.status,
|
||||
authorId: space.authorId ?? null,
|
||||
createdAt: space.createdAt ?? null,
|
||||
homepageId: space.homepageId ?? null,
|
||||
description: space.description ?? null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ spaces })
|
||||
return NextResponse.json({
|
||||
spaces,
|
||||
nextCursor: data._links?.next
|
||||
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error listing Confluence spaces:', error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { GithubIcon } from '@/components/icons'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface ChatHeaderProps {
|
||||
chatConfig: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import Image from 'next/image'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { inter } from '@/app/_styles/fonts/inter/inter'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export function PoweredBySim() {
|
||||
const brandConfig = useBrandConfig()
|
||||
|
||||
@@ -2,9 +2,12 @@ import type { Metadata, Viewport } from 'next'
|
||||
import Script from 'next/script'
|
||||
import { PublicEnvScript } from 'next-runtime-env'
|
||||
import { BrandedLayout } from '@/components/branded-layout'
|
||||
import { generateThemeCSS } from '@/lib/branding/inject-theme'
|
||||
import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata'
|
||||
import { PostHogProvider } from '@/app/_shell/providers/posthog-provider'
|
||||
import {
|
||||
generateBrandedMetadata,
|
||||
generateStructuredData,
|
||||
generateThemeCSS,
|
||||
} from '@/ee/whitelabeling'
|
||||
import '@/app/_styles/globals.css'
|
||||
import { OneDollarStats } from '@/components/analytics/onedollarstats'
|
||||
import { isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/feature-flags'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
const brand = getBrandConfig()
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
import type { ResumeStatus } from '@/executor/types'
|
||||
|
||||
interface ResumeLinks {
|
||||
|
||||
@@ -74,6 +74,12 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'write:label:confluence': 'Add and remove labels',
|
||||
'search:confluence': 'Search Confluence content',
|
||||
'readonly:content.attachment:confluence': 'View attachments',
|
||||
'read:blogpost:confluence': 'View Confluence blog posts',
|
||||
'write:blogpost:confluence': 'Create and update Confluence blog posts',
|
||||
'read:content.property:confluence': 'View properties on Confluence content',
|
||||
'write:content.property:confluence': 'Create and manage content properties',
|
||||
'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)',
|
||||
'read:content.metadata:confluence': 'View content metadata (required for ancestors)',
|
||||
'read:me': 'Read profile information',
|
||||
'database.read': 'Read database',
|
||||
'database.write': 'Write to database',
|
||||
@@ -358,6 +364,7 @@ export function OAuthRequiredModal({
|
||||
logger.info('Linking OAuth2:', {
|
||||
providerId,
|
||||
requiredScopes,
|
||||
hasNewScopes: newScopes.length > 0,
|
||||
})
|
||||
|
||||
if (providerId === 'trello') {
|
||||
|
||||
@@ -100,7 +100,7 @@ const BlockRow = memo(function BlockRow({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<div
|
||||
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
|
||||
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||
@@ -276,7 +276,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<div
|
||||
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
|
||||
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ background: bgColor }}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
import { Input, Skeleton } from '@/components/ui'
|
||||
import { signOut, useSession } from '@/lib/auth/auth-client'
|
||||
import { ANONYMOUS_USER_ID } from '@/lib/auth/constants'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/hooks/use-profile-picture-upload'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
|
||||
import { useUpdateUserProfile, useUserProfile } from '@/hooks/queries/user-profile'
|
||||
import { clearUserData } from '@/stores'
|
||||
|
||||
@@ -397,7 +397,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
|
||||
return () => window.clearInterval(interval)
|
||||
}, [isHovered, pillCount, startAnimationIndex])
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && !subscriptionData) {
|
||||
return (
|
||||
<div className='flex flex-shrink-0 flex-col gap-[8px] border-t px-[13.5px] pt-[8px] pb-[10px]'>
|
||||
<div className='flex h-[18px] items-center justify-between'>
|
||||
|
||||
@@ -75,6 +75,12 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
@@ -334,6 +340,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
ts: { type: 'string', description: 'Timestamp' },
|
||||
pageId: { type: 'string', description: 'Page identifier' },
|
||||
content: { type: 'string', description: 'Page content' },
|
||||
body: { type: 'json', description: 'Page body with storage format' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
url: { type: 'string', description: 'Page or resource URL' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
@@ -371,31 +378,46 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
// Page Operations
|
||||
{ label: 'Read Page', id: 'read' },
|
||||
{ label: 'Create Page', id: 'create' },
|
||||
{ label: 'Update Page', id: 'update' },
|
||||
{ label: 'Delete Page', id: 'delete' },
|
||||
{ label: 'List Pages in Space', id: 'list_pages_in_space' },
|
||||
{ label: 'Get Page Children', id: 'get_page_children' },
|
||||
{ label: 'Get Page Ancestors', id: 'get_page_ancestors' },
|
||||
// Version Operations
|
||||
{ label: 'List Page Versions', id: 'list_page_versions' },
|
||||
{ label: 'Get Page Version', id: 'get_page_version' },
|
||||
// Page Property Operations
|
||||
{ label: 'List Page Properties', id: 'list_page_properties' },
|
||||
{ label: 'Create Page Property', id: 'create_page_property' },
|
||||
// Search Operations
|
||||
{ label: 'Search Content', id: 'search' },
|
||||
{ label: 'Search in Space', id: 'search_in_space' },
|
||||
// Blog Post Operations
|
||||
{ label: 'List Blog Posts', id: 'list_blogposts' },
|
||||
{ label: 'Get Blog Post', id: 'get_blogpost' },
|
||||
{ label: 'Create Blog Post', id: 'create_blogpost' },
|
||||
{ label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' },
|
||||
// Comment Operations
|
||||
{ label: 'Create Comment', id: 'create_comment' },
|
||||
{ label: 'List Comments', id: 'list_comments' },
|
||||
{ label: 'Update Comment', id: 'update_comment' },
|
||||
{ label: 'Delete Comment', id: 'delete_comment' },
|
||||
// Attachment Operations
|
||||
{ label: 'Upload Attachment', id: 'upload_attachment' },
|
||||
{ label: 'List Attachments', id: 'list_attachments' },
|
||||
{ label: 'Delete Attachment', id: 'delete_attachment' },
|
||||
// Label Operations
|
||||
{ label: 'List Labels', id: 'list_labels' },
|
||||
{ label: 'Add Label', id: 'add_label' },
|
||||
// Space Operations
|
||||
{ label: 'Get Space', id: 'get_space' },
|
||||
{ label: 'List Spaces', id: 'list_spaces' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Confluence Account',
|
||||
@@ -424,10 +446,23 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'pageId',
|
||||
title: 'Select Page',
|
||||
@@ -437,6 +472,20 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
placeholder: 'Select Confluence page',
|
||||
dependsOn: ['credential', 'domain'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_pages_in_space',
|
||||
'list_blogposts',
|
||||
'get_blogpost',
|
||||
'list_blogposts_in_space',
|
||||
'search',
|
||||
'search_in_space',
|
||||
'get_space',
|
||||
'list_spaces',
|
||||
],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'manualPageId',
|
||||
@@ -445,6 +494,20 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
canonicalParamId: 'pageId',
|
||||
placeholder: 'Enter Confluence page ID',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_pages_in_space',
|
||||
'list_blogposts',
|
||||
'get_blogpost',
|
||||
'list_blogposts_in_space',
|
||||
'search',
|
||||
'search_in_space',
|
||||
'get_space',
|
||||
'list_spaces',
|
||||
],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'spaceId',
|
||||
@@ -452,21 +515,63 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Confluence space ID',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: ['create', 'get_space'] },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create',
|
||||
'get_space',
|
||||
'list_pages_in_space',
|
||||
'search_in_space',
|
||||
'create_blogpost',
|
||||
'list_blogposts_in_space',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'blogPostId',
|
||||
title: 'Blog Post ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter blog post ID',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'get_blogpost' },
|
||||
},
|
||||
{
|
||||
id: 'versionNumber',
|
||||
title: 'Version Number',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter version number',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'get_page_version' },
|
||||
},
|
||||
{
|
||||
id: 'propertyKey',
|
||||
title: 'Property Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter property key/name',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'create_page_property' },
|
||||
},
|
||||
{
|
||||
id: 'propertyValue',
|
||||
title: 'Property Value',
|
||||
type: 'long-input',
|
||||
placeholder: 'Enter property value (JSON supported)',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'create_page_property' },
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter title for the page',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
placeholder: 'Enter title',
|
||||
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'Content',
|
||||
type: 'long-input',
|
||||
placeholder: 'Enter content for the page',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
placeholder: 'Enter content',
|
||||
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
|
||||
},
|
||||
{
|
||||
id: 'parentId',
|
||||
@@ -481,7 +586,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter search query',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
condition: { field: 'operation', value: ['search', 'search_in_space'] },
|
||||
},
|
||||
{
|
||||
id: 'comment',
|
||||
@@ -545,40 +650,140 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter label name',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: ['add_label', 'remove_label'] },
|
||||
condition: { field: 'operation', value: 'add_label' },
|
||||
},
|
||||
{
|
||||
id: 'labelPrefix',
|
||||
title: 'Label Prefix',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Global (default)', id: 'global' },
|
||||
{ label: 'My', id: 'my' },
|
||||
{ label: 'Team', id: 'team' },
|
||||
{ label: 'System', id: 'system' },
|
||||
],
|
||||
value: () => 'global',
|
||||
condition: { field: 'operation', value: 'add_label' },
|
||||
},
|
||||
{
|
||||
id: 'blogPostStatus',
|
||||
title: 'Status',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Published (current)', id: 'current' },
|
||||
{ label: 'Draft', id: 'draft' },
|
||||
],
|
||||
value: () => 'current',
|
||||
condition: { field: 'operation', value: 'create_blogpost' },
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
title: 'Permanently Delete',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
},
|
||||
{
|
||||
id: 'bodyFormat',
|
||||
title: 'Body Format',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Storage (default)', id: 'storage' },
|
||||
{ label: 'Atlas Doc Format', id: 'atlas_doc_format' },
|
||||
{ label: 'View', id: 'view' },
|
||||
{ label: 'Export View', id: 'export_view' },
|
||||
],
|
||||
value: () => 'storage',
|
||||
condition: { field: 'operation', value: 'list_comments' },
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter maximum number of results (default: 25)',
|
||||
placeholder: 'Enter maximum number of results (default: 50, max: 250)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['search', 'list_comments', 'list_attachments', 'list_spaces'],
|
||||
value: [
|
||||
'search',
|
||||
'search_in_space',
|
||||
'list_comments',
|
||||
'list_attachments',
|
||||
'list_spaces',
|
||||
'list_pages_in_space',
|
||||
'list_blogposts',
|
||||
'list_blogposts_in_space',
|
||||
'get_page_children',
|
||||
'list_page_versions',
|
||||
'list_page_properties',
|
||||
'list_labels',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
title: 'Pagination Cursor',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter cursor from previous response (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_comments',
|
||||
'list_attachments',
|
||||
'list_spaces',
|
||||
'list_pages_in_space',
|
||||
'list_blogposts',
|
||||
'list_blogposts_in_space',
|
||||
'get_page_children',
|
||||
'list_page_versions',
|
||||
'list_page_properties',
|
||||
'list_labels',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
// Page Tools
|
||||
'confluence_retrieve',
|
||||
'confluence_update',
|
||||
'confluence_create_page',
|
||||
'confluence_delete_page',
|
||||
'confluence_list_pages_in_space',
|
||||
'confluence_get_page_children',
|
||||
'confluence_get_page_ancestors',
|
||||
// Version Tools
|
||||
'confluence_list_page_versions',
|
||||
'confluence_get_page_version',
|
||||
// Property Tools
|
||||
'confluence_list_page_properties',
|
||||
'confluence_create_page_property',
|
||||
// Search Tools
|
||||
'confluence_search',
|
||||
'confluence_search_in_space',
|
||||
// Blog Post Tools
|
||||
'confluence_list_blogposts',
|
||||
'confluence_get_blogpost',
|
||||
'confluence_create_blogpost',
|
||||
'confluence_list_blogposts_in_space',
|
||||
// Comment Tools
|
||||
'confluence_create_comment',
|
||||
'confluence_list_comments',
|
||||
'confluence_update_comment',
|
||||
'confluence_delete_comment',
|
||||
// Attachment Tools
|
||||
'confluence_upload_attachment',
|
||||
'confluence_list_attachments',
|
||||
'confluence_delete_attachment',
|
||||
// Label Tools
|
||||
'confluence_list_labels',
|
||||
'confluence_add_label',
|
||||
// Space Tools
|
||||
'confluence_get_space',
|
||||
'confluence_list_spaces',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
// Page Operations
|
||||
case 'read':
|
||||
return 'confluence_retrieve'
|
||||
case 'create':
|
||||
@@ -587,8 +792,37 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
return 'confluence_update'
|
||||
case 'delete':
|
||||
return 'confluence_delete_page'
|
||||
case 'list_pages_in_space':
|
||||
return 'confluence_list_pages_in_space'
|
||||
case 'get_page_children':
|
||||
return 'confluence_get_page_children'
|
||||
case 'get_page_ancestors':
|
||||
return 'confluence_get_page_ancestors'
|
||||
// Version Operations
|
||||
case 'list_page_versions':
|
||||
return 'confluence_list_page_versions'
|
||||
case 'get_page_version':
|
||||
return 'confluence_get_page_version'
|
||||
// Property Operations
|
||||
case 'list_page_properties':
|
||||
return 'confluence_list_page_properties'
|
||||
case 'create_page_property':
|
||||
return 'confluence_create_page_property'
|
||||
// Search Operations
|
||||
case 'search':
|
||||
return 'confluence_search'
|
||||
case 'search_in_space':
|
||||
return 'confluence_search_in_space'
|
||||
// Blog Post Operations
|
||||
case 'list_blogposts':
|
||||
return 'confluence_list_blogposts'
|
||||
case 'get_blogpost':
|
||||
return 'confluence_get_blogpost'
|
||||
case 'create_blogpost':
|
||||
return 'confluence_create_blogpost'
|
||||
case 'list_blogposts_in_space':
|
||||
return 'confluence_list_blogposts_in_space'
|
||||
// Comment Operations
|
||||
case 'create_comment':
|
||||
return 'confluence_create_comment'
|
||||
case 'list_comments':
|
||||
@@ -597,14 +831,19 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
return 'confluence_update_comment'
|
||||
case 'delete_comment':
|
||||
return 'confluence_delete_comment'
|
||||
// Attachment Operations
|
||||
case 'upload_attachment':
|
||||
return 'confluence_upload_attachment'
|
||||
case 'list_attachments':
|
||||
return 'confluence_list_attachments'
|
||||
case 'delete_attachment':
|
||||
return 'confluence_delete_attachment'
|
||||
// Label Operations
|
||||
case 'list_labels':
|
||||
return 'confluence_list_labels'
|
||||
case 'add_label':
|
||||
return 'confluence_add_label'
|
||||
// Space Operations
|
||||
case 'get_space':
|
||||
return 'confluence_get_space'
|
||||
case 'list_spaces':
|
||||
@@ -624,6 +863,15 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
attachmentFile,
|
||||
attachmentFileName,
|
||||
attachmentComment,
|
||||
blogPostId,
|
||||
versionNumber,
|
||||
propertyKey,
|
||||
propertyValue,
|
||||
labelPrefix,
|
||||
blogPostStatus,
|
||||
purge,
|
||||
bodyFormat,
|
||||
cursor,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
@@ -638,9 +886,23 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
'list_attachments',
|
||||
'list_labels',
|
||||
'upload_attachment',
|
||||
'add_label',
|
||||
'get_page_children',
|
||||
'get_page_ancestors',
|
||||
'list_page_versions',
|
||||
'get_page_version',
|
||||
'list_page_properties',
|
||||
'create_page_property',
|
||||
]
|
||||
|
||||
const requiresSpaceId = ['create', 'get_space']
|
||||
const requiresSpaceId = [
|
||||
'create',
|
||||
'get_space',
|
||||
'list_pages_in_space',
|
||||
'search_in_space',
|
||||
'create_blogpost',
|
||||
'list_blogposts_in_space',
|
||||
]
|
||||
|
||||
if (requiresPageId.includes(operation) && !effectivePageId) {
|
||||
throw new Error('Page ID is required. Please select a page or enter a page ID manually.')
|
||||
@@ -650,6 +912,91 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
throw new Error('Space ID is required for this operation.')
|
||||
}
|
||||
|
||||
if (operation === 'get_blogpost' && !blogPostId) {
|
||||
throw new Error('Blog Post ID is required for this operation.')
|
||||
}
|
||||
|
||||
if (operation === 'get_page_version' && !versionNumber) {
|
||||
throw new Error('Version number is required for this operation.')
|
||||
}
|
||||
|
||||
if (operation === 'add_label') {
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
operation,
|
||||
prefix: labelPrefix || 'global',
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'create_blogpost') {
|
||||
return {
|
||||
credential,
|
||||
operation,
|
||||
status: blogPostStatus || 'current',
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
operation,
|
||||
purge: purge || false,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'list_comments') {
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
operation,
|
||||
bodyFormat: bodyFormat || 'storage',
|
||||
cursor: cursor || undefined,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
// Operations that support cursor pagination
|
||||
const supportsCursor = [
|
||||
'list_attachments',
|
||||
'list_spaces',
|
||||
'list_pages_in_space',
|
||||
'list_blogposts',
|
||||
'list_blogposts_in_space',
|
||||
'get_page_children',
|
||||
'list_page_versions',
|
||||
'list_page_properties',
|
||||
'list_labels',
|
||||
]
|
||||
|
||||
if (supportsCursor.includes(operation) && cursor) {
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId || undefined,
|
||||
operation,
|
||||
cursor,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'create_page_property') {
|
||||
if (!propertyKey) {
|
||||
throw new Error('Property key is required for this operation.')
|
||||
}
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId,
|
||||
operation,
|
||||
key: propertyKey,
|
||||
value: propertyValue,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'upload_attachment') {
|
||||
const fileInput = attachmentFileUpload || attachmentFileReference || attachmentFile
|
||||
const normalizedFile = normalizeFileInput(fileInput, { single: true })
|
||||
@@ -670,6 +1017,8 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
return {
|
||||
credential,
|
||||
pageId: effectivePageId || undefined,
|
||||
blogPostId: blogPostId || undefined,
|
||||
versionNumber: versionNumber ? Number.parseInt(String(versionNumber), 10) : undefined,
|
||||
operation,
|
||||
...rest,
|
||||
}
|
||||
@@ -683,8 +1032,12 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
pageId: { type: 'string', description: 'Page identifier' },
|
||||
manualPageId: { type: 'string', description: 'Manual page identifier' },
|
||||
spaceId: { type: 'string', description: 'Space identifier' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
content: { type: 'string', description: 'Page content' },
|
||||
blogPostId: { type: 'string', description: 'Blog post identifier' },
|
||||
versionNumber: { type: 'number', description: 'Page version number' },
|
||||
propertyKey: { type: 'string', description: 'Property key/name' },
|
||||
propertyValue: { type: 'json', description: 'Property value (JSON)' },
|
||||
title: { type: 'string', description: 'Page or blog post title' },
|
||||
content: { type: 'string', description: 'Page or blog post content' },
|
||||
parentId: { type: 'string', description: 'Parent page identifier' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
comment: { type: 'string', description: 'Comment text' },
|
||||
@@ -696,6 +1049,62 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
|
||||
attachmentFileName: { type: 'string', description: 'Custom file name for attachment' },
|
||||
attachmentComment: { type: 'string', description: 'Comment for the attachment' },
|
||||
labelName: { type: 'string', description: 'Label name' },
|
||||
labelPrefix: { type: 'string', description: 'Label prefix (global, my, team, system)' },
|
||||
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' },
|
||||
limit: { type: 'number', description: 'Maximum number of results' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor from previous response' },
|
||||
},
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp' },
|
||||
pageId: { type: 'string', description: 'Page identifier' },
|
||||
content: { type: 'string', description: 'Page content' },
|
||||
body: { type: 'json', description: 'Page body with storage format' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
url: { type: 'string', description: 'Page or resource URL' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
deleted: { type: 'boolean', description: 'Deletion status' },
|
||||
added: { type: 'boolean', description: 'Addition status' },
|
||||
removed: { type: 'boolean', description: 'Removal status' },
|
||||
updated: { type: 'boolean', description: 'Update status' },
|
||||
// Search & List Results
|
||||
results: { type: 'array', description: 'Search results' },
|
||||
pages: { type: 'array', description: 'List of pages' },
|
||||
children: { type: 'array', description: 'List of child pages' },
|
||||
ancestors: { type: 'array', description: 'List of ancestor pages' },
|
||||
// Comment Results
|
||||
comments: { type: 'array', description: 'List of comments' },
|
||||
commentId: { type: 'string', description: 'Comment identifier' },
|
||||
// Attachment Results
|
||||
attachments: { type: 'array', description: 'List of attachments' },
|
||||
attachmentId: { type: 'string', description: 'Attachment identifier' },
|
||||
fileSize: { type: 'number', description: 'Attachment file size in bytes' },
|
||||
mediaType: { type: 'string', description: 'Attachment MIME type' },
|
||||
downloadUrl: { type: 'string', description: 'Attachment download URL' },
|
||||
// Label Results
|
||||
labels: { type: 'array', description: 'List of labels' },
|
||||
labelName: { type: 'string', description: 'Label name' },
|
||||
// Space Results
|
||||
spaces: { type: 'array', description: 'List of spaces' },
|
||||
spaceId: { type: 'string', description: 'Space identifier' },
|
||||
name: { type: 'string', description: 'Space name' },
|
||||
key: { type: 'string', description: 'Space key' },
|
||||
type: { type: 'string', description: 'Space or content type' },
|
||||
status: { type: 'string', description: 'Space status' },
|
||||
// Blog Post Results
|
||||
blogPosts: { type: 'array', description: 'List of blog posts' },
|
||||
blogPostId: { type: 'string', description: 'Blog post identifier' },
|
||||
// Version Results
|
||||
versions: { type: 'array', description: 'List of page versions' },
|
||||
version: { type: 'json', description: 'Version information' },
|
||||
versionNumber: { type: 'number', description: 'Version number' },
|
||||
// Property Results
|
||||
properties: { type: 'array', description: 'List of page properties' },
|
||||
propertyId: { type: 'string', description: 'Property identifier' },
|
||||
propertyKey: { type: 'string', description: 'Property key' },
|
||||
propertyValue: { type: 'json', description: 'Property value' },
|
||||
// Pagination
|
||||
nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface BrandedLayoutProps {
|
||||
children: React.ReactNode
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Section, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface OTPVerificationEmailProps {
|
||||
otp: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface ResetPasswordEmailProps {
|
||||
username?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface WelcomeEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles, colors } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface CreditPurchaseEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface EnterpriseSubscriptionEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles, colors, typography } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface FreeTierUpgradeEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles, colors } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface PaymentFailedEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface PlanWelcomeEmailProps {
|
||||
planName: 'Pro' | 'Team'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface UsageThresholdEmailProps {
|
||||
userName?: string
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Text } from '@react-email/components'
|
||||
import { format } from 'date-fns'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface CareersConfirmationEmailProps {
|
||||
name: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Container, Img, Link, Section } from '@react-email/components'
|
||||
import { baseStyles, colors, spacing, typography } from '@/components/emails/_styles'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface EmailFooterProps {
|
||||
baseUrl?: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Body, Container, Head, Html, Img, Preview, Section } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailFooter } from '@/components/emails/components/email-footer'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface EmailLayoutProps {
|
||||
/** Preview text shown in email client list view */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface WorkspaceInvitation {
|
||||
workspaceId: string
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface InvitationEmailProps {
|
||||
inviterName?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
interface PollingGroupInvitationEmailProps {
|
||||
inviterName?: string
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
const logger = createLogger('WorkspaceInvitationEmail')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link, Section, Text } from '@react-email/components'
|
||||
import { baseStyles } from '@/components/emails/_styles'
|
||||
import { EmailLayout } from '@/components/emails/components'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
/**
|
||||
* Serialized rate limit status for email payloads.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
/** Email subject type for all supported email templates */
|
||||
export type EmailSubjectType =
|
||||
|
||||
@@ -27,7 +27,7 @@ under the following terms:
|
||||
3. ENTERPRISE SUBSCRIPTION
|
||||
|
||||
Production deployment of enterprise features requires an active Sim Enterprise
|
||||
subscription. Contact sales@simstudio.ai for licensing information.
|
||||
subscription. Contact sales@sim.ai for licensing information.
|
||||
|
||||
4. DISCLAIMER
|
||||
|
||||
@@ -40,4 +40,4 @@ under the following terms:
|
||||
IN NO EVENT SHALL SIM STUDIO, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
||||
|
||||
For questions about enterprise licensing, contact: sales@simstudio.ai
|
||||
For questions about enterprise licensing, contact: sales@sim.ai
|
||||
|
||||
@@ -7,7 +7,7 @@ for production use.
|
||||
|
||||
- **SSO (Single Sign-On)**: OIDC and SAML authentication integration
|
||||
- **Access Control**: Permission groups for fine-grained user access management
|
||||
- **Credential Sets**: Shared credential pools for email polling workflows
|
||||
- **Whitelabeling**: Custom branding and theming for enterprise deployments
|
||||
|
||||
## Licensing
|
||||
|
||||
|
||||
45
apps/sim/ee/whitelabeling/branding.ts
Normal file
45
apps/sim/ee/whitelabeling/branding.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { type BrandConfig, defaultBrandConfig, type ThemeColors } from '@/lib/branding'
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
|
||||
export type { BrandConfig, ThemeColors }
|
||||
|
||||
const getThemeColors = (): ThemeColors => {
|
||||
return {
|
||||
primaryColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultBrandConfig.theme?.primaryColor,
|
||||
primaryHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') ||
|
||||
defaultBrandConfig.theme?.primaryHoverColor,
|
||||
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultBrandConfig.theme?.accentColor,
|
||||
accentHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultBrandConfig.theme?.accentHoverColor,
|
||||
backgroundColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultBrandConfig.theme?.backgroundColor,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branding configuration from environment variables
|
||||
* Supports runtime configuration via Docker/Kubernetes
|
||||
*/
|
||||
export const getBrandConfig = (): BrandConfig => {
|
||||
return {
|
||||
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultBrandConfig.name,
|
||||
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultBrandConfig.logoUrl,
|
||||
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultBrandConfig.faviconUrl,
|
||||
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultBrandConfig.customCssUrl,
|
||||
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultBrandConfig.supportEmail,
|
||||
documentationUrl:
|
||||
getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultBrandConfig.documentationUrl,
|
||||
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultBrandConfig.termsUrl,
|
||||
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultBrandConfig.privacyUrl,
|
||||
theme: getThemeColors(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to use brand configuration in React components
|
||||
*/
|
||||
export const useBrandConfig = () => {
|
||||
return getBrandConfig()
|
||||
}
|
||||
4
apps/sim/ee/whitelabeling/index.ts
Normal file
4
apps/sim/ee/whitelabeling/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type { BrandConfig, ThemeColors } from './branding'
|
||||
export { getBrandConfig, useBrandConfig } from './branding'
|
||||
export { generateThemeCSS } from './inject-theme'
|
||||
export { generateBrandedMetadata, generateStructuredData } from './metadata'
|
||||
@@ -1,4 +1,6 @@
|
||||
// Helper to detect if background is dark
|
||||
/**
|
||||
* Helper to detect if background is dark
|
||||
*/
|
||||
function isDarkBackground(hexColor: string): boolean {
|
||||
const hex = hexColor.replace('#', '')
|
||||
const r = Number.parseInt(hex.substr(0, 2), 16)
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { getBrandConfig } from '@/lib/branding/branding'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getBrandConfig } from '@/ee/whitelabeling/branding'
|
||||
|
||||
/**
|
||||
* Generate dynamic metadata based on brand configuration
|
||||
@@ -151,7 +151,8 @@ export const auth = betterAuth({
|
||||
create: {
|
||||
before: async (account) => {
|
||||
// Only one credential per (userId, providerId) is allowed
|
||||
// If user reconnects (even with a different external account), replace the existing one
|
||||
// If user reconnects (even with a different external account), delete the old one
|
||||
// and let Better Auth create the new one (returning false breaks account linking flow)
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(
|
||||
eq(schema.account.userId, account.userId),
|
||||
@@ -159,101 +160,59 @@ export const auth = betterAuth({
|
||||
),
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
let scopeToStore = account.scope
|
||||
const modifiedAccount = { ...account }
|
||||
|
||||
if (account.providerId === 'salesforce' && account.accessToken) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://login.salesforce.com/services/oauth2/userinfo',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account.accessToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.profile) {
|
||||
const match = data.profile.match(/^(https:\/\/[^/]+)/)
|
||||
if (match && match[1] !== 'https://login.salesforce.com') {
|
||||
const instanceUrl = match[1]
|
||||
scopeToStore = `__sf_instance__:${instanceUrl} ${account.scope}`
|
||||
}
|
||||
}
|
||||
if (account.providerId === 'salesforce' && account.accessToken) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://login.salesforce.com/services/oauth2/userinfo',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${account.accessToken}`,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch Salesforce instance URL', { error })
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
|
||||
? getMicrosoftRefreshTokenExpiry()
|
||||
: account.refreshTokenExpiresAt
|
||||
|
||||
await db
|
||||
.update(schema.account)
|
||||
.set({
|
||||
accountId: account.accountId,
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
idToken: account.idToken,
|
||||
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
||||
refreshTokenExpiresAt,
|
||||
scope: scopeToStore,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.account.id, existing.id))
|
||||
|
||||
// Sync webhooks for credential sets after reconnecting
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const userMemberships = await db
|
||||
.select({
|
||||
credentialSetId: schema.credentialSetMember.credentialSetId,
|
||||
providerId: schema.credentialSet.providerId,
|
||||
})
|
||||
.from(schema.credentialSetMember)
|
||||
.innerJoin(
|
||||
schema.credentialSet,
|
||||
eq(schema.credentialSetMember.credentialSetId, schema.credentialSet.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.credentialSetMember.userId, account.userId),
|
||||
eq(schema.credentialSetMember.status, 'active')
|
||||
)
|
||||
)
|
||||
|
||||
for (const membership of userMemberships) {
|
||||
if (membership.providerId === account.providerId) {
|
||||
try {
|
||||
await syncAllWebhooksForCredentialSet(membership.credentialSetId, requestId)
|
||||
logger.info(
|
||||
'[account.create.before] Synced webhooks after credential reconnect',
|
||||
{
|
||||
credentialSetId: membership.credentialSetId,
|
||||
providerId: account.providerId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[account.create.before] Failed to sync webhooks after credential reconnect',
|
||||
{
|
||||
credentialSetId: membership.credentialSetId,
|
||||
providerId: account.providerId,
|
||||
error,
|
||||
}
|
||||
)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.profile) {
|
||||
const match = data.profile.match(/^(https:\/\/[^/]+)/)
|
||||
if (match && match[1] !== 'https://login.salesforce.com') {
|
||||
const instanceUrl = match[1]
|
||||
modifiedAccount.scope = `__sf_instance__:${instanceUrl} ${account.scope}`
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch Salesforce instance URL', { error })
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return { data: account }
|
||||
// Handle Microsoft refresh token expiry
|
||||
if (isMicrosoftProvider(account.providerId)) {
|
||||
modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
// Delete the existing account so Better Auth can create the new one
|
||||
// This allows account linking/re-authorization to succeed
|
||||
await db.delete(schema.account).where(eq(schema.account.id, existing.id))
|
||||
|
||||
// Preserve the existing account ID so references (like workspace notifications) continue to work
|
||||
modifiedAccount.id = existing.id
|
||||
|
||||
logger.info('[account.create.before] Deleted existing account for re-authorization', {
|
||||
userId: account.userId,
|
||||
providerId: account.providerId,
|
||||
existingAccountId: existing.id,
|
||||
preservingId: true,
|
||||
})
|
||||
|
||||
// Sync webhooks for credential sets after reconnecting (in after hook)
|
||||
}
|
||||
|
||||
return { data: modifiedAccount }
|
||||
},
|
||||
after: async (account) => {
|
||||
try {
|
||||
@@ -1687,6 +1646,12 @@ export const auth = betterAuth({
|
||||
'search:confluence',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'read:blogpost:confluence',
|
||||
'write:blogpost:confluence',
|
||||
'read:content.property:confluence',
|
||||
'write:content.property:confluence',
|
||||
'read:hierarchical-content:confluence',
|
||||
'read:content.metadata:confluence',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { getEnv } from '@/lib/core/config/env'
|
||||
|
||||
export interface ThemeColors {
|
||||
primaryColor?: string
|
||||
primaryHoverColor?: string
|
||||
accentColor?: string
|
||||
accentHoverColor?: string
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
export interface BrandConfig {
|
||||
name: string
|
||||
logoUrl?: string
|
||||
faviconUrl?: string
|
||||
customCssUrl?: string
|
||||
supportEmail?: string
|
||||
documentationUrl?: string
|
||||
termsUrl?: string
|
||||
privacyUrl?: string
|
||||
theme?: ThemeColors
|
||||
}
|
||||
|
||||
/**
|
||||
* Default brand configuration values
|
||||
*/
|
||||
const defaultConfig: BrandConfig = {
|
||||
name: 'Sim',
|
||||
logoUrl: undefined,
|
||||
faviconUrl: '/favicon/favicon.ico',
|
||||
customCssUrl: undefined,
|
||||
supportEmail: 'help@sim.ai',
|
||||
documentationUrl: undefined,
|
||||
termsUrl: undefined,
|
||||
privacyUrl: undefined,
|
||||
theme: {
|
||||
primaryColor: '#701ffc',
|
||||
primaryHoverColor: '#802fff',
|
||||
accentColor: '#9d54ff',
|
||||
accentHoverColor: '#a66fff',
|
||||
backgroundColor: '#0c0c0c',
|
||||
},
|
||||
}
|
||||
|
||||
const getThemeColors = (): ThemeColors => {
|
||||
return {
|
||||
primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.theme?.primaryColor,
|
||||
primaryHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') || defaultConfig.theme?.primaryHoverColor,
|
||||
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.theme?.accentColor,
|
||||
accentHoverColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultConfig.theme?.accentHoverColor,
|
||||
backgroundColor:
|
||||
getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultConfig.theme?.backgroundColor,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branding configuration from environment variables
|
||||
* Supports runtime configuration via Docker/Kubernetes
|
||||
*/
|
||||
export const getBrandConfig = (): BrandConfig => {
|
||||
return {
|
||||
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultConfig.name,
|
||||
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultConfig.logoUrl,
|
||||
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultConfig.faviconUrl,
|
||||
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultConfig.customCssUrl,
|
||||
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultConfig.supportEmail,
|
||||
documentationUrl: getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultConfig.documentationUrl,
|
||||
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultConfig.termsUrl,
|
||||
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultConfig.privacyUrl,
|
||||
theme: getThemeColors(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to use brand configuration in React components
|
||||
*/
|
||||
export const useBrandConfig = () => {
|
||||
return getBrandConfig()
|
||||
}
|
||||
22
apps/sim/lib/branding/defaults.ts
Normal file
22
apps/sim/lib/branding/defaults.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { BrandConfig } from './types'
|
||||
|
||||
/**
|
||||
* Default brand configuration values
|
||||
*/
|
||||
export const defaultBrandConfig: BrandConfig = {
|
||||
name: 'Sim',
|
||||
logoUrl: undefined,
|
||||
faviconUrl: '/favicon/favicon.ico',
|
||||
customCssUrl: undefined,
|
||||
supportEmail: 'help@sim.ai',
|
||||
documentationUrl: undefined,
|
||||
termsUrl: undefined,
|
||||
privacyUrl: undefined,
|
||||
theme: {
|
||||
primaryColor: '#701ffc',
|
||||
primaryHoverColor: '#802fff',
|
||||
accentColor: '#9d54ff',
|
||||
accentHoverColor: '#a66fff',
|
||||
backgroundColor: '#0c0c0c',
|
||||
},
|
||||
}
|
||||
2
apps/sim/lib/branding/index.ts
Normal file
2
apps/sim/lib/branding/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { defaultBrandConfig } from './defaults'
|
||||
export type { BrandConfig, ThemeColors } from './types'
|
||||
19
apps/sim/lib/branding/types.ts
Normal file
19
apps/sim/lib/branding/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface ThemeColors {
|
||||
primaryColor?: string
|
||||
primaryHoverColor?: string
|
||||
accentColor?: string
|
||||
accentHoverColor?: string
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
||||
export interface BrandConfig {
|
||||
name: string
|
||||
logoUrl?: string
|
||||
faviconUrl?: string
|
||||
customCssUrl?: string
|
||||
supportEmail?: string
|
||||
documentationUrl?: string
|
||||
termsUrl?: string
|
||||
privacyUrl?: string
|
||||
theme?: ThemeColors
|
||||
}
|
||||
123
apps/sim/tools/confluence/add_label.ts
Normal file
123
apps/sim/tools/confluence/add_label.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceAddLabelParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
labelName: string
|
||||
prefix?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceAddLabelResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
labelName: string
|
||||
labelId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceAddLabelTool: ToolConfig<
|
||||
ConfluenceAddLabelParams,
|
||||
ConfluenceAddLabelResponse
|
||||
> = {
|
||||
id: 'confluence_add_label',
|
||||
name: 'Confluence Add Label',
|
||||
description: 'Add a label to a Confluence page for organization and categorization.',
|
||||
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 add the label to',
|
||||
},
|
||||
labelName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the label to add',
|
||||
},
|
||||
prefix: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Label prefix: global (default), my, team, or system',
|
||||
},
|
||||
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: 'POST',
|
||||
headers: (params: ConfluenceAddLabelParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceAddLabelParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
labelName: params.labelName?.trim(),
|
||||
prefix: params.prefix || 'global',
|
||||
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 ?? data.name ?? '',
|
||||
labelId: data.id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: {
|
||||
type: 'string',
|
||||
description: 'Page ID that the label was added to',
|
||||
},
|
||||
labelName: {
|
||||
type: 'string',
|
||||
description: 'Name of the added label',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
description: 'ID of the added label',
|
||||
},
|
||||
},
|
||||
}
|
||||
151
apps/sim/tools/confluence/create_blogpost.ts
Normal file
151
apps/sim/tools/confluence/create_blogpost.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
TIMESTAMP_OUTPUT,
|
||||
VERSION_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceCreateBlogPostParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
spaceId: string
|
||||
title: string
|
||||
content: string
|
||||
status?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceCreateBlogPostResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string
|
||||
authorId: string | null
|
||||
body: Record<string, any> | null
|
||||
version: Record<string, any> | null
|
||||
webUrl: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceCreateBlogPostTool: ToolConfig<
|
||||
ConfluenceCreateBlogPostParams,
|
||||
ConfluenceCreateBlogPostResponse
|
||||
> = {
|
||||
id: 'confluence_create_blogpost',
|
||||
name: 'Confluence Create Blog Post',
|
||||
description: 'Create a new blog post in 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 space to create the blog post in',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the blog post',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Blog post content in Confluence storage format (HTML)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Blog post status: current (default) or draft',
|
||||
},
|
||||
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/blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceCreateBlogPostParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceCreateBlogPostParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
spaceId: params.spaceId?.trim(),
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
status: params.status || 'current',
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
id: data.id ?? '',
|
||||
title: data.title ?? '',
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? '',
|
||||
authorId: data.authorId ?? null,
|
||||
body: data.body ?? null,
|
||||
version: data.version ?? null,
|
||||
webUrl: data.webUrl ?? data._links?.webui ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
id: { type: 'string', description: 'Created blog post ID' },
|
||||
title: { type: 'string', description: 'Blog post title' },
|
||||
status: { type: 'string', description: 'Blog post status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID' },
|
||||
authorId: { type: 'string', description: 'Author account ID', optional: true },
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Blog post body content',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Blog post version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
webUrl: { type: 'string', description: 'URL to view the blog post', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CONTENT_BODY_OUTPUT_PROPERTIES, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceCreatePageParams {
|
||||
@@ -16,6 +17,11 @@ export interface ConfluenceCreatePageResponse {
|
||||
ts: string
|
||||
pageId: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
parentId: string | null
|
||||
body: Record<string, any> | null
|
||||
version: Record<string, any> | null
|
||||
url: string
|
||||
}
|
||||
}
|
||||
@@ -109,8 +115,13 @@ export const confluenceCreatePageTool: ToolConfig<
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id,
|
||||
title: data.title,
|
||||
pageId: data.id ?? '',
|
||||
title: data.title ?? '',
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
parentId: data.parentId ?? null,
|
||||
body: data.body ?? null,
|
||||
version: data.version ?? null,
|
||||
url: data.url || data._links?.webui || '',
|
||||
},
|
||||
}
|
||||
@@ -120,6 +131,21 @@ export const confluenceCreatePageTool: ToolConfig<
|
||||
ts: { type: 'string', description: 'Timestamp of creation' },
|
||||
pageId: { type: 'string', description: 'Created page ID' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
status: { type: 'string', description: 'Page status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
parentId: { type: 'string', description: 'Parent page ID', optional: true },
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Page body content',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Page version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
url: { type: 'string', description: 'Page URL' },
|
||||
},
|
||||
}
|
||||
|
||||
127
apps/sim/tools/confluence/create_page_property.ts
Normal file
127
apps/sim/tools/confluence/create_page_property.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceCreatePagePropertyParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
key: string
|
||||
value: any
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceCreatePagePropertyResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
propertyId: string
|
||||
key: string
|
||||
value: any
|
||||
version: {
|
||||
number: number
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceCreatePagePropertyTool: ToolConfig<
|
||||
ConfluenceCreatePagePropertyParams,
|
||||
ConfluenceCreatePagePropertyResponse
|
||||
> = {
|
||||
id: 'confluence_create_page_property',
|
||||
name: 'Confluence Create Page Property',
|
||||
description: 'Create a new custom property (metadata) on 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: 'The ID of the page to add the property to',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The key/name for the property',
|
||||
},
|
||||
value: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The value for the property (can be any JSON value)',
|
||||
},
|
||||
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: 'POST',
|
||||
headers: (params: ConfluenceCreatePagePropertyParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceCreatePagePropertyParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
key: params.key,
|
||||
value: params.value,
|
||||
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.id ?? '',
|
||||
key: data.key ?? '',
|
||||
value: data.value ?? null,
|
||||
version: data.version ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: { type: 'string', description: 'ID of the page' },
|
||||
propertyId: { type: 'string', description: 'ID of the created property' },
|
||||
key: { type: 'string', description: 'Property key' },
|
||||
value: { type: 'json', description: 'Property value' },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface ConfluenceDeletePageParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
purge?: boolean
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
@@ -22,7 +23,8 @@ export const confluenceDeletePageTool: ToolConfig<
|
||||
> = {
|
||||
id: 'confluence_delete_page',
|
||||
name: 'Confluence Delete Page',
|
||||
description: 'Delete a Confluence page (moves it to trash where it can be restored).',
|
||||
description:
|
||||
'Delete a Confluence page. By default moves to trash; use purge=true to permanently delete.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
@@ -49,6 +51,13 @@ export const confluenceDeletePageTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Confluence page ID to delete',
|
||||
},
|
||||
purge: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'If true, permanently deletes the page instead of moving to trash (default: false)',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -72,6 +81,7 @@ export const confluenceDeletePageTool: ToolConfig<
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId,
|
||||
purge: params.purge || false,
|
||||
cloudId: params.cloudId,
|
||||
}
|
||||
},
|
||||
|
||||
144
apps/sim/tools/confluence/get_blogpost.ts
Normal file
144
apps/sim/tools/confluence/get_blogpost.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
TIMESTAMP_OUTPUT,
|
||||
VERSION_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceGetBlogPostParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
blogPostId: string
|
||||
bodyFormat?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceGetBlogPostResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
version: {
|
||||
number: number
|
||||
message?: string
|
||||
createdAt?: string
|
||||
} | null
|
||||
body: {
|
||||
storage?: { value: string }
|
||||
} | null
|
||||
webUrl: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceGetBlogPostTool: ToolConfig<
|
||||
ConfluenceGetBlogPostParams,
|
||||
ConfluenceGetBlogPostResponse
|
||||
> = {
|
||||
id: 'confluence_get_blogpost',
|
||||
name: 'Confluence Get Blog Post',
|
||||
description: 'Get a specific Confluence blog post by ID, including its content.',
|
||||
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)',
|
||||
},
|
||||
blogPostId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the blog post to retrieve',
|
||||
},
|
||||
bodyFormat: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Format for blog post body: storage, atlas_doc_format, or view',
|
||||
},
|
||||
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/blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetBlogPostParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceGetBlogPostParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
blogPostId: params.blogPostId?.trim(),
|
||||
bodyFormat: params.bodyFormat || 'storage',
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
id: data.id ?? '',
|
||||
title: data.title ?? '',
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
version: data.version ?? null,
|
||||
body: data.body ?? null,
|
||||
webUrl: data.webUrl ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
id: { type: 'string', description: 'Blog post ID' },
|
||||
title: { type: 'string', description: 'Blog post title' },
|
||||
status: { type: 'string', description: 'Blog post status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
authorId: { type: 'string', description: 'Author account ID', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Blog post body content in requested format(s)',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
webUrl: { type: 'string', description: 'URL to view the blog post', optional: true },
|
||||
},
|
||||
}
|
||||
126
apps/sim/tools/confluence/get_page_ancestors.ts
Normal file
126
apps/sim/tools/confluence/get_page_ancestors.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceGetPageAncestorsParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceGetPageAncestorsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
ancestors: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
webUrl: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceGetPageAncestorsTool: ToolConfig<
|
||||
ConfluenceGetPageAncestorsParams,
|
||||
ConfluenceGetPageAncestorsResponse
|
||||
> = {
|
||||
id: 'confluence_get_page_ancestors',
|
||||
name: 'Confluence Get Page Ancestors',
|
||||
description:
|
||||
'Get the ancestor (parent) pages of a specific Confluence page. Returns the full hierarchy from the page up to the root.',
|
||||
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 to get ancestors for',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of ancestors to return (default: 25, max: 250)',
|
||||
},
|
||||
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-ancestors',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageAncestorsParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceGetPageAncestorsParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.pageId ?? '',
|
||||
ancestors: data.ancestors ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: {
|
||||
type: 'string',
|
||||
description: 'ID of the page whose ancestors were retrieved',
|
||||
},
|
||||
ancestors: {
|
||||
type: 'array',
|
||||
description: 'Array of ancestor pages, ordered from direct parent to root',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Ancestor page ID' },
|
||||
title: { type: 'string', description: 'Ancestor page title' },
|
||||
status: { type: 'string', description: 'Page status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
webUrl: { type: 'string', description: 'URL to view the page', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
143
apps/sim/tools/confluence/get_page_children.ts
Normal file
143
apps/sim/tools/confluence/get_page_children.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceGetPageChildrenParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceGetPageChildrenResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
parentId: string
|
||||
children: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
childPosition: number | null
|
||||
webUrl: string | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceGetPageChildrenTool: ToolConfig<
|
||||
ConfluenceGetPageChildrenParams,
|
||||
ConfluenceGetPageChildrenResponse
|
||||
> = {
|
||||
id: 'confluence_get_page_children',
|
||||
name: 'Confluence Get Page Children',
|
||||
description:
|
||||
'Get all child pages of a specific Confluence page. Useful for navigating page hierarchies.',
|
||||
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 parent page to get children from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of child pages to return (default: 50, max: 250)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response to get the next page of results',
|
||||
},
|
||||
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-children',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageChildrenParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceGetPageChildrenParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
limit: params.limit ? Number(params.limit) : 50,
|
||||
cursor: params.cursor,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
parentId: data.parentId ?? '',
|
||||
children: data.children ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
parentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the parent page',
|
||||
},
|
||||
children: {
|
||||
type: 'array',
|
||||
description: 'Array of child pages',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Child page ID' },
|
||||
title: { type: 'string', description: 'Child page title' },
|
||||
status: { type: 'string', description: 'Page status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
childPosition: { type: 'number', description: 'Position among siblings', optional: true },
|
||||
webUrl: { type: 'string', description: 'URL to view the page', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
123
apps/sim/tools/confluence/get_page_version.ts
Normal file
123
apps/sim/tools/confluence/get_page_version.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { DETAILED_VERSION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceGetPageVersionParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
versionNumber: number
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceGetPageVersionResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
version: {
|
||||
number: number
|
||||
message: string | null
|
||||
minorEdit: boolean
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
contentTypeModified: boolean | null
|
||||
collaborators: string[] | null
|
||||
prevVersion: number | null
|
||||
nextVersion: number | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceGetPageVersionTool: ToolConfig<
|
||||
ConfluenceGetPageVersionParams,
|
||||
ConfluenceGetPageVersionResponse
|
||||
> = {
|
||||
id: 'confluence_get_page_version',
|
||||
name: 'Confluence Get Page Version',
|
||||
description: 'Get details about a specific version of 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: 'The ID of the page',
|
||||
},
|
||||
versionNumber: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The version number to retrieve (e.g., 1, 2, 3)',
|
||||
},
|
||||
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-versions',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceGetPageVersionParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceGetPageVersionParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
versionNumber: Number(params.versionNumber),
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.pageId ?? '',
|
||||
version: data.version ?? {
|
||||
number: 0,
|
||||
message: null,
|
||||
minorEdit: false,
|
||||
authorId: null,
|
||||
createdAt: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: { type: 'string', description: 'ID of the page' },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Detailed version information',
|
||||
properties: DETAILED_VERSION_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceGetSpaceParams {
|
||||
@@ -17,6 +18,13 @@ export interface ConfluenceGetSpaceResponse {
|
||||
type: string
|
||||
status: string
|
||||
url: string
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
homepageId: string | null
|
||||
description: {
|
||||
value: string
|
||||
representation: string
|
||||
} | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,17 +103,34 @@ export const confluenceGetSpaceTool: ToolConfig<
|
||||
type: data.type,
|
||||
status: data.status,
|
||||
url: data._links?.webui || '',
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
homepageId: data.homepageId ?? null,
|
||||
description: data.description ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of retrieval' },
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
spaceId: { type: 'string', description: 'Space ID' },
|
||||
name: { type: 'string', description: 'Space name' },
|
||||
key: { type: 'string', description: 'Space key' },
|
||||
type: { type: 'string', description: 'Space type' },
|
||||
status: { type: 'string', description: 'Space status' },
|
||||
url: { type: 'string', description: 'Space URL' },
|
||||
type: { type: 'string', description: 'Space type (global, personal)' },
|
||||
status: { type: 'string', description: 'Space status (current, archived)' },
|
||||
url: { type: 'string', description: 'URL to view the space in Confluence' },
|
||||
authorId: { type: 'string', description: 'Account ID of the space creator', optional: true },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the space was created',
|
||||
optional: true,
|
||||
},
|
||||
homepageId: { type: 'string', description: 'ID of the space homepage', optional: true },
|
||||
description: {
|
||||
type: 'object',
|
||||
description: 'Space description content',
|
||||
properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
import { confluenceAddLabelTool } from '@/tools/confluence/add_label'
|
||||
import { confluenceCreateBlogPostTool } from '@/tools/confluence/create_blogpost'
|
||||
import { confluenceCreateCommentTool } from '@/tools/confluence/create_comment'
|
||||
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 { confluenceDeletePageTool } from '@/tools/confluence/delete_page'
|
||||
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 { confluenceGetSpaceTool } from '@/tools/confluence/get_space'
|
||||
import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments'
|
||||
import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts'
|
||||
import { confluenceListBlogPostsInSpaceTool } from '@/tools/confluence/list_blogposts_in_space'
|
||||
import { confluenceListCommentsTool } from '@/tools/confluence/list_comments'
|
||||
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 { confluenceListSpacesTool } from '@/tools/confluence/list_spaces'
|
||||
import { confluenceRetrieveTool } from '@/tools/confluence/retrieve'
|
||||
import { confluenceSearchTool } from '@/tools/confluence/search'
|
||||
import { confluenceSearchInSpaceTool } from '@/tools/confluence/search_in_space'
|
||||
import {
|
||||
ATTACHMENT_ITEM_PROPERTIES,
|
||||
ATTACHMENT_OUTPUT,
|
||||
ATTACHMENTS_OUTPUT,
|
||||
BODY_FORMAT_PROPERTIES,
|
||||
COMMENT_BODY_OUTPUT_PROPERTIES,
|
||||
COMMENT_ITEM_PROPERTIES,
|
||||
COMMENT_OUTPUT,
|
||||
COMMENTS_OUTPUT,
|
||||
CONTENT_BODY_OUTPUT,
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
DELETED_OUTPUT,
|
||||
DETAILED_VERSION_OUTPUT,
|
||||
DETAILED_VERSION_OUTPUT_PROPERTIES,
|
||||
LABEL_ITEM_PROPERTIES,
|
||||
LABEL_OUTPUT,
|
||||
LABELS_OUTPUT,
|
||||
@@ -46,20 +64,41 @@ import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment'
|
||||
import { confluenceUploadAttachmentTool } from '@/tools/confluence/upload_attachment'
|
||||
|
||||
export {
|
||||
// Tools
|
||||
// Page Tools
|
||||
confluenceRetrieveTool,
|
||||
confluenceUpdateTool,
|
||||
confluenceCreatePageTool,
|
||||
confluenceDeletePageTool,
|
||||
confluenceListPagesInSpaceTool,
|
||||
confluenceGetPageChildrenTool,
|
||||
confluenceGetPageAncestorsTool,
|
||||
// Page Version Tools
|
||||
confluenceListPageVersionsTool,
|
||||
confluenceGetPageVersionTool,
|
||||
// Page Properties Tools
|
||||
confluenceListPagePropertiesTool,
|
||||
confluenceCreatePagePropertyTool,
|
||||
// Blog Post Tools
|
||||
confluenceListBlogPostsTool,
|
||||
confluenceGetBlogPostTool,
|
||||
confluenceCreateBlogPostTool,
|
||||
confluenceListBlogPostsInSpaceTool,
|
||||
// Search Tools
|
||||
confluenceSearchTool,
|
||||
confluenceSearchInSpaceTool,
|
||||
// Comment Tools
|
||||
confluenceCreateCommentTool,
|
||||
confluenceListCommentsTool,
|
||||
confluenceUpdateCommentTool,
|
||||
confluenceDeleteCommentTool,
|
||||
// Attachment Tools
|
||||
confluenceListAttachmentsTool,
|
||||
confluenceDeleteAttachmentTool,
|
||||
confluenceUploadAttachmentTool,
|
||||
// Label Tools
|
||||
confluenceListLabelsTool,
|
||||
confluenceAddLabelTool,
|
||||
// Space Tools
|
||||
confluenceGetSpaceTool,
|
||||
confluenceListSpacesTool,
|
||||
// Item property constants (for use in outputs)
|
||||
@@ -70,7 +109,10 @@ export {
|
||||
SEARCH_RESULT_ITEM_PROPERTIES,
|
||||
SPACE_ITEM_PROPERTIES,
|
||||
VERSION_OUTPUT_PROPERTIES,
|
||||
DETAILED_VERSION_OUTPUT_PROPERTIES,
|
||||
COMMENT_BODY_OUTPUT_PROPERTIES,
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
BODY_FORMAT_PROPERTIES,
|
||||
SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
|
||||
SEARCH_RESULT_SPACE_PROPERTIES,
|
||||
PAGINATION_LINKS_PROPERTIES,
|
||||
@@ -79,6 +121,8 @@ export {
|
||||
ATTACHMENTS_OUTPUT,
|
||||
COMMENT_OUTPUT,
|
||||
COMMENTS_OUTPUT,
|
||||
CONTENT_BODY_OUTPUT,
|
||||
DETAILED_VERSION_OUTPUT,
|
||||
LABEL_OUTPUT,
|
||||
LABELS_OUTPUT,
|
||||
PAGE_OUTPUT,
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ConfluenceListAttachmentsParams {
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ export interface ConfluenceListAttachmentsResponse {
|
||||
mediaType: string
|
||||
downloadUrl: string
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +62,13 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of attachments to return (default: 25)',
|
||||
description: 'Maximum number of attachments to return (default: 50, max: 250)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
@@ -77,8 +85,11 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId,
|
||||
limit: String(params.limit || 25),
|
||||
limit: String(params.limit || 50),
|
||||
})
|
||||
if (params.cursor) {
|
||||
query.set('cursor', params.cursor)
|
||||
}
|
||||
if (params.cloudId) {
|
||||
query.set('cloudId', params.cloudId)
|
||||
}
|
||||
@@ -91,15 +102,6 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params: ConfluenceListAttachmentsParams) => {
|
||||
return {
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
cloudId: params.cloudId,
|
||||
pageId: params.pageId,
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
@@ -109,6 +111,7 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
attachments: data.attachments || [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -116,5 +119,10 @@ export const confluenceListAttachmentsTool: ToolConfig<
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
attachments: ATTACHMENTS_OUTPUT,
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
167
apps/sim/tools/confluence/list_blogposts.ts
Normal file
167
apps/sim/tools/confluence/list_blogposts.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceListBlogPostsParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
limit?: number
|
||||
status?: string
|
||||
sort?: string
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListBlogPostsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
blogPosts: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
version: {
|
||||
number: number
|
||||
message?: string
|
||||
createdAt?: string
|
||||
} | null
|
||||
webUrl: string | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceListBlogPostsTool: ToolConfig<
|
||||
ConfluenceListBlogPostsParams,
|
||||
ConfluenceListBlogPostsResponse
|
||||
> = {
|
||||
id: 'confluence_list_blogposts',
|
||||
name: 'Confluence List Blog Posts',
|
||||
description: 'List all blog posts across all accessible Confluence spaces.',
|
||||
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)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of blog posts to return (default: 25, max: 250)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by status: current, archived, trashed, or draft',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort order: created-date, -created-date, modified-date, -modified-date, title, -title',
|
||||
},
|
||||
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: ConfluenceListBlogPostsParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
limit: String(params.limit || 25),
|
||||
})
|
||||
if (params.status) {
|
||||
query.set('status', params.status)
|
||||
}
|
||||
if (params.sort) {
|
||||
query.set('sort', params.sort)
|
||||
}
|
||||
if (params.cursor) {
|
||||
query.set('cursor', params.cursor)
|
||||
}
|
||||
if (params.cloudId) {
|
||||
query.set('cloudId', params.cloudId)
|
||||
}
|
||||
return `/api/tools/confluence/blogposts?${query.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: ConfluenceListBlogPostsParams) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
blogPosts: data.blogPosts ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
blogPosts: {
|
||||
type: 'array',
|
||||
description: 'Array of blog posts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Blog post ID' },
|
||||
title: { type: 'string', description: 'Blog post title' },
|
||||
status: { type: 'string', description: 'Blog post status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
authorId: { type: 'string', description: 'Author account ID', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
webUrl: { type: 'string', description: 'URL to view the blog post', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
178
apps/sim/tools/confluence/list_blogposts_in_space.ts
Normal file
178
apps/sim/tools/confluence/list_blogposts_in_space.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
TIMESTAMP_OUTPUT,
|
||||
VERSION_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceListBlogPostsInSpaceParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
spaceId: string
|
||||
limit?: number
|
||||
status?: string
|
||||
bodyFormat?: string
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListBlogPostsInSpaceResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
blogPosts: Array<{
|
||||
id: string
|
||||
title: string
|
||||
status: string | null
|
||||
spaceId: string | null
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
version: {
|
||||
number: number
|
||||
message?: string
|
||||
createdAt?: string
|
||||
} | null
|
||||
body: {
|
||||
storage?: { value: string }
|
||||
} | null
|
||||
webUrl: string | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceListBlogPostsInSpaceTool: ToolConfig<
|
||||
ConfluenceListBlogPostsInSpaceParams,
|
||||
ConfluenceListBlogPostsInSpaceResponse
|
||||
> = {
|
||||
id: 'confluence_list_blogposts_in_space',
|
||||
name: 'Confluence List Blog Posts in Space',
|
||||
description: 'List all blog posts within a specific 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 blog posts from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of blog posts to return (default: 25, max: 250)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by status: current, archived, trashed, or draft',
|
||||
},
|
||||
bodyFormat: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Format for blog post body: storage, atlas_doc_format, or view',
|
||||
},
|
||||
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: () => '/api/tools/confluence/space-blogposts',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListBlogPostsInSpaceParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceListBlogPostsInSpaceParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
spaceId: params.spaceId?.trim(),
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
status: params.status,
|
||||
bodyFormat: params.bodyFormat,
|
||||
cursor: params.cursor,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
blogPosts: data.blogPosts ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
blogPosts: {
|
||||
type: 'array',
|
||||
description: 'Array of blog posts in the space',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Blog post ID' },
|
||||
title: { type: 'string', description: 'Blog post title' },
|
||||
status: { type: 'string', description: 'Blog post status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
authorId: { type: 'string', description: 'Author account ID', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Blog post body content',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
webUrl: { type: 'string', description: 'URL to view the blog post', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -6,6 +6,8 @@ export interface ConfluenceListCommentsParams {
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
bodyFormat?: string
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
@@ -19,6 +21,7 @@ export interface ConfluenceListCommentsResponse {
|
||||
createdAt: string
|
||||
authorId: string
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,19 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of comments to return (default: 25)',
|
||||
},
|
||||
bodyFormat: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Format for the comment body: storage, atlas_doc_format, view, or export_view (default: storage)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
@@ -78,6 +94,12 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
pageId: params.pageId,
|
||||
limit: String(params.limit || 25),
|
||||
})
|
||||
if (params.bodyFormat) {
|
||||
query.set('bodyFormat', params.bodyFormat)
|
||||
}
|
||||
if (params.cursor) {
|
||||
query.set('cursor', params.cursor)
|
||||
}
|
||||
if (params.cloudId) {
|
||||
query.set('cloudId', params.cloudId)
|
||||
}
|
||||
@@ -90,15 +112,6 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params: ConfluenceListCommentsParams) => {
|
||||
return {
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
cloudId: params.cloudId,
|
||||
pageId: params.pageId,
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
@@ -108,6 +121,7 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
comments: data.comments || [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -115,5 +129,10 @@ export const confluenceListCommentsTool: ToolConfig<
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
comments: COMMENTS_OUTPUT,
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface ConfluenceListLabelsParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
@@ -17,6 +19,7 @@ export interface ConfluenceListLabelsResponse {
|
||||
name: string
|
||||
prefix: string
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +56,18 @@ export const confluenceListLabelsTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Confluence page ID 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,
|
||||
@@ -68,7 +83,11 @@ export const confluenceListLabelsTool: ToolConfig<
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId,
|
||||
limit: String(params.limit || 25),
|
||||
})
|
||||
if (params.cursor) {
|
||||
query.set('cursor', params.cursor)
|
||||
}
|
||||
if (params.cloudId) {
|
||||
query.set('cloudId', params.cloudId)
|
||||
}
|
||||
@@ -90,6 +109,7 @@ export const confluenceListLabelsTool: ToolConfig<
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
labels: data.labels || [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -104,5 +124,10 @@ export const confluenceListLabelsTool: ToolConfig<
|
||||
properties: LABEL_ITEM_PROPERTIES,
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
149
apps/sim/tools/confluence/list_page_properties.ts
Normal file
149
apps/sim/tools/confluence/list_page_properties.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceListPagePropertiesParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListPagePropertiesResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
properties: Array<{
|
||||
id: string
|
||||
key: string
|
||||
value: any
|
||||
version: {
|
||||
number: number
|
||||
message?: string
|
||||
createdAt?: string
|
||||
} | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceListPagePropertiesTool: ToolConfig<
|
||||
ConfluenceListPagePropertiesParams,
|
||||
ConfluenceListPagePropertiesResponse
|
||||
> = {
|
||||
id: 'confluence_list_page_properties',
|
||||
name: 'Confluence List Page Properties',
|
||||
description: 'List all custom properties (metadata) attached to 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: 'The ID of the page to list properties from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of properties 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: ConfluenceListPagePropertiesParams) => {
|
||||
const query = new URLSearchParams({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId,
|
||||
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/page-properties?${query.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: ConfluenceListPagePropertiesParams) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.pageId ?? '',
|
||||
properties: data.properties ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: { type: 'string', description: 'ID of the page' },
|
||||
properties: {
|
||||
type: 'array',
|
||||
description: 'Array of content properties',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Property ID' },
|
||||
key: { type: 'string', description: 'Property key' },
|
||||
value: { type: 'json', description: 'Property value (can be any JSON)' },
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
131
apps/sim/tools/confluence/list_page_versions.ts
Normal file
131
apps/sim/tools/confluence/list_page_versions.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceListPageVersionsParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListPageVersionsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
versions: Array<{
|
||||
number: number
|
||||
message: string | null
|
||||
minorEdit: boolean
|
||||
authorId: string | null
|
||||
createdAt: string | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceListPageVersionsTool: ToolConfig<
|
||||
ConfluenceListPageVersionsParams,
|
||||
ConfluenceListPageVersionsResponse
|
||||
> = {
|
||||
id: 'confluence_list_page_versions',
|
||||
name: 'Confluence List Page Versions',
|
||||
description: 'List all versions (revision history) of 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: 'The ID of the page to get versions for',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of versions 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: () => '/api/tools/confluence/page-versions',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListPageVersionsParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceListPageVersionsParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
pageId: params.pageId?.trim(),
|
||||
limit: params.limit ? Number(params.limit) : 50,
|
||||
cursor: params.cursor,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.pageId ?? '',
|
||||
versions: data.versions ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: { type: 'string', description: 'ID of the page' },
|
||||
versions: {
|
||||
type: 'array',
|
||||
description: 'Array of page versions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
174
apps/sim/tools/confluence/list_pages_in_space.ts
Normal file
174
apps/sim/tools/confluence/list_pages_in_space.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
PAGE_ITEM_PROPERTIES,
|
||||
TIMESTAMP_OUTPUT,
|
||||
} from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceListPagesInSpaceParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
spaceId: string
|
||||
limit?: number
|
||||
status?: string
|
||||
bodyFormat?: string
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListPagesInSpaceResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: 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
|
||||
body: {
|
||||
storage?: { value: string }
|
||||
} | null
|
||||
webUrl: string | null
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceListPagesInSpaceTool: ToolConfig<
|
||||
ConfluenceListPagesInSpaceParams,
|
||||
ConfluenceListPagesInSpaceResponse
|
||||
> = {
|
||||
id: 'confluence_list_pages_in_space',
|
||||
name: 'Confluence List Pages in Space',
|
||||
description:
|
||||
'List all pages within a specific Confluence space. Supports pagination and filtering by status.',
|
||||
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 pages from',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of pages to return (default: 50, max: 250)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter pages by status: current, archived, trashed, or draft',
|
||||
},
|
||||
bodyFormat: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Format for page body content: storage, atlas_doc_format, or view. If not specified, body is not included.',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response to get the next page of results',
|
||||
},
|
||||
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/space-pages',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceListPagesInSpaceParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceListPagesInSpaceParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
spaceId: params.spaceId?.trim(),
|
||||
limit: params.limit ? Number(params.limit) : 50,
|
||||
status: params.status,
|
||||
bodyFormat: params.bodyFormat,
|
||||
cursor: params.cursor,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pages: data.pages ?? [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pages: {
|
||||
type: 'array',
|
||||
description: 'Array of pages in the space',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...PAGE_ITEM_PROPERTIES,
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Page body content (if bodyFormat was specified)',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
webUrl: {
|
||||
type: 'string',
|
||||
description: 'URL to view the page in Confluence',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export interface ConfluenceListSpacesParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@ export interface ConfluenceListSpacesResponse {
|
||||
type: string
|
||||
status: string
|
||||
}>
|
||||
nextCursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +55,13 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of spaces to return (default: 25)',
|
||||
description: 'Maximum number of spaces to return (default: 25, max: 250)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
@@ -71,6 +79,9 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
accessToken: params.accessToken,
|
||||
limit: String(params.limit || 25),
|
||||
})
|
||||
if (params.cursor) {
|
||||
query.set('cursor', params.cursor)
|
||||
}
|
||||
if (params.cloudId) {
|
||||
query.set('cloudId', params.cloudId)
|
||||
}
|
||||
@@ -83,14 +94,6 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params: ConfluenceListSpacesParams) => {
|
||||
return {
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
cloudId: params.cloudId,
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
@@ -100,6 +103,7 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
spaces: data.spaces || [],
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -107,5 +111,10 @@ export const confluenceListSpacesTool: ToolConfig<
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
spaces: SPACES_OUTPUT,
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for fetching the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { ConfluenceRetrieveParams, ConfluenceRetrieveResponse } from '@/tools/confluence/types'
|
||||
import {
|
||||
BODY_FORMAT_PROPERTIES,
|
||||
TIMESTAMP_OUTPUT,
|
||||
VERSION_OUTPUT_PROPERTIES,
|
||||
} from '@/tools/confluence/types'
|
||||
import { transformPageData } from '@/tools/confluence/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
@@ -71,9 +76,42 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of retrieval' },
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
content: { type: 'string', description: 'Page content with HTML tags stripped' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
content: { type: 'string', description: 'Page content with HTML tags stripped' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Page status (current, archived, trashed, draft)',
|
||||
optional: true,
|
||||
},
|
||||
spaceId: { type: 'string', description: 'ID of the space containing the page', optional: true },
|
||||
parentId: { type: 'string', description: 'ID of the parent page', optional: true },
|
||||
authorId: { type: 'string', description: 'Account ID of the page author', optional: true },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the page was created',
|
||||
optional: true,
|
||||
},
|
||||
url: { type: 'string', description: 'URL to view the page in Confluence', optional: true },
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Raw page body content in storage format',
|
||||
properties: {
|
||||
storage: {
|
||||
type: 'object',
|
||||
description: 'Body in storage format (Confluence markup)',
|
||||
properties: BODY_FORMAT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Page version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
144
apps/sim/tools/confluence/search_in_space.ts
Normal file
144
apps/sim/tools/confluence/search_in_space.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { SEARCH_RESULT_ITEM_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface ConfluenceSearchInSpaceParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
spaceKey: string
|
||||
query?: string
|
||||
contentType?: string
|
||||
limit?: number
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceSearchInSpaceResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
ts: string
|
||||
spaceKey: string
|
||||
totalSize: number
|
||||
results: Array<{
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
status: string | null
|
||||
url: string
|
||||
excerpt: string
|
||||
lastModified: string | null
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export const confluenceSearchInSpaceTool: ToolConfig<
|
||||
ConfluenceSearchInSpaceParams,
|
||||
ConfluenceSearchInSpaceResponse
|
||||
> = {
|
||||
id: 'confluence_search_in_space',
|
||||
name: 'Confluence Search in Space',
|
||||
description:
|
||||
'Search for content within a specific Confluence space. Optionally filter by text query and content type.',
|
||||
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)',
|
||||
},
|
||||
spaceKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The key of the Confluence space to search in (e.g., "ENG", "HR")',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Text search query. If not provided, returns all content in the space.',
|
||||
},
|
||||
contentType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by content type: page, blogpost, attachment, or comment',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return (default: 25, max: 250)',
|
||||
},
|
||||
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/search-in-space',
|
||||
method: 'POST',
|
||||
headers: (params: ConfluenceSearchInSpaceParams) => ({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
body: (params: ConfluenceSearchInSpaceParams) => ({
|
||||
domain: params.domain,
|
||||
accessToken: params.accessToken,
|
||||
spaceKey: params.spaceKey?.trim(),
|
||||
query: params.query,
|
||||
contentType: params.contentType,
|
||||
limit: params.limit ? Number(params.limit) : 25,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
spaceKey: data.spaceKey ?? '',
|
||||
totalSize: data.totalSize ?? 0,
|
||||
results: data.results ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: TIMESTAMP_OUTPUT,
|
||||
spaceKey: {
|
||||
type: 'string',
|
||||
description: 'The space key that was searched',
|
||||
},
|
||||
totalSize: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching results',
|
||||
},
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search results',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: SEARCH_RESULT_ITEM_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -26,6 +26,52 @@ export const VERSION_OUTPUT_PROPERTIES = {
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Detailed version object properties for get_page_version endpoint.
|
||||
* Based on Confluence API v2 DetailedVersion schema.
|
||||
*/
|
||||
export const DETAILED_VERSION_OUTPUT_PROPERTIES = {
|
||||
number: { type: 'number', description: 'Version number' },
|
||||
message: { type: 'string', description: 'Version message', optional: true },
|
||||
minorEdit: { type: 'boolean', description: 'Whether this is a minor edit' },
|
||||
authorId: { type: 'string', description: 'Account ID of the version author', optional: true },
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp of version creation',
|
||||
optional: true,
|
||||
},
|
||||
contentTypeModified: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the content type was modified in this version',
|
||||
optional: true,
|
||||
},
|
||||
collaborators: {
|
||||
type: 'array',
|
||||
description: 'List of collaborator account IDs for this version',
|
||||
items: { type: 'string' },
|
||||
optional: true,
|
||||
},
|
||||
prevVersion: {
|
||||
type: 'number',
|
||||
description: 'Previous version number',
|
||||
optional: true,
|
||||
},
|
||||
nextVersion: {
|
||||
type: 'number',
|
||||
description: 'Next version number',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete detailed version object output definition.
|
||||
*/
|
||||
export const DETAILED_VERSION_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Detailed version information',
|
||||
properties: DETAILED_VERSION_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete version object output definition.
|
||||
*/
|
||||
@@ -137,6 +183,54 @@ export const SPACES_OUTPUT: OutputProperty = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Body format inner object properties (storage, view, atlas_doc_format).
|
||||
* Based on Confluence API v2 body structure.
|
||||
*/
|
||||
export const BODY_FORMAT_PROPERTIES = {
|
||||
value: { type: 'string', description: 'The content value in the specified format' },
|
||||
representation: {
|
||||
type: 'string',
|
||||
description: 'Content representation type',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Page/Blog post body object properties.
|
||||
* Based on Confluence API v2 body structure with multiple format options.
|
||||
*/
|
||||
export const CONTENT_BODY_OUTPUT_PROPERTIES = {
|
||||
storage: {
|
||||
type: 'object',
|
||||
description: 'Body in storage format (Confluence markup)',
|
||||
properties: BODY_FORMAT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
view: {
|
||||
type: 'object',
|
||||
description: 'Body in view format (rendered HTML)',
|
||||
properties: BODY_FORMAT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
atlas_doc_format: {
|
||||
type: 'object',
|
||||
description: 'Body in Atlassian Document Format (ADF)',
|
||||
properties: BODY_FORMAT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete body object output definition for pages and blog posts.
|
||||
*/
|
||||
export const CONTENT_BODY_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Page or blog post body content in requested format(s)',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment body object properties.
|
||||
* Based on Confluence API v2 comment body structure.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ConfluenceUpdateParams, ConfluenceUpdateResponse } from '@/tools/confluence/types'
|
||||
import { CONTENT_BODY_OUTPUT_PROPERTIES, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, ConfluenceUpdateResponse> = {
|
||||
@@ -98,9 +99,13 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id,
|
||||
title: data.title,
|
||||
body: data.body,
|
||||
pageId: data.id ?? '',
|
||||
title: data.title ?? '',
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
body: data.body ?? null,
|
||||
version: data.version ?? null,
|
||||
url: data._links?.webui ?? null,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
@@ -110,6 +115,21 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
|
||||
ts: { type: 'string', description: 'Timestamp of update' },
|
||||
pageId: { type: 'string', description: 'Confluence page ID' },
|
||||
title: { type: 'string', description: 'Updated page title' },
|
||||
status: { type: 'string', description: 'Page status', optional: true },
|
||||
spaceId: { type: 'string', description: 'Space ID', optional: true },
|
||||
body: {
|
||||
type: 'object',
|
||||
description: 'Page body content in storage format',
|
||||
properties: CONTENT_BODY_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
version: {
|
||||
type: 'object',
|
||||
description: 'Page version information',
|
||||
properties: VERSION_OUTPUT_PROPERTIES,
|
||||
optional: true,
|
||||
},
|
||||
url: { type: 'string', description: 'URL to view the page in Confluence', optional: true },
|
||||
success: { type: 'boolean', description: 'Update operation success status' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,15 +57,10 @@ function stripHtmlTags(html: string): string {
|
||||
}
|
||||
|
||||
export function transformPageData(data: any) {
|
||||
const content =
|
||||
data.body?.view?.value ||
|
||||
data.body?.storage?.value ||
|
||||
data.body?.atlas_doc_format?.value ||
|
||||
data.content ||
|
||||
data.description ||
|
||||
`Content for page ${data.title || 'Unknown'}`
|
||||
const rawContent =
|
||||
data.body?.storage?.value || data.body?.view?.value || data.body?.atlas_doc_format?.value || ''
|
||||
|
||||
let cleanContent = stripHtmlTags(content)
|
||||
let cleanContent = stripHtmlTags(rawContent)
|
||||
cleanContent = decodeHtmlEntities(cleanContent)
|
||||
cleanContent = cleanContent.replace(/\s+/g, ' ').trim()
|
||||
|
||||
@@ -73,9 +68,17 @@ export function transformPageData(data: any) {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id || '',
|
||||
pageId: data.id ?? '',
|
||||
title: data.title ?? '',
|
||||
content: cleanContent,
|
||||
title: data.title || '',
|
||||
status: data.status ?? null,
|
||||
spaceId: data.spaceId ?? null,
|
||||
parentId: data.parentId ?? null,
|
||||
authorId: data.authorId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
url: data._links?.webui ?? null,
|
||||
body: data.body ?? null,
|
||||
version: data.version ?? null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,17 +110,30 @@ import {
|
||||
clerkUpdateUserTool,
|
||||
} from '@/tools/clerk'
|
||||
import {
|
||||
confluenceAddLabelTool,
|
||||
confluenceCreateBlogPostTool,
|
||||
confluenceCreateCommentTool,
|
||||
confluenceCreatePagePropertyTool,
|
||||
confluenceCreatePageTool,
|
||||
confluenceDeleteAttachmentTool,
|
||||
confluenceDeleteCommentTool,
|
||||
confluenceDeletePageTool,
|
||||
confluenceGetBlogPostTool,
|
||||
confluenceGetPageAncestorsTool,
|
||||
confluenceGetPageChildrenTool,
|
||||
confluenceGetPageVersionTool,
|
||||
confluenceGetSpaceTool,
|
||||
confluenceListAttachmentsTool,
|
||||
confluenceListBlogPostsInSpaceTool,
|
||||
confluenceListBlogPostsTool,
|
||||
confluenceListCommentsTool,
|
||||
confluenceListLabelsTool,
|
||||
confluenceListPagePropertiesTool,
|
||||
confluenceListPagesInSpaceTool,
|
||||
confluenceListPageVersionsTool,
|
||||
confluenceListSpacesTool,
|
||||
confluenceRetrieveTool,
|
||||
confluenceSearchInSpaceTool,
|
||||
confluenceSearchTool,
|
||||
confluenceUpdateCommentTool,
|
||||
confluenceUpdateTool,
|
||||
@@ -2608,7 +2621,19 @@ export const tools: Record<string, ToolConfig> = {
|
||||
confluence_update: confluenceUpdateTool,
|
||||
confluence_create_page: confluenceCreatePageTool,
|
||||
confluence_delete_page: confluenceDeletePageTool,
|
||||
confluence_list_pages_in_space: confluenceListPagesInSpaceTool,
|
||||
confluence_get_page_children: confluenceGetPageChildrenTool,
|
||||
confluence_get_page_ancestors: confluenceGetPageAncestorsTool,
|
||||
confluence_list_page_versions: confluenceListPageVersionsTool,
|
||||
confluence_get_page_version: confluenceGetPageVersionTool,
|
||||
confluence_list_page_properties: confluenceListPagePropertiesTool,
|
||||
confluence_create_page_property: confluenceCreatePagePropertyTool,
|
||||
confluence_list_blogposts: confluenceListBlogPostsTool,
|
||||
confluence_get_blogpost: confluenceGetBlogPostTool,
|
||||
confluence_create_blogpost: confluenceCreateBlogPostTool,
|
||||
confluence_list_blogposts_in_space: confluenceListBlogPostsInSpaceTool,
|
||||
confluence_search: confluenceSearchTool,
|
||||
confluence_search_in_space: confluenceSearchInSpaceTool,
|
||||
confluence_create_comment: confluenceCreateCommentTool,
|
||||
confluence_list_comments: confluenceListCommentsTool,
|
||||
confluence_update_comment: confluenceUpdateCommentTool,
|
||||
@@ -2617,6 +2642,7 @@ export const tools: Record<string, ToolConfig> = {
|
||||
confluence_upload_attachment: confluenceUploadAttachmentTool,
|
||||
confluence_delete_attachment: confluenceDeleteAttachmentTool,
|
||||
confluence_list_labels: confluenceListLabelsTool,
|
||||
confluence_add_label: confluenceAddLabelTool,
|
||||
confluence_get_space: confluenceGetSpaceTool,
|
||||
confluence_list_spaces: confluenceListSpacesTool,
|
||||
cursor_list_agents: cursorListAgentsTool,
|
||||
|
||||
Reference in New Issue
Block a user