Compare commits

...

17 Commits

Author SHA1 Message Date
Waleed Latif
e958ba13d5 fix(confluence): fetch current space name when updating only description
The Confluence v2 PUT /spaces/{id} endpoint requires the name field.
When the user only provides a description update, fetch the current
space first to preserve the existing name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:01:05 -08:00
Waleed Latif
b68a264965 fix(confluence): add body-format=storage to page update GET request
Same bug class as the blogpost fix — without this param, the Confluence
v2 API does not return body content, causing the fallback to erase page
content when only updating the title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 14:00:36 -08:00
Waleed Latif
399db331a2 fix(confluence): add body-format=storage to blogpost update GET request
Without this param, the Confluence v2 API does not return body content,
causing the fallback to erase existing content when only updating the title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 13:57:34 -08:00
Waleed Latif
04461de3ca fix(confluence): remove spaceId requirement for create_space and fix list_tasks pagination
- Remove create_space from spaceId condition array since creating a space
  doesn't require a space ID input
- Remove list_tasks from generic supportsCursor array so it uses its
  dedicated handler that correctly passes assignedTo and status filters
  during pagination

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:46:07 -08:00
Waleed Latif
e96b84764f fix(confluence): reject empty update body for space PUT
Return 400 when neither name nor description is provided for space
update, instead of sending an empty body to the Confluence API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:34:11 -08:00
waleed
f185b6b0cf upgrade turborepo 2026-02-25 16:32:00 -08:00
waleed
490de05a25 update mock 2026-02-25 16:31:48 -08:00
waleed
ea45ee6c4e ran lint 2026-02-25 16:22:13 -08:00
Waleed Latif
a494656afe fix(confluence): use validatePathSegment for Atlassian account IDs
validateAlphanumericId rejects valid Atlassian account IDs that contain
colons (e.g. 557058:6b9c9931-4693-49c1-8b3a-931f1af98134). Use
validatePathSegment with a custom pattern allowing colons instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:13 -08:00
waleed
6da784eed4 lint 2026-02-25 16:22:13 -08:00
Waleed Latif
973fe96418 feat(confluence): add missing response fields for descendants and tasks
- Add type and depth fields to page descendants (from Confluence API)
- Add body field (storage format) to task list/get/update responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:13 -08:00
Waleed Latif
3ff834285f fix(confluence): address PR review feedback
- Move get_user from GET to POST to avoid exposing access token in URL
- Add 400 validation for missing params in space-properties create/delete
- Add null check for blog post version before update to prevent TypeError

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:13 -08:00
Waleed Latif
58a5c7dd00 fix(confluence): fix truncated get_user tool description in docs
Remove apostrophe from description that caused MDX generation to
truncate at the escape character.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:13 -08:00
waleed
1dc4ff2c62 lint 2026-02-25 16:22:13 -08:00
Waleed Latif
f09fef9aee fix(confluence): add missing OAuth scopes to auth.ts provider config
The OAuth authorization flow uses scopes from auth.ts, not oauth.ts.
The 9 new scopes were only added to oauth.ts and the block config but
not to the actual provider config in auth.ts, causing re-auth to still
return tokens without the new scopes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:13 -08:00
Waleed Latif
2a97824a05 feat(confluence): add missing tools for tasks, blog posts, spaces, descendants, permissions, and properties
Add 16 new Confluence operations: list/get/update tasks, update/delete blog posts,
create/update/delete spaces, get page descendants, list space permissions,
list/create/delete space properties. Includes API routes, tool definitions,
block config wiring, OAuth scopes, and generated docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:22:12 -08:00
Waleed Latif
3eb6c082f2 feat(confluence): add get user by account ID tool 2026-02-25 16:22:12 -08:00
35 changed files with 3835 additions and 25 deletions

View File

@@ -38,8 +38,8 @@ import {
EyeIcon,
FirecrawlIcon,
FirefliesIcon,
GithubIcon,
GitLabIcon,
GithubIcon,
GmailIcon,
GongIcon,
GoogleBooksIcon,
@@ -73,9 +73,9 @@ import {
LinearIcon,
LinkedInIcon,
LinkupIcon,
MailServerIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
@@ -108,6 +108,8 @@ import {
ResendIcon,
RevenueCatIcon,
S3Icon,
SQSIcon,
STTIcon,
SalesforceIcon,
SearchIcon,
SendgridIcon,
@@ -119,19 +121,17 @@ import {
SimilarwebIcon,
SlackIcon,
SmtpIcon,
SQSIcon,
SshIcon,
STTIcon,
StagehandIcon,
StripeIcon,
SupabaseIcon,
TTSIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
TinybirdIcon,
TranslateIcon,
TrelloIcon,
TTSIcon,
TwilioIcon,
TypeformIcon,
UpstashIcon,
@@ -142,11 +142,11 @@ import {
WhatsAppIcon,
WikipediaIcon,
WordpressIcon,
xIcon,
YouTubeIcon,
ZendeskIcon,
ZepIcon,
ZoomIcon,
xIcon,
} from '@/components/icons'
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>

View File

@@ -1013,6 +1013,85 @@ Get details about a specific Confluence space.
| ↳ `value` | string | Description text content |
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
### `confluence_create_space`
Create a new Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `name` | string | Yes | Name for the new space |
| `key` | string | Yes | Unique key for the space \(uppercase, no spaces\) |
| `description` | string | No | Description for the new space |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Created space ID |
| `name` | string | Space name |
| `key` | string | Space key |
| `type` | string | Space type |
| `status` | string | Space status |
| `url` | string | URL to view the space |
| `homepageId` | string | Homepage ID |
| `description` | object | Space description |
| ↳ `value` | string | Description text content |
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
### `confluence_update_space`
Update a Confluence space name or description.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | ID of the space to update |
| `name` | string | No | New name for the space |
| `description` | string | No | New description for the space |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Updated space ID |
| `name` | string | Space name |
| `key` | string | Space key |
| `type` | string | Space type |
| `status` | string | Space status |
| `url` | string | URL to view the space |
| `description` | object | Space description |
| ↳ `value` | string | Description text content |
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
### `confluence_delete_space`
Delete a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | ID of the space to delete |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Deleted space ID |
| `deleted` | boolean | Deletion status |
### `confluence_list_spaces`
List all Confluence spaces accessible to the user.
@@ -1045,4 +1124,311 @@ List all Confluence spaces accessible to the user.
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_list_space_properties`
List properties on a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | Space ID to list properties for |
| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `properties` | array | Array of space properties |
| ↳ `id` | string | Property ID |
| ↳ `key` | string | Property key |
| ↳ `value` | json | Property value |
| `spaceId` | string | Space ID |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_create_space_property`
Create a property on a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | Space ID to create the property on |
| `key` | string | Yes | Property key/name |
| `value` | json | No | Property value \(JSON\) |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `propertyId` | string | Created property ID |
| `key` | string | Property key |
| `value` | json | Property value |
| `spaceId` | string | Space ID |
### `confluence_delete_space_property`
Delete a property from a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | Space ID the property belongs to |
| `propertyId` | string | Yes | Property ID to delete |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Space ID |
| `propertyId` | string | Deleted property ID |
| `deleted` | boolean | Deletion status |
### `confluence_list_space_permissions`
List permissions for a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | Space ID to list permissions for |
| `limit` | number | No | Maximum number of permissions to return \(default: 50, max: 250\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `permissions` | array | Array of space permissions |
| ↳ `id` | string | Permission ID |
| ↳ `principalType` | string | Principal type \(user, group, role\) |
| ↳ `principalId` | string | Principal ID |
| ↳ `operationKey` | string | Operation key \(read, create, delete, etc.\) |
| ↳ `operationTargetType` | string | Target type \(page, blogpost, space, etc.\) |
| ↳ `anonymousAccess` | boolean | Whether anonymous access is allowed |
| ↳ `unlicensedAccess` | boolean | Whether unlicensed access is allowed |
| `spaceId` | string | Space ID |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_page_descendants`
Get all descendants of a Confluence page recursively.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Page ID to get descendants for |
| `limit` | number | No | Maximum number of descendants to return \(default: 50, max: 250\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `descendants` | array | Array of descendant pages |
| ↳ `id` | string | Page ID |
| ↳ `title` | string | Page title |
| ↳ `type` | string | Content type \(page, whiteboard, database, etc.\) |
| ↳ `status` | string | Page status |
| ↳ `spaceId` | string | Space ID |
| ↳ `parentId` | string | Parent page ID |
| ↳ `childPosition` | number | Position among siblings |
| ↳ `depth` | number | Depth in the hierarchy |
| `pageId` | string | Parent page ID |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_list_tasks`
List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | No | Filter tasks by page ID |
| `spaceId` | string | No | Filter tasks by space ID |
| `assignedTo` | string | No | Filter tasks by assignee account ID |
| `status` | string | No | Filter tasks by status \(complete or incomplete\) |
| `limit` | number | No | Maximum number of tasks to return \(default: 50, max: 250\) |
| `cursor` | string | No | Pagination cursor from previous response |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `tasks` | array | Array of Confluence tasks |
| ↳ `id` | string | Task ID |
| ↳ `localId` | string | Local task ID |
| ↳ `spaceId` | string | Space ID |
| ↳ `pageId` | string | Page ID |
| ↳ `blogPostId` | string | Blog post ID |
| ↳ `status` | string | Task status \(complete or incomplete\) |
| ↳ `body` | string | Task body content in storage format |
| ↳ `createdBy` | string | Creator account ID |
| ↳ `assignedTo` | string | Assignee account ID |
| ↳ `completedBy` | string | Completer account ID |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `updatedAt` | string | Last update timestamp |
| ↳ `dueAt` | string | Due date |
| ↳ `completedAt` | string | Completion timestamp |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_task`
Get a specific Confluence inline task by ID.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `taskId` | string | Yes | The ID of the task to retrieve |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `id` | string | Task ID |
| `localId` | string | Local task ID |
| `spaceId` | string | Space ID |
| `pageId` | string | Page ID |
| `blogPostId` | string | Blog post ID |
| `status` | string | Task status \(complete or incomplete\) |
| `body` | string | Task body content in storage format |
| `createdBy` | string | Creator account ID |
| `assignedTo` | string | Assignee account ID |
| `completedBy` | string | Completer account ID |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `dueAt` | string | Due date |
| `completedAt` | string | Completion timestamp |
### `confluence_update_task`
Update the status of a Confluence inline task (complete or incomplete).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `taskId` | string | Yes | The ID of the task to update |
| `status` | string | Yes | New status for the task \(complete or incomplete\) |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `id` | string | Task ID |
| `localId` | string | Local task ID |
| `spaceId` | string | Space ID |
| `pageId` | string | Page ID |
| `blogPostId` | string | Blog post ID |
| `status` | string | Updated task status |
| `body` | string | Task body content in storage format |
| `createdBy` | string | Creator account ID |
| `assignedTo` | string | Assignee account ID |
| `completedBy` | string | Completer account ID |
| `createdAt` | string | Creation timestamp |
| `updatedAt` | string | Last update timestamp |
| `dueAt` | string | Due date |
| `completedAt` | string | Completion timestamp |
### `confluence_update_blogpost`
Update an existing Confluence blog post title and/or content.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `blogPostId` | string | Yes | The ID of the blog post to update |
| `title` | string | No | New title for the blog post |
| `content` | string | No | New content for the blog post in storage format |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `blogPostId` | string | Updated blog post ID |
| `title` | string | Blog post title |
| `status` | string | Blog post status |
| `spaceId` | string | Space ID |
| `version` | json | Version information |
| `url` | string | URL to view the blog post |
### `confluence_delete_blogpost`
Delete a Confluence blog post.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `blogPostId` | string | Yes | The ID of the blog post to delete |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `blogPostId` | string | Deleted blog post ID |
| `deleted` | boolean | Deletion status |
### `confluence_get_user`
Get display name and profile info for a Confluence user by account ID.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `accountId` | string | Yes | The Atlassian account ID of the user to look up |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `accountId` | string | Atlassian account ID of the user |
| `displayName` | string | Display name of the user |
| `email` | string | Email address of the user |
| `accountType` | string | Account type \(e.g., atlassian, app, customer\) |
| `profilePicture` | string | Path to the user profile picture |
| `publicName` | string | Public name of the user |

View File

@@ -147,4 +147,4 @@
"zep",
"zoom"
]
}
}

View File

@@ -283,3 +283,165 @@ export async function POST(request: NextRequest) {
)
}
}
/**
* Update a blog post
*/
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 { domain, accessToken, blogPostId, title, content, cloudId: providedCloudId } = body
if (!domain || !accessToken || !blogPostId) {
return NextResponse.json(
{ error: 'Domain, access token, and blog post ID are required' },
{ status: 400 }
)
}
const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255)
if (!blogPostIdValidation.isValid) {
return NextResponse.json({ error: blogPostIdValidation.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 })
}
// Fetch current blog post to get version number
const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}?body-format=storage`
const currentResponse = await fetch(currentUrl, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!currentResponse.ok) {
throw new Error(`Failed to fetch current blog post: ${currentResponse.status}`)
}
const currentPost = await currentResponse.json()
if (!currentPost.version?.number) {
return NextResponse.json(
{ error: 'Unable to determine current blog post version' },
{ status: 422 }
)
}
const currentVersion = currentPost.version.number
const updateBody: Record<string, unknown> = {
id: blogPostId,
version: { number: currentVersion + 1 },
status: 'current',
title: title || currentPost.title,
body: {
representation: 'storage',
value: content || currentPost.body?.storage?.value || '',
},
}
const response = await fetch(currentUrl, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(updateBody),
})
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 blog post (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
logger.error('Error updating blog post:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Delete a blog post
*/
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 { domain, accessToken, blogPostId, cloudId: providedCloudId } = body
if (!domain || !accessToken || !blogPostId) {
return NextResponse.json(
{ error: 'Domain, access token, and blog post ID are required' },
{ status: 400 }
)
}
const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255)
if (!blogPostIdValidation.isValid) {
return NextResponse.json({ error: blogPostIdValidation.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/blogposts/${blogPostId}`
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 blog post (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
return NextResponse.json({ blogPostId, deleted: true })
} catch (error) {
logger.error('Error deleting blog post:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,107 @@
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('ConfluencePageDescendantsAPI')
export const dynamic = 'force-dynamic'
/**
* Get all descendants of a Confluence page recursively.
* Uses GET /wiki/api/v2/pages/{id}/descendants
*/
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}/descendants?${queryParams.toString()}`
logger.info(`Fetching descendants 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 descendants (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const descendants = (data.results || []).map((page: any) => ({
id: page.id,
title: page.title,
type: page.type ?? null,
status: page.status ?? null,
spaceId: page.spaceId ?? null,
parentId: page.parentId ?? null,
childPosition: page.childPosition ?? null,
depth: page.depth ?? null,
}))
return NextResponse.json({
descendants,
pageId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error getting page descendants:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -185,7 +185,7 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}`
const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage`
const currentPageResponse = await fetch(currentPageUrl, {
headers: {
Accept: 'application/json',

View File

@@ -0,0 +1,106 @@
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('ConfluenceSpacePermissionsAPI')
export const dynamic = 'force-dynamic'
/**
* List permissions for a Confluence space.
* Uses GET /wiki/api/v2/spaces/{id}/permissions
*/
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, 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 (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/permissions?${queryParams.toString()}`
logger.info(`Fetching permissions for 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 space permissions (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const permissions = (data.results || []).map((perm: any) => ({
id: perm.id,
principalType: perm.principal?.type ?? null,
principalId: perm.principal?.id ?? null,
operationKey: perm.operation?.key ?? null,
operationTargetType: perm.operation?.targetType ?? null,
anonymousAccess: perm.anonymousAccess ?? false,
unlicensedAccess: perm.unlicensedAccess ?? false,
}))
return NextResponse.json({
permissions,
spaceId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing space permissions:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,199 @@
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('ConfluenceSpacePropertiesAPI')
export const dynamic = 'force-dynamic'
/**
* List, create, or delete space properties.
* Uses GET/POST /wiki/api/v2/spaces/{id}/properties
* and DELETE /wiki/api/v2/spaces/{id}/properties/{propertyId}
*/
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,
action,
key,
value,
propertyId,
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 (!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 baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/properties`
// Validate required params for specific actions
if (action === 'delete' && !propertyId) {
return NextResponse.json(
{ error: 'Property ID is required for delete action' },
{ status: 400 }
)
}
if (action === 'create' && !key) {
return NextResponse.json(
{ error: 'Property key is required for create action' },
{ status: 400 }
)
}
// Delete a property
if (action === 'delete' && propertyId) {
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
if (!propertyIdValidation.isValid) {
return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 })
}
const url = `${baseUrl}/${propertyId}`
logger.info(`Deleting space property ${propertyId} from space ${spaceId}`)
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 space property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
return NextResponse.json({ spaceId, propertyId, deleted: true })
}
// Create a property
if (action === 'create' && key) {
logger.info(`Creating space property '${key}' on space ${spaceId}`)
const response = await fetch(baseUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ key, value: 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 space property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
propertyId: data.id,
key: data.key,
value: data.value ?? null,
spaceId,
})
}
// List properties
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) queryParams.append('cursor', cursor)
const url = `${baseUrl}?${queryParams.toString()}`
logger.info(`Fetching properties for 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 space 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,
}))
return NextResponse.json({
properties,
spaceId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error with space properties:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -78,3 +78,258 @@ export async function GET(request: NextRequest) {
)
}
}
/**
* Create a new Confluence space.
* Uses POST /wiki/api/v2/spaces
*/
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, name, key, description, cloudId: providedCloudId } = 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 (!name) {
return NextResponse.json({ error: 'Space name is required' }, { status: 400 })
}
if (!key) {
return NextResponse.json({ error: 'Space key 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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces`
const createBody: Record<string, unknown> = { name, key }
if (description) {
createBody.description = { value: description, representation: 'plain' }
}
logger.info(`Creating space with key ${key}`)
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 space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
logger.error('Error creating Confluence space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Update a Confluence space.
* Uses PUT /wiki/api/v2/spaces/{id}
*/
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 { domain, accessToken, spaceId, name, description, cloudId: providedCloudId } = 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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
if (!name && description === undefined) {
return NextResponse.json(
{ error: 'At least one of name or description is required for update' },
{ status: 400 }
)
}
const updateBody: Record<string, unknown> = {}
if (name) {
updateBody.name = name
} else {
const currentResponse = await fetch(url, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!currentResponse.ok) {
return NextResponse.json(
{ error: `Failed to fetch current space: ${currentResponse.status}` },
{ status: currentResponse.status }
)
}
const currentSpace = await currentResponse.json()
updateBody.name = currentSpace.name
}
if (description !== undefined) {
updateBody.description = { value: description, representation: 'plain' }
}
logger.info(`Updating space ${spaceId}`)
const response = await fetch(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(updateBody),
})
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 space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
logger.error('Error updating Confluence space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Delete a Confluence space.
* Uses DELETE /wiki/api/v2/spaces/{id}
*/
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 { domain, accessToken, spaceId, cloudId: providedCloudId } = 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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}`
logger.info(`Deleting space ${spaceId}`)
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 space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
return NextResponse.json({ spaceId, deleted: true })
} catch (error) {
logger.error('Error deleting Confluence space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,244 @@
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('ConfluenceTasksAPI')
export const dynamic = 'force-dynamic'
/**
* List, get, or update Confluence inline tasks.
* Uses GET /wiki/api/v2/tasks, GET /wiki/api/v2/tasks/{id}, PUT /wiki/api/v2/tasks/{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()
const {
domain,
accessToken,
cloudId: providedCloudId,
action,
taskId,
status: taskStatus,
pageId,
spaceId,
assignedTo,
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 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
// Update a task
if (action === 'update' && taskId) {
const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255)
if (!taskIdValidation.isValid) {
return NextResponse.json({ error: taskIdValidation.error }, { status: 400 })
}
// First fetch the current task to get required fields
const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
const getResponse = await fetch(getUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!getResponse.ok) {
const errorData = await getResponse.json().catch(() => null)
const errorMessage = errorData?.message || `Failed to fetch task (${getResponse.status})`
return NextResponse.json({ error: errorMessage }, { status: getResponse.status })
}
const currentTask = await getResponse.json()
const updateBody: Record<string, unknown> = {
id: taskId,
status: taskStatus || currentTask.status,
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
logger.info(`Updating task ${taskId}`)
const response = await fetch(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(updateBody),
})
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 task (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
task: {
id: data.id,
localId: data.localId ?? null,
spaceId: data.spaceId ?? null,
pageId: data.pageId ?? null,
blogPostId: data.blogPostId ?? null,
status: data.status,
body: data.body?.storage?.value ?? null,
createdBy: data.createdBy ?? null,
assignedTo: data.assignedTo ?? null,
completedBy: data.completedBy ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
dueAt: data.dueAt ?? null,
completedAt: data.completedAt ?? null,
},
})
}
// Get a specific task
if (taskId) {
const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255)
if (!taskIdValidation.isValid) {
return NextResponse.json({ error: taskIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
logger.info(`Fetching task ${taskId}`)
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 task (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
task: {
id: data.id,
localId: data.localId ?? null,
spaceId: data.spaceId ?? null,
pageId: data.pageId ?? null,
blogPostId: data.blogPostId ?? null,
status: data.status,
body: data.body?.storage?.value ?? null,
createdBy: data.createdBy ?? null,
assignedTo: data.assignedTo ?? null,
completedBy: data.completedBy ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
dueAt: data.dueAt ?? null,
completedAt: data.completedAt ?? null,
},
})
}
// List tasks
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) queryParams.append('cursor', cursor)
if (taskStatus) queryParams.append('status', taskStatus)
if (pageId) queryParams.append('page-id', pageId)
if (spaceId) queryParams.append('space-id', spaceId)
if (assignedTo) queryParams.append('assigned-to', assignedTo)
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks?${queryParams.toString()}`
logger.info('Fetching tasks')
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 tasks (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const tasks = (data.results || []).map((task: any) => ({
id: task.id,
localId: task.localId ?? null,
spaceId: task.spaceId ?? null,
pageId: task.pageId ?? null,
blogPostId: task.blogPostId ?? null,
status: task.status,
body: task.body?.storage?.value ?? null,
createdBy: task.createdBy ?? null,
assignedTo: task.assignedTo ?? null,
completedBy: task.completedBy ?? null,
createdAt: task.createdAt ?? null,
updatedAt: task.updatedAt ?? null,
dueAt: task.dueAt ?? null,
completedAt: task.completedAt ?? null,
}))
return NextResponse.json({
tasks,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error with tasks:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,85 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validatePathSegment } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceUserAPI')
export const dynamic = 'force-dynamic'
/**
* Get a Confluence user by account ID.
* Uses GET /wiki/rest/api/user?accountId={accountId}
*/
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, accountId, cloudId: providedCloudId } = 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 (!accountId) {
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
}
// Atlassian account IDs use format like 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
const accountIdValidation = validatePathSegment(accountId, {
paramName: 'accountId',
maxLength: 255,
customPattern: /^[a-zA-Z0-9:-]+$/,
})
if (!accountIdValidation.isValid) {
return NextResponse.json({ error: accountIdValidation.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/rest/api/user?accountId=${encodeURIComponent(accountId)}`
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 Confluence user (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
logger.error('Error getting Confluence user:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -81,6 +81,15 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'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:user:confluence': 'View Confluence user profiles',
'read:task:confluence': 'View Confluence inline tasks',
'write:task:confluence': 'Update Confluence inline tasks',
'delete:blogpost:confluence': 'Delete Confluence blog posts',
'write:space:confluence': 'Create and update Confluence spaces',
'delete:space:confluence': 'Delete Confluence spaces',
'read:space.property:confluence': 'View Confluence space properties',
'write:space.property:confluence': 'Create and manage space properties',
'read:space.permission:confluence': 'View Confluence space permissions',
'read:me': 'Read profile information',
'database.read': 'Read database',
'database.write': 'Write to database',

View File

@@ -84,6 +84,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
],
placeholder: 'Select Confluence account',
required: true,
@@ -414,6 +415,8 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
{ label: 'List Blog Posts', id: 'list_blogposts' },
{ label: 'Get Blog Post', id: 'get_blogpost' },
{ label: 'Create Blog Post', id: 'create_blogpost' },
{ label: 'Update Blog Post', id: 'update_blogpost' },
{ label: 'Delete Blog Post', id: 'delete_blogpost' },
{ label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' },
// Comment Operations
{ label: 'Create Comment', id: 'create_comment' },
@@ -432,7 +435,24 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
{ label: 'List Space Labels', id: 'list_space_labels' },
// Space Operations
{ label: 'Get Space', id: 'get_space' },
{ label: 'Create Space', id: 'create_space' },
{ label: 'Update Space', id: 'update_space' },
{ label: 'Delete Space', id: 'delete_space' },
{ label: 'List Spaces', id: 'list_spaces' },
// Space Property Operations
{ label: 'List Space Properties', id: 'list_space_properties' },
{ label: 'Create Space Property', id: 'create_space_property' },
{ label: 'Delete Space Property', id: 'delete_space_property' },
// Space Permission Operations
{ label: 'List Space Permissions', id: 'list_space_permissions' },
// Page Descendant Operations
{ label: 'Get Page Descendants', id: 'get_page_descendants' },
// Task Operations
{ label: 'List Tasks', id: 'list_tasks' },
{ label: 'Get Task', id: 'get_task' },
{ label: 'Update Task', id: 'update_task' },
// User Operations
{ label: 'Get User', id: 'get_user' },
],
value: () => 'read',
},
@@ -472,6 +492,15 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
'read:task:confluence',
'write:task:confluence',
'delete:blogpost:confluence',
'write:space:confluence',
'delete:space:confluence',
'read:space.property:confluence',
'write:space.property:confluence',
'read:space.permission:confluence',
],
placeholder: 'Select Confluence account',
required: true,
@@ -507,13 +536,26 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_pages_in_space',
'list_blogposts',
'get_blogpost',
'update_blogpost',
'delete_blogpost',
'list_blogposts_in_space',
'search',
'search_in_space',
'get_space',
'create_space',
'update_space',
'delete_space',
'list_spaces',
'get_pages_by_label',
'list_space_labels',
'list_space_permissions',
'list_space_properties',
'create_space_property',
'delete_space_property',
'list_tasks',
'get_task',
'update_task',
'get_user',
],
not: true,
},
@@ -537,6 +579,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'get_page_version',
'list_page_properties',
'create_page_property',
'get_page_descendants',
],
},
},
@@ -553,13 +596,26 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_pages_in_space',
'list_blogposts',
'get_blogpost',
'update_blogpost',
'delete_blogpost',
'list_blogposts_in_space',
'search',
'search_in_space',
'get_space',
'create_space',
'update_space',
'delete_space',
'list_spaces',
'get_pages_by_label',
'list_space_labels',
'list_space_permissions',
'list_space_properties',
'create_space_property',
'delete_space_property',
'list_tasks',
'get_task',
'update_task',
'get_user',
],
not: true,
},
@@ -583,6 +639,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'get_page_version',
'list_page_properties',
'create_page_property',
'get_page_descendants',
],
},
},
@@ -597,11 +654,17 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
value: [
'create',
'get_space',
'update_space',
'delete_space',
'list_pages_in_space',
'search_in_space',
'create_blogpost',
'list_blogposts_in_space',
'list_space_labels',
'list_space_permissions',
'list_space_properties',
'create_space_property',
'delete_space_property',
],
},
},
@@ -611,7 +674,10 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
type: 'short-input',
placeholder: 'Enter blog post ID',
required: true,
condition: { field: 'operation', value: 'get_blogpost' },
condition: {
field: 'operation',
value: ['get_blogpost', 'update_blogpost', 'delete_blogpost'],
},
},
{
id: 'versionNumber',
@@ -621,6 +687,86 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
required: true,
condition: { field: 'operation', value: 'get_page_version' },
},
{
id: 'accountId',
title: 'Account ID',
type: 'short-input',
placeholder: 'Enter Atlassian account ID',
required: true,
condition: { field: 'operation', value: 'get_user' },
},
{
id: 'taskId',
title: 'Task ID',
type: 'short-input',
placeholder: 'Enter task ID',
required: true,
condition: { field: 'operation', value: ['get_task', 'update_task'] },
},
{
id: 'taskStatus',
title: 'Task Status',
type: 'dropdown',
options: [
{ label: 'Complete', id: 'complete' },
{ label: 'Incomplete', id: 'incomplete' },
],
value: () => 'complete',
condition: { field: 'operation', value: 'update_task' },
},
{
id: 'taskAssignedTo',
title: 'Assigned To',
type: 'short-input',
placeholder: 'Filter by assignee account ID (optional)',
condition: { field: 'operation', value: 'list_tasks' },
},
{
id: 'spaceName',
title: 'Space Name',
type: 'short-input',
placeholder: 'Enter space name',
required: true,
condition: { field: 'operation', value: 'create_space' },
},
{
id: 'spaceKey',
title: 'Space Key',
type: 'short-input',
placeholder: 'Enter space key (e.g., MYSPACE)',
required: true,
condition: { field: 'operation', value: 'create_space' },
},
{
id: 'spaceDescription',
title: 'Description',
type: 'long-input',
placeholder: 'Enter space description (optional)',
condition: { field: 'operation', value: ['create_space', 'update_space'] },
},
{
id: 'spacePropertyKey',
title: 'Property Key',
type: 'short-input',
placeholder: 'Enter property key/name',
required: true,
condition: { field: 'operation', value: 'create_space_property' },
},
{
id: 'spacePropertyValue',
title: 'Property Value',
type: 'long-input',
placeholder: 'Enter property value (JSON supported)',
condition: { field: 'operation', value: 'create_space_property' },
},
{
id: 'spacePropertyId',
title: 'Property ID',
type: 'short-input',
placeholder: 'Enter property ID to delete',
required: true,
condition: { field: 'operation', value: 'delete_space_property' },
},
{
id: 'propertyKey',
title: 'Property Key',
@@ -650,14 +796,20 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
title: 'Title',
type: 'short-input',
placeholder: 'Enter title',
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
condition: {
field: 'operation',
value: ['create', 'update', 'create_blogpost', 'update_blogpost', 'update_space'],
},
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Enter content',
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
condition: {
field: 'operation',
value: ['create', 'update', 'create_blogpost', 'update_blogpost'],
},
},
{
id: 'parentId',
@@ -813,6 +965,10 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_labels',
'get_pages_by_label',
'list_space_labels',
'get_page_descendants',
'list_space_permissions',
'list_space_properties',
'list_tasks',
],
},
},
@@ -836,6 +992,10 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_labels',
'get_pages_by_label',
'list_space_labels',
'get_page_descendants',
'list_space_permissions',
'list_space_properties',
'list_tasks',
],
},
},
@@ -921,7 +1081,27 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'confluence_list_space_labels',
// Space Tools
'confluence_get_space',
'confluence_create_space',
'confluence_update_space',
'confluence_delete_space',
'confluence_list_spaces',
// Space Property Tools
'confluence_list_space_properties',
'confluence_create_space_property',
'confluence_delete_space_property',
// Space Permission Tools
'confluence_list_space_permissions',
// Page Descendant Tools
'confluence_get_page_descendants',
// Task Tools
'confluence_list_tasks',
'confluence_get_task',
'confluence_update_task',
// Blog Post Update/Delete
'confluence_update_blogpost',
'confluence_delete_blogpost',
// User Tools
'confluence_get_user',
],
config: {
tool: (params) => {
@@ -965,6 +1145,10 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
return 'confluence_get_blogpost'
case 'create_blogpost':
return 'confluence_create_blogpost'
case 'update_blogpost':
return 'confluence_update_blogpost'
case 'delete_blogpost':
return 'confluence_delete_blogpost'
case 'list_blogposts_in_space':
return 'confluence_list_blogposts_in_space'
// Comment Operations
@@ -997,8 +1181,37 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
// Space Operations
case 'get_space':
return 'confluence_get_space'
case 'create_space':
return 'confluence_create_space'
case 'update_space':
return 'confluence_update_space'
case 'delete_space':
return 'confluence_delete_space'
case 'list_spaces':
return 'confluence_list_spaces'
// Space Property Operations
case 'list_space_properties':
return 'confluence_list_space_properties'
case 'create_space_property':
return 'confluence_create_space_property'
case 'delete_space_property':
return 'confluence_delete_space_property'
// Space Permission Operations
case 'list_space_permissions':
return 'confluence_list_space_permissions'
// Page Descendant Operations
case 'get_page_descendants':
return 'confluence_get_page_descendants'
// Task Operations
case 'list_tasks':
return 'confluence_list_tasks'
case 'get_task':
return 'confluence_get_task'
case 'update_task':
return 'confluence_update_task'
// User Operations
case 'get_user':
return 'confluence_get_user'
default:
return 'confluence_retrieve'
}
@@ -1013,6 +1226,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
attachmentComment,
blogPostId,
versionNumber,
accountId,
propertyKey,
propertyValue,
propertyId,
@@ -1022,6 +1236,15 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
purge,
bodyFormat,
cursor,
taskId,
taskStatus,
taskAssignedTo,
spaceName,
spaceKey,
spaceDescription,
spacePropertyKey,
spacePropertyValue,
spacePropertyId,
...rest
} = params
@@ -1069,8 +1292,8 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
}
// Operations that support generic cursor pagination.
// get_pages_by_label and list_space_labels have dedicated handlers
// below that pass cursor along with their required params (labelId, spaceId).
// get_pages_by_label, list_space_labels, and list_tasks have dedicated handlers
// below that pass cursor along with their required params.
const supportsCursor = [
'list_attachments',
'list_spaces',
@@ -1081,6 +1304,9 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_page_versions',
'list_page_properties',
'list_labels',
'get_page_descendants',
'list_space_permissions',
'list_space_properties',
]
if (supportsCursor.includes(operation) && cursor) {
@@ -1152,6 +1378,122 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
}
}
if (operation === 'get_user') {
return {
credential: oauthCredential,
operation,
accountId: accountId ? String(accountId).trim() : undefined,
...rest,
}
}
if (operation === 'update_blogpost' || operation === 'delete_blogpost') {
return {
credential: oauthCredential,
operation,
blogPostId: blogPostId || undefined,
...rest,
}
}
if (operation === 'create_space') {
return {
credential: oauthCredential,
operation,
name: spaceName,
key: spaceKey,
description: spaceDescription,
...rest,
}
}
if (operation === 'update_space') {
return {
credential: oauthCredential,
operation,
name: spaceName || rest.title,
description: spaceDescription,
...rest,
}
}
if (operation === 'delete_space') {
return {
credential: oauthCredential,
operation,
...rest,
}
}
if (operation === 'create_space_property') {
return {
credential: oauthCredential,
operation,
key: spacePropertyKey,
value: spacePropertyValue,
...rest,
}
}
if (operation === 'delete_space_property') {
return {
credential: oauthCredential,
operation,
propertyId: spacePropertyId,
...rest,
}
}
if (operation === 'list_space_permissions' || operation === 'list_space_properties') {
return {
credential: oauthCredential,
operation,
cursor: cursor || undefined,
...rest,
}
}
if (operation === 'get_page_descendants') {
return {
credential: oauthCredential,
pageId: effectivePageId,
operation,
cursor: cursor || undefined,
...rest,
}
}
if (operation === 'get_task') {
return {
credential: oauthCredential,
operation,
taskId,
...rest,
}
}
if (operation === 'update_task') {
return {
credential: oauthCredential,
operation,
taskId,
status: taskStatus,
...rest,
}
}
if (operation === 'list_tasks') {
return {
credential: oauthCredential,
operation,
pageId: effectivePageId || undefined,
assignedTo: taskAssignedTo || undefined,
status: taskStatus || undefined,
cursor: cursor || undefined,
...rest,
}
}
return {
credential: oauthCredential,
pageId: effectivePageId || undefined,
@@ -1171,6 +1513,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
spaceId: { type: 'string', description: 'Space identifier' },
blogPostId: { type: 'string', description: 'Blog post identifier' },
versionNumber: { type: 'number', description: 'Page version number' },
accountId: { type: 'string', description: 'Atlassian account ID' },
propertyKey: { type: 'string', description: 'Property key/name' },
propertyValue: { type: 'json', description: 'Property value (JSON)' },
title: { type: 'string', description: 'Page or blog post title' },
@@ -1192,6 +1535,15 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
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' },
taskId: { type: 'string', description: 'Task identifier' },
taskStatus: { type: 'string', description: 'Task status (complete or incomplete)' },
taskAssignedTo: { type: 'string', description: 'Filter tasks by assignee account ID' },
spaceName: { type: 'string', description: 'Space name for create/update' },
spaceKey: { type: 'string', description: 'Space key for create' },
spaceDescription: { type: 'string', description: 'Space description' },
spacePropertyKey: { type: 'string', description: 'Space property key' },
spacePropertyValue: { type: 'json', description: 'Space property value' },
spacePropertyId: { type: 'string', description: 'Space property identifier' },
},
outputs: {
ts: { type: 'string', description: 'Timestamp' },
@@ -1242,6 +1594,23 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
propertyId: { type: 'string', description: 'Property identifier' },
propertyKey: { type: 'string', description: 'Property key' },
propertyValue: { type: 'json', description: 'Property value' },
// User Results
accountId: { type: 'string', description: 'Atlassian account ID' },
displayName: { type: 'string', description: 'User display name' },
email: { type: 'string', description: 'User email address' },
accountType: { type: 'string', description: 'Account type (atlassian, app, customer)' },
profilePicture: { type: 'string', description: 'Path to user profile picture' },
publicName: { type: 'string', description: 'User public name' },
// Task Results
tasks: { type: 'array', description: 'List of tasks' },
taskId: { type: 'string', description: 'Task identifier' },
// Descendant Results
descendants: { type: 'array', description: 'List of descendant pages' },
// Permission Results
permissions: { type: 'array', description: 'List of space permissions' },
// Space Property Results
homepageId: { type: 'string', description: 'Space homepage ID' },
description: { type: 'json', description: 'Space description' },
// Pagination
nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' },
},

View File

@@ -1846,6 +1846,15 @@ export const auth = betterAuth({
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
'read:task:confluence',
'write:task:confluence',
'delete:blogpost:confluence',
'write:space:confluence',
'delete:space:confluence',
'read:space.property:confluence',
'write:space.property:confluence',
'read:space.permission:confluence',
],
responseType: 'code',
pkce: true,

View File

@@ -330,6 +330,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'search:confluence',
'read:me',
'offline_access',
'read:blogpost:confluence',
'write:blogpost:confluence',
'delete:blogpost:confluence',
'read:content.property:confluence',
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
'read:task:confluence',
'write:task:confluence',
'write:space:confluence',
'delete:space:confluence',
'read:space.property:confluence',
'write:space.property:confluence',
'read:space.permission:confluence',
],
},
},

View File

@@ -270,9 +270,7 @@ async function auditWorkflowLockToggle(workflowId: string, actorId: string): Pro
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,
resourceName: wf.name,
description: allLocked
? `Locked workflow "${wf.name}"`
: `Unlocked workflow "${wf.name}"`,
description: allLocked ? `Locked workflow "${wf.name}"` : `Unlocked workflow "${wf.name}"`,
metadata: { blockCount: blocks.length },
})
}

View File

@@ -0,0 +1,134 @@
import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceCreateSpaceParams {
accessToken: string
domain: string
name: string
key: string
description?: string
cloudId?: string
}
export interface ConfluenceCreateSpaceResponse {
success: boolean
output: {
ts: string
spaceId: string
name: string
key: string
type: string
status: string
url: string
homepageId: string | null
description: { value: string; representation: string } | null
}
}
export const confluenceCreateSpaceTool: ToolConfig<
ConfluenceCreateSpaceParams,
ConfluenceCreateSpaceResponse
> = {
id: 'confluence_create_space',
name: 'Confluence Create Space',
description: 'Create a new 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)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new space',
},
key: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Unique key for the space (uppercase, no spaces)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description for the new space',
},
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',
method: 'POST',
headers: (params: ConfluenceCreateSpaceParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceCreateSpaceParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
name: params.name,
key: params.key,
description: params.description,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.id ?? '',
name: data.name ?? '',
key: data.key ?? '',
type: data.type ?? '',
status: data.status ?? '',
url: data._links?.webui ?? '',
homepageId: data.homepageId ?? null,
description: data.description ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Created 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: 'URL to view the space' },
homepageId: { type: 'string', description: 'Homepage ID', optional: true },
description: {
type: 'object',
description: 'Space description',
properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
optional: true,
},
},
}

View File

@@ -0,0 +1,118 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceCreateSpacePropertyParams {
accessToken: string
domain: string
spaceId: string
key: string
value?: unknown
cloudId?: string
}
export interface ConfluenceCreateSpacePropertyResponse {
success: boolean
output: {
ts: string
propertyId: string
key: string
value: unknown
spaceId: string
}
}
export const confluenceCreateSpacePropertyTool: ToolConfig<
ConfluenceCreateSpacePropertyParams,
ConfluenceCreateSpacePropertyResponse
> = {
id: 'confluence_create_space_property',
name: 'Confluence Create Space Property',
description: 'Create a property on 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: 'Space ID to create the property on',
},
key: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Property key/name',
},
value: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Property value (JSON)',
},
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-properties',
method: 'POST',
headers: (params: ConfluenceCreateSpacePropertyParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceCreateSpacePropertyParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
action: 'create',
key: params.key,
value: params.value,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
propertyId: data.propertyId ?? '',
key: data.key ?? '',
value: data.value ?? null,
spaceId: data.spaceId ?? '',
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
propertyId: { type: 'string', description: 'Created property ID' },
key: { type: 'string', description: 'Property key' },
value: { type: 'json', description: 'Property value' },
spaceId: { type: 'string', description: 'Space ID' },
},
}

View File

@@ -0,0 +1,95 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceDeleteBlogPostParams {
accessToken: string
domain: string
blogPostId: string
cloudId?: string
}
export interface ConfluenceDeleteBlogPostResponse {
success: boolean
output: {
ts: string
blogPostId: string
deleted: boolean
}
}
export const confluenceDeleteBlogPostTool: ToolConfig<
ConfluenceDeleteBlogPostParams,
ConfluenceDeleteBlogPostResponse
> = {
id: 'confluence_delete_blogpost',
name: 'Confluence Delete Blog Post',
description: 'Delete a Confluence blog post.',
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 delete',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/blogposts',
method: 'DELETE',
headers: (params: ConfluenceDeleteBlogPostParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceDeleteBlogPostParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
blogPostId: params.blogPostId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
blogPostId: data.blogPostId ?? '',
deleted: true,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
blogPostId: { type: 'string', description: 'Deleted blog post ID' },
deleted: { type: 'boolean', description: 'Deletion status' },
},
}

View File

@@ -0,0 +1,95 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceDeleteSpaceParams {
accessToken: string
domain: string
spaceId: string
cloudId?: string
}
export interface ConfluenceDeleteSpaceResponse {
success: boolean
output: {
ts: string
spaceId: string
deleted: boolean
}
}
export const confluenceDeleteSpaceTool: ToolConfig<
ConfluenceDeleteSpaceParams,
ConfluenceDeleteSpaceResponse
> = {
id: 'confluence_delete_space',
name: 'Confluence Delete Space',
description: 'Delete 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: 'ID of the space to delete',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space',
method: 'DELETE',
headers: (params: ConfluenceDeleteSpaceParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceDeleteSpaceParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.spaceId ?? '',
deleted: true,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Deleted space ID' },
deleted: { type: 'boolean', description: 'Deletion status' },
},
}

View File

@@ -0,0 +1,107 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceDeleteSpacePropertyParams {
accessToken: string
domain: string
spaceId: string
propertyId: string
cloudId?: string
}
export interface ConfluenceDeleteSpacePropertyResponse {
success: boolean
output: {
ts: string
spaceId: string
propertyId: string
deleted: boolean
}
}
export const confluenceDeleteSpacePropertyTool: ToolConfig<
ConfluenceDeleteSpacePropertyParams,
ConfluenceDeleteSpacePropertyResponse
> = {
id: 'confluence_delete_space_property',
name: 'Confluence Delete Space Property',
description: 'Delete a property from 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: 'Space ID the property belongs to',
},
propertyId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Property ID to delete',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space-properties',
method: 'POST',
headers: (params: ConfluenceDeleteSpacePropertyParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceDeleteSpacePropertyParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
action: 'delete',
propertyId: params.propertyId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.spaceId ?? '',
propertyId: data.propertyId ?? '',
deleted: true,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Space ID' },
propertyId: { type: 'string', description: 'Deleted property ID' },
deleted: { type: 'boolean', description: 'Deletion status' },
},
}

View File

@@ -0,0 +1,147 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceGetPageDescendantsParams {
accessToken: string
domain: string
pageId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceGetPageDescendantsResponse {
success: boolean
output: {
ts: string
descendants: Array<{
id: string
title: string
type: string | null
status: string | null
spaceId: string | null
parentId: string | null
childPosition: number | null
depth: number | null
}>
pageId: string
nextCursor: string | null
}
}
export const confluenceGetPageDescendantsTool: ToolConfig<
ConfluenceGetPageDescendantsParams,
ConfluenceGetPageDescendantsResponse
> = {
id: 'confluence_get_page_descendants',
name: 'Confluence Get Page Descendants',
description: 'Get all descendants of a Confluence page recursively.',
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: 'Page ID to get descendants for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of descendants 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-descendants',
method: 'POST',
headers: (params: ConfluenceGetPageDescendantsParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceGetPageDescendantsParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
pageId: params.pageId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
descendants: data.descendants || [],
pageId: data.pageId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
descendants: {
type: 'array',
description: 'Array of descendant pages',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Page ID' },
title: { type: 'string', description: 'Page title' },
type: {
type: 'string',
description: 'Content type (page, whiteboard, database, etc.)',
optional: true,
},
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 },
childPosition: { type: 'number', description: 'Position among siblings', optional: true },
depth: { type: 'number', description: 'Depth in the hierarchy', optional: true },
},
},
},
pageId: { type: 'string', description: 'Parent page ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,130 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceGetTaskParams {
accessToken: string
domain: string
taskId: string
cloudId?: string
}
export interface ConfluenceGetTaskResponse {
success: boolean
output: {
ts: string
id: string
localId: string | null
spaceId: string | null
pageId: string | null
blogPostId: string | null
status: string
body: string | null
createdBy: string | null
assignedTo: string | null
completedBy: string | null
createdAt: string | null
updatedAt: string | null
dueAt: string | null
completedAt: string | null
}
}
export const confluenceGetTaskTool: ToolConfig<ConfluenceGetTaskParams, ConfluenceGetTaskResponse> =
{
id: 'confluence_get_task',
name: 'Confluence Get Task',
description: 'Get a specific Confluence inline task by ID.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to retrieve',
},
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/tasks',
method: 'POST',
headers: (params: ConfluenceGetTaskParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceGetTaskParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
taskId: params.taskId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const task = data.task || data
return {
success: true,
output: {
ts: new Date().toISOString(),
id: task.id ?? '',
localId: task.localId ?? null,
spaceId: task.spaceId ?? null,
pageId: task.pageId ?? null,
blogPostId: task.blogPostId ?? null,
status: task.status ?? '',
body: task.body ?? null,
createdBy: task.createdBy ?? null,
assignedTo: task.assignedTo ?? null,
completedBy: task.completedBy ?? null,
createdAt: task.createdAt ?? null,
updatedAt: task.updatedAt ?? null,
dueAt: task.dueAt ?? null,
completedAt: task.completedAt ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
id: { type: 'string', description: 'Task ID' },
localId: { type: 'string', description: 'Local task ID', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
pageId: { type: 'string', description: 'Page ID', optional: true },
blogPostId: { type: 'string', description: 'Blog post ID', optional: true },
status: { type: 'string', description: 'Task status (complete or incomplete)' },
body: { type: 'string', description: 'Task body content in storage format', optional: true },
createdBy: { type: 'string', description: 'Creator account ID', optional: true },
assignedTo: { type: 'string', description: 'Assignee account ID', optional: true },
completedBy: { type: 'string', description: 'Completer account ID', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
dueAt: { type: 'string', description: 'Due date', optional: true },
completedAt: { type: 'string', description: 'Completion timestamp', optional: true },
},
}

View File

@@ -0,0 +1,113 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceGetUserParams {
accessToken: string
domain: string
accountId: string
cloudId?: string
}
export interface ConfluenceGetUserResponse {
success: boolean
output: {
ts: string
accountId: string
displayName: string
email: string | null
accountType: string | null
profilePicture: string | null
publicName: string | null
}
}
export const confluenceGetUserTool: ToolConfig<ConfluenceGetUserParams, ConfluenceGetUserResponse> =
{
id: 'confluence_get_user',
name: 'Confluence Get User',
description: 'Get display name and profile info for a Confluence user by account ID.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Atlassian account ID of the user to look up',
},
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/user',
method: 'POST',
headers: (params: ConfluenceGetUserParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceGetUserParams) => ({
domain: params.domain,
accessToken: params.accessToken,
accountId: params.accountId?.trim(),
cloudId: params.cloudId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
accountId: data.accountId ?? '',
displayName: data.displayName ?? '',
email: data.email ?? null,
accountType: data.accountType ?? null,
profilePicture: data.profilePicture?.path ?? null,
publicName: data.publicName ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
accountId: { type: 'string', description: 'Atlassian account ID of the user' },
displayName: { type: 'string', description: 'Display name of the user' },
email: { type: 'string', description: 'Email address of the user', optional: true },
accountType: {
type: 'string',
description: 'Account type (e.g., atlassian, app, customer)',
optional: true,
},
profilePicture: {
type: 'string',
description: 'Path to the user profile picture',
optional: true,
},
publicName: { type: 'string', description: 'Public name of the user', optional: true },
},
}

View File

@@ -3,17 +3,25 @@ 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 { confluenceCreateSpaceTool } from '@/tools/confluence/create_space'
import { confluenceCreateSpacePropertyTool } from '@/tools/confluence/create_space_property'
import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment'
import { confluenceDeleteBlogPostTool } from '@/tools/confluence/delete_blogpost'
import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment'
import { confluenceDeleteLabelTool } from '@/tools/confluence/delete_label'
import { confluenceDeletePageTool } from '@/tools/confluence/delete_page'
import { confluenceDeletePagePropertyTool } from '@/tools/confluence/delete_page_property'
import { confluenceDeleteSpaceTool } from '@/tools/confluence/delete_space'
import { confluenceDeleteSpacePropertyTool } from '@/tools/confluence/delete_space_property'
import { confluenceGetBlogPostTool } from '@/tools/confluence/get_blogpost'
import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ancestors'
import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children'
import { confluenceGetPageDescendantsTool } from '@/tools/confluence/get_page_descendants'
import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version'
import { confluenceGetPagesByLabelTool } from '@/tools/confluence/get_pages_by_label'
import { confluenceGetSpaceTool } from '@/tools/confluence/get_space'
import { confluenceGetTaskTool } from '@/tools/confluence/get_task'
import { confluenceGetUserTool } from '@/tools/confluence/get_user'
import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments'
import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts'
import { confluenceListBlogPostsInSpaceTool } from '@/tools/confluence/list_blogposts_in_space'
@@ -23,7 +31,10 @@ import { confluenceListPagePropertiesTool } from '@/tools/confluence/list_page_p
import { confluenceListPageVersionsTool } from '@/tools/confluence/list_page_versions'
import { confluenceListPagesInSpaceTool } from '@/tools/confluence/list_pages_in_space'
import { confluenceListSpaceLabelsTool } from '@/tools/confluence/list_space_labels'
import { confluenceListSpacePermissionsTool } from '@/tools/confluence/list_space_permissions'
import { confluenceListSpacePropertiesTool } from '@/tools/confluence/list_space_properties'
import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces'
import { confluenceListTasksTool } from '@/tools/confluence/list_tasks'
import { confluenceRetrieveTool } from '@/tools/confluence/retrieve'
import { confluenceSearchTool } from '@/tools/confluence/search'
import { confluenceSearchInSpaceTool } from '@/tools/confluence/search_in_space'
@@ -64,7 +75,10 @@ import {
VERSION_OUTPUT_PROPERTIES,
} from '@/tools/confluence/types'
import { confluenceUpdateTool } from '@/tools/confluence/update'
import { confluenceUpdateBlogPostTool } from '@/tools/confluence/update_blogpost'
import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment'
import { confluenceUpdateSpaceTool } from '@/tools/confluence/update_space'
import { confluenceUpdateTaskTool } from '@/tools/confluence/update_task'
import { confluenceUploadAttachmentTool } from '@/tools/confluence/upload_attachment'
export {
@@ -76,6 +90,7 @@ export {
confluenceListPagesInSpaceTool,
confluenceGetPageChildrenTool,
confluenceGetPageAncestorsTool,
confluenceGetPageDescendantsTool,
// Page Version Tools
confluenceListPageVersionsTool,
confluenceGetPageVersionTool,
@@ -87,6 +102,8 @@ export {
confluenceListBlogPostsTool,
confluenceGetBlogPostTool,
confluenceCreateBlogPostTool,
confluenceUpdateBlogPostTool,
confluenceDeleteBlogPostTool,
confluenceListBlogPostsInSpaceTool,
// Search Tools
confluenceSearchTool,
@@ -106,9 +123,24 @@ export {
confluenceDeleteLabelTool,
confluenceGetPagesByLabelTool,
confluenceListSpaceLabelsTool,
// User Tools
confluenceGetUserTool,
// Space Tools
confluenceGetSpaceTool,
confluenceCreateSpaceTool,
confluenceUpdateSpaceTool,
confluenceDeleteSpaceTool,
confluenceListSpacesTool,
// Space Property Tools
confluenceListSpacePropertiesTool,
confluenceCreateSpacePropertyTool,
confluenceDeleteSpacePropertyTool,
// Space Permission Tools
confluenceListSpacePermissionsTool,
// Task Tools
confluenceListTasksTool,
confluenceGetTaskTool,
confluenceUpdateTaskTool,
// Item property constants (for use in outputs)
ATTACHMENT_ITEM_PROPERTIES,
COMMENT_ITEM_PROPERTIES,

View File

@@ -0,0 +1,156 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListSpacePermissionsParams {
accessToken: string
domain: string
spaceId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListSpacePermissionsResponse {
success: boolean
output: {
ts: string
permissions: Array<{
id: string
principalType: string | null
principalId: string | null
operationKey: string | null
operationTargetType: string | null
anonymousAccess: boolean
unlicensedAccess: boolean
}>
spaceId: string
nextCursor: string | null
}
}
export const confluenceListSpacePermissionsTool: ToolConfig<
ConfluenceListSpacePermissionsParams,
ConfluenceListSpacePermissionsResponse
> = {
id: 'confluence_list_space_permissions',
name: 'Confluence List Space Permissions',
description: 'List permissions for 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: 'Space ID to list permissions for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of permissions 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/space-permissions',
method: 'POST',
headers: (params: ConfluenceListSpacePermissionsParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListSpacePermissionsParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
permissions: data.permissions || [],
spaceId: data.spaceId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
permissions: {
type: 'array',
description: 'Array of space permissions',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Permission ID' },
principalType: {
type: 'string',
description: 'Principal type (user, group, role)',
optional: true,
},
principalId: { type: 'string', description: 'Principal ID', optional: true },
operationKey: {
type: 'string',
description: 'Operation key (read, create, delete, etc.)',
optional: true,
},
operationTargetType: {
type: 'string',
description: 'Target type (page, blogpost, space, etc.)',
optional: true,
},
anonymousAccess: { type: 'boolean', description: 'Whether anonymous access is allowed' },
unlicensedAccess: {
type: 'boolean',
description: 'Whether unlicensed access is allowed',
},
},
},
},
spaceId: { type: 'string', description: 'Space ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,133 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListSpacePropertiesParams {
accessToken: string
domain: string
spaceId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListSpacePropertiesResponse {
success: boolean
output: {
ts: string
properties: Array<{
id: string
key: string
value: unknown
}>
spaceId: string
nextCursor: string | null
}
}
export const confluenceListSpacePropertiesTool: ToolConfig<
ConfluenceListSpacePropertiesParams,
ConfluenceListSpacePropertiesResponse
> = {
id: 'confluence_list_space_properties',
name: 'Confluence List Space Properties',
description: 'List properties on 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: 'Space ID to list properties for',
},
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: () => '/api/tools/confluence/space-properties',
method: 'POST',
headers: (params: ConfluenceListSpacePropertiesParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListSpacePropertiesParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
properties: data.properties || [],
spaceId: data.spaceId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
properties: {
type: 'array',
description: 'Array of space properties',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Property ID' },
key: { type: 'string', description: 'Property key' },
value: { type: 'json', description: 'Property value' },
},
},
},
spaceId: { type: 'string', description: 'Space ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,181 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListTasksParams {
accessToken: string
domain: string
pageId?: string
spaceId?: string
assignedTo?: string
status?: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListTasksResponse {
success: boolean
output: {
ts: string
tasks: Array<{
id: string
localId: string | null
spaceId: string | null
pageId: string | null
blogPostId: string | null
status: string
body: string | null
createdBy: string | null
assignedTo: string | null
completedBy: string | null
createdAt: string | null
updatedAt: string | null
dueAt: string | null
completedAt: string | null
}>
nextCursor: string | null
}
}
export const confluenceListTasksTool: ToolConfig<
ConfluenceListTasksParams,
ConfluenceListTasksResponse
> = {
id: 'confluence_list_tasks',
name: 'Confluence List Tasks',
description:
'List inline tasks from Confluence. Optionally filter by page, space, assignee, or 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)',
},
pageId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by page ID',
},
spaceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by space ID',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by assignee account ID',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by status (complete or incomplete)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tasks 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/tasks',
method: 'POST',
headers: (params: ConfluenceListTasksParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListTasksParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
pageId: params.pageId,
spaceId: params.spaceId,
assignedTo: params.assignedTo,
status: params.status,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
tasks: data.tasks || [],
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
tasks: {
type: 'array',
description: 'Array of Confluence tasks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
localId: { type: 'string', description: 'Local task ID', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
pageId: { type: 'string', description: 'Page ID', optional: true },
blogPostId: { type: 'string', description: 'Blog post ID', optional: true },
status: { type: 'string', description: 'Task status (complete or incomplete)' },
body: {
type: 'string',
description: 'Task body content in storage format',
optional: true,
},
createdBy: { type: 'string', description: 'Creator account ID', optional: true },
assignedTo: { type: 'string', description: 'Assignee account ID', optional: true },
completedBy: { type: 'string', description: 'Completer account ID', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
dueAt: { type: 'string', description: 'Due date', optional: true },
completedAt: { type: 'string', description: 'Completion timestamp', optional: true },
},
},
},
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,123 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateBlogPostParams {
accessToken: string
domain: string
blogPostId: string
title?: string
content?: string
cloudId?: string
}
export interface ConfluenceUpdateBlogPostResponse {
success: boolean
output: {
ts: string
blogPostId: string
title: string
status: string | null
spaceId: string | null
version: Record<string, unknown> | null
url: string
}
}
export const confluenceUpdateBlogPostTool: ToolConfig<
ConfluenceUpdateBlogPostParams,
ConfluenceUpdateBlogPostResponse
> = {
id: 'confluence_update_blogpost',
name: 'Confluence Update Blog Post',
description: 'Update an existing Confluence blog post title and/or 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 update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the blog post',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New content for the blog post in storage format',
},
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: 'PUT',
headers: (params: ConfluenceUpdateBlogPostParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateBlogPostParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
blogPostId: params.blogPostId,
title: params.title,
content: params.content,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
blogPostId: data.id ?? '',
title: data.title ?? '',
status: data.status ?? null,
spaceId: data.spaceId ?? null,
version: data.version ?? null,
url: data._links?.webui ?? '',
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
blogPostId: { type: 'string', description: 'Updated 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 },
version: { type: 'json', description: 'Version information', optional: true },
url: { type: 'string', description: 'URL to view the blog post' },
},
}

View File

@@ -0,0 +1,131 @@
import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateSpaceParams {
accessToken: string
domain: string
spaceId: string
name?: string
description?: string
cloudId?: string
}
export interface ConfluenceUpdateSpaceResponse {
success: boolean
output: {
ts: string
spaceId: string
name: string
key: string
type: string
status: string
url: string
description: { value: string; representation: string } | null
}
}
export const confluenceUpdateSpaceTool: ToolConfig<
ConfluenceUpdateSpaceParams,
ConfluenceUpdateSpaceResponse
> = {
id: 'confluence_update_space',
name: 'Confluence Update Space',
description: 'Update a Confluence space name or description.',
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: 'ID of the space to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New name for the space',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New description for the space',
},
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',
method: 'PUT',
headers: (params: ConfluenceUpdateSpaceParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateSpaceParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
name: params.name,
description: params.description,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.id ?? '',
name: data.name ?? '',
key: data.key ?? '',
type: data.type ?? '',
status: data.status ?? '',
url: data._links?.webui ?? '',
description: data.description ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Updated 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: 'URL to view the space' },
description: {
type: 'object',
description: 'Space description',
properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
optional: true,
},
},
}

View File

@@ -0,0 +1,141 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateTaskParams {
accessToken: string
domain: string
taskId: string
status: string
cloudId?: string
}
export interface ConfluenceUpdateTaskResponse {
success: boolean
output: {
ts: string
id: string
localId: string | null
spaceId: string | null
pageId: string | null
blogPostId: string | null
status: string
body: string | null
createdBy: string | null
assignedTo: string | null
completedBy: string | null
createdAt: string | null
updatedAt: string | null
dueAt: string | null
completedAt: string | null
}
}
export const confluenceUpdateTaskTool: ToolConfig<
ConfluenceUpdateTaskParams,
ConfluenceUpdateTaskResponse
> = {
id: 'confluence_update_task',
name: 'Confluence Update Task',
description: 'Update the status of a Confluence inline task (complete or incomplete).',
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)',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to update',
},
status: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New status for the task (complete or incomplete)',
},
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/tasks',
method: 'POST',
headers: (params: ConfluenceUpdateTaskParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateTaskParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
action: 'update',
taskId: params.taskId,
status: params.status,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const task = data.task || data
return {
success: true,
output: {
ts: new Date().toISOString(),
id: task.id ?? '',
localId: task.localId ?? null,
spaceId: task.spaceId ?? null,
pageId: task.pageId ?? null,
blogPostId: task.blogPostId ?? null,
status: task.status ?? '',
body: task.body ?? null,
createdBy: task.createdBy ?? null,
assignedTo: task.assignedTo ?? null,
completedBy: task.completedBy ?? null,
createdAt: task.createdAt ?? null,
updatedAt: task.updatedAt ?? null,
dueAt: task.dueAt ?? null,
completedAt: task.completedAt ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
id: { type: 'string', description: 'Task ID' },
localId: { type: 'string', description: 'Local task ID', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
pageId: { type: 'string', description: 'Page ID', optional: true },
blogPostId: { type: 'string', description: 'Blog post ID', optional: true },
status: { type: 'string', description: 'Updated task status' },
body: { type: 'string', description: 'Task body content in storage format', optional: true },
createdBy: { type: 'string', description: 'Creator account ID', optional: true },
assignedTo: { type: 'string', description: 'Assignee account ID', optional: true },
completedBy: { type: 'string', description: 'Completer account ID', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
dueAt: { type: 'string', description: 'Due date', optional: true },
completedAt: { type: 'string', description: 'Completion timestamp', optional: true },
},
}

View File

@@ -190,17 +190,25 @@ import {
confluenceCreateCommentTool,
confluenceCreatePagePropertyTool,
confluenceCreatePageTool,
confluenceCreateSpacePropertyTool,
confluenceCreateSpaceTool,
confluenceDeleteAttachmentTool,
confluenceDeleteBlogPostTool,
confluenceDeleteCommentTool,
confluenceDeleteLabelTool,
confluenceDeletePagePropertyTool,
confluenceDeletePageTool,
confluenceDeleteSpacePropertyTool,
confluenceDeleteSpaceTool,
confluenceGetBlogPostTool,
confluenceGetPageAncestorsTool,
confluenceGetPageChildrenTool,
confluenceGetPageDescendantsTool,
confluenceGetPagesByLabelTool,
confluenceGetPageVersionTool,
confluenceGetSpaceTool,
confluenceGetTaskTool,
confluenceGetUserTool,
confluenceListAttachmentsTool,
confluenceListBlogPostsInSpaceTool,
confluenceListBlogPostsTool,
@@ -210,11 +218,17 @@ import {
confluenceListPagesInSpaceTool,
confluenceListPageVersionsTool,
confluenceListSpaceLabelsTool,
confluenceListSpacePermissionsTool,
confluenceListSpacePropertiesTool,
confluenceListSpacesTool,
confluenceListTasksTool,
confluenceRetrieveTool,
confluenceSearchInSpaceTool,
confluenceSearchTool,
confluenceUpdateBlogPostTool,
confluenceUpdateCommentTool,
confluenceUpdateSpaceTool,
confluenceUpdateTaskTool,
confluenceUpdateTool,
confluenceUploadAttachmentTool,
} from '@/tools/confluence'
@@ -3032,8 +3046,22 @@ export const tools: Record<string, ToolConfig> = {
confluence_list_space_labels: confluenceListSpaceLabelsTool,
confluence_delete_label: confluenceDeleteLabelTool,
confluence_delete_page_property: confluenceDeletePagePropertyTool,
confluence_get_page_descendants: confluenceGetPageDescendantsTool,
confluence_get_space: confluenceGetSpaceTool,
confluence_create_space: confluenceCreateSpaceTool,
confluence_update_space: confluenceUpdateSpaceTool,
confluence_delete_space: confluenceDeleteSpaceTool,
confluence_get_user: confluenceGetUserTool,
confluence_list_spaces: confluenceListSpacesTool,
confluence_update_blogpost: confluenceUpdateBlogPostTool,
confluence_delete_blogpost: confluenceDeleteBlogPostTool,
confluence_list_tasks: confluenceListTasksTool,
confluence_get_task: confluenceGetTaskTool,
confluence_update_task: confluenceUpdateTaskTool,
confluence_list_space_permissions: confluenceListSpacePermissionsTool,
confluence_list_space_properties: confluenceListSpacePropertiesTool,
confluence_create_space_property: confluenceCreateSpacePropertyTool,
confluence_delete_space_property: confluenceDeleteSpacePropertyTool,
cursor_list_agents: cursorListAgentsTool,
cursor_list_agents_v2: cursorListAgentsV2Tool,
cursor_get_agent: cursorGetAgentTool,

View File

@@ -13,7 +13,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.10",
"turbo": "2.8.11",
},
},
"apps/docs": {
@@ -3437,19 +3437,19 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.10", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.10", "turbo-darwin-arm64": "2.8.10", "turbo-linux-64": "2.8.10", "turbo-linux-arm64": "2.8.10", "turbo-windows-64": "2.8.10", "turbo-windows-arm64": "2.8.10" }, "bin": { "turbo": "bin/turbo" } }, "sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ=="],
"turbo": ["turbo@2.8.11", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.11", "turbo-darwin-arm64": "2.8.11", "turbo-linux-64": "2.8.11", "turbo-linux-arm64": "2.8.11", "turbo-windows-64": "2.8.11", "turbo-windows-arm64": "2.8.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-H+rwSHHPLoyPOSoHdmI1zY0zy0GGj1Dmr7SeJW+nZiWLz2nex8EJ+fkdVabxXFMNEux+aywI4Sae8EqhmnOv4A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-XKaCWaz4OCt77oYYvGCIRpvYD4c/aNaKjRkUpv+e8rN3RZb+5Xsyew4yRO+gaHdMIUhQznXNXfHlhs+/p7lIhA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VvynLHGUNvQ9k7GZjRPSsRcK4VkioTfFb7O7liAk4nHKjEcMdls7GqxzjVWgJiKz3hWmQGaP9hRa9UUnhVWCxA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.10", "", { "os": "linux", "cpu": "x64" }, "sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cbSn37dcm+EmkQ7DD0euy7xV7o2el4GAOr1XujvkAyKjjNvQ+6QIUeDgQcwAx3D17zPpDvfDMJY2dLQadWnkmQ=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+trymp2s2aBrhS04l6qFxcExzZ8ffndevuUB9c5RCeqsVpZeiWuGQlWNm5XjOmzoMayxRARZ5ma7yiWbGMiLqQ=="],
"turbo-windows-64": ["turbo-windows-64@2.8.10", "", { "os": "win32", "cpu": "x64" }, "sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw=="],
"turbo-windows-64": ["turbo-windows-64@2.8.11", "", { "os": "win32", "cpu": "x64" }, "sha512-3kJjFSM4yw1n9Uzmi+XkAUgCae19l/bH6RJ442xo7mnZm0tpOjo33F+FYHoSVpIWVMd0HG0LDccyafPSdylQbA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOM4uF2vuLsJUvibdR6X9QqdZr6BhC6Nhlrw4LKFPsXZZI/9HHLoqAiYRpE4MuzIwldCH/jVySnWXrI1SKto0g=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],

View File

@@ -42,7 +42,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.10"
"turbo": "2.8.11"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [

View File

@@ -86,6 +86,8 @@ export const auditMock = {
WORKFLOW_DEPLOYED: 'workflow.deployed',
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
WORKFLOW_DUPLICATED: 'workflow.duplicated',
WORKFLOW_LOCKED: 'workflow.locked',
WORKFLOW_UNLOCKED: 'workflow.unlocked',
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',