From 552dc56fc39598e71696f738e6313026879d770d Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 4 Feb 2026 19:46:28 -0800 Subject: [PATCH] feat(confluence): added more confluence endpoints (#3139) * feat(confluence): added more confluence endpoints * update license * updated * updated docs --- apps/docs/components/ui/icon-mapping.ts | 18 +- apps/docs/content/docs/de/tools/file.mdx | 2 +- apps/docs/content/docs/de/tools/fireflies.mdx | 2 +- .../content/docs/de/tools/mistral_parse.mdx | 2 +- .../docs/content/docs/en/tools/confluence.mdx | 539 +++++++++++++++++- apps/docs/content/docs/en/tools/discord.mdx | 1 + apps/docs/content/docs/en/tools/dropbox.mdx | 12 +- apps/docs/content/docs/en/tools/file.mdx | 11 +- apps/docs/content/docs/en/tools/fireflies.mdx | 2 +- apps/docs/content/docs/en/tools/github.mdx | 1 + .../content/docs/en/tools/google_drive.mdx | 6 +- .../content/docs/en/tools/google_slides.mdx | 2 +- apps/docs/content/docs/en/tools/jira.mdx | 22 + apps/docs/content/docs/en/tools/linear.mdx | 3 +- .../content/docs/en/tools/microsoft_teams.mdx | 2 + .../content/docs/en/tools/mistral_parse.mdx | 32 +- apps/docs/content/docs/en/tools/notion.mdx | 20 + apps/docs/content/docs/en/tools/pipedrive.mdx | 2 + apps/docs/content/docs/en/tools/pulse.mdx | 21 +- apps/docs/content/docs/en/tools/reducto.mdx | 18 +- apps/docs/content/docs/en/tools/s3.mdx | 1 + apps/docs/content/docs/en/tools/sendgrid.mdx | 2 +- apps/docs/content/docs/en/tools/sftp.mdx | 1 + apps/docs/content/docs/en/tools/slack.mdx | 1 + apps/docs/content/docs/en/tools/ssh.mdx | 1 + apps/docs/content/docs/en/tools/stt.mdx | 78 +-- apps/docs/content/docs/en/tools/telegram.mdx | 1 + apps/docs/content/docs/en/tools/textract.mdx | 41 +- .../content/docs/en/tools/twilio_voice.mdx | 1 + apps/docs/content/docs/en/tools/typeform.mdx | 1 + .../content/docs/en/tools/video_generator.mdx | 12 +- apps/docs/content/docs/en/tools/vision.mdx | 14 +- apps/docs/content/docs/en/tools/zoom.mdx | 2 + apps/docs/content/docs/es/tools/file.mdx | 2 +- apps/docs/content/docs/es/tools/fireflies.mdx | 2 +- .../content/docs/es/tools/mistral_parse.mdx | 2 +- apps/docs/content/docs/fr/tools/file.mdx | 2 +- apps/docs/content/docs/fr/tools/fireflies.mdx | 2 +- .../content/docs/fr/tools/mistral_parse.mdx | 2 +- apps/docs/content/docs/ja/tools/file.mdx | 2 +- apps/docs/content/docs/ja/tools/fireflies.mdx | 2 +- .../content/docs/ja/tools/mistral_parse.mdx | 2 +- apps/docs/content/docs/zh/tools/file.mdx | 2 +- apps/docs/content/docs/zh/tools/fireflies.mdx | 2 +- .../content/docs/zh/tools/mistral_parse.mdx | 2 +- .../app/(auth)/components/support-footer.tsx | 2 +- apps/sim/app/(landing)/components/nav/nav.tsx | 2 +- apps/sim/app/api/a2a/serve/[agentId]/route.ts | 2 +- .../api/tools/confluence/attachments/route.ts | 23 +- .../api/tools/confluence/blogposts/route.ts | 285 +++++++++ .../api/tools/confluence/comments/route.ts | 41 +- .../app/api/tools/confluence/labels/route.ts | 38 +- .../tools/confluence/page-ancestors/route.ts | 96 ++++ .../tools/confluence/page-children/route.ts | 104 ++++ .../tools/confluence/page-properties/route.ts | 365 ++++++++++++ .../tools/confluence/page-versions/route.ts | 151 +++++ .../app/api/tools/confluence/page/route.ts | 30 +- .../app/api/tools/confluence/pages/route.ts | 4 - .../tools/confluence/search-in-space/route.ts | 120 ++++ .../app/api/tools/confluence/search/route.ts | 32 +- .../tools/confluence/space-blogposts/route.ts | 124 ++++ .../api/tools/confluence/space-pages/route.ts | 125 ++++ .../app/api/tools/confluence/spaces/route.ts | 19 +- .../sim/app/chat/components/header/header.tsx | 2 +- .../components/powered-by-sim.tsx | 2 +- apps/sim/app/layout.tsx | 7 +- apps/sim/app/manifest.ts | 2 +- .../[executionId]/resume-page-client.tsx | 2 +- .../components/oauth-required-modal.tsx | 7 + .../components/terminal/terminal.tsx | 4 +- .../components/general/general.tsx | 2 +- .../usage-indicator/usage-indicator.tsx | 2 +- apps/sim/blocks/blocks/confluence.ts | 447 ++++++++++++++- apps/sim/components/branded-layout.tsx | 2 +- .../emails/auth/otp-verification-email.tsx | 2 +- .../emails/auth/reset-password-email.tsx | 2 +- .../components/emails/auth/welcome-email.tsx | 2 +- .../emails/billing/credit-purchase-email.tsx | 2 +- .../billing/enterprise-subscription-email.tsx | 2 +- .../billing/free-tier-upgrade-email.tsx | 2 +- .../emails/billing/payment-failed-email.tsx | 2 +- .../emails/billing/plan-welcome-email.tsx | 2 +- .../emails/billing/usage-threshold-email.tsx | 2 +- .../careers/careers-confirmation-email.tsx | 2 +- .../emails/components/email-footer.tsx | 2 +- .../emails/components/email-layout.tsx | 2 +- .../invitations/batch-invitation-email.tsx | 2 +- .../emails/invitations/invitation-email.tsx | 2 +- .../polling-group-invitation-email.tsx | 2 +- .../workspace-invitation-email.tsx | 2 +- .../workflow-notification-email.tsx | 2 +- apps/sim/components/emails/subjects.ts | 2 +- apps/sim/ee/LICENSE | 4 +- apps/sim/ee/README.md | 2 +- apps/sim/ee/whitelabeling/branding.ts | 45 ++ apps/sim/ee/whitelabeling/index.ts | 4 + .../whitelabeling}/inject-theme.ts | 4 +- .../branding => ee/whitelabeling}/metadata.ts | 2 +- apps/sim/lib/auth/auth.ts | 139 ++--- apps/sim/lib/branding/branding.ts | 80 --- apps/sim/lib/branding/defaults.ts | 22 + apps/sim/lib/branding/index.ts | 2 + apps/sim/lib/branding/types.ts | 19 + apps/sim/tools/confluence/add_label.ts | 123 ++++ apps/sim/tools/confluence/create_blogpost.ts | 151 +++++ apps/sim/tools/confluence/create_page.ts | 30 +- .../tools/confluence/create_page_property.ts | 127 +++++ apps/sim/tools/confluence/delete_page.ts | 12 +- apps/sim/tools/confluence/get_blogpost.ts | 144 +++++ .../tools/confluence/get_page_ancestors.ts | 126 ++++ .../sim/tools/confluence/get_page_children.ts | 143 +++++ apps/sim/tools/confluence/get_page_version.ts | 123 ++++ apps/sim/tools/confluence/get_space.ts | 33 +- apps/sim/tools/confluence/index.ts | 46 +- apps/sim/tools/confluence/list_attachments.ts | 30 +- apps/sim/tools/confluence/list_blogposts.ts | 167 ++++++ .../confluence/list_blogposts_in_space.ts | 178 ++++++ apps/sim/tools/confluence/list_comments.ts | 37 +- apps/sim/tools/confluence/list_labels.ts | 25 + .../tools/confluence/list_page_properties.ts | 149 +++++ .../tools/confluence/list_page_versions.ts | 131 +++++ .../tools/confluence/list_pages_in_space.ts | 174 ++++++ apps/sim/tools/confluence/list_spaces.ts | 27 +- apps/sim/tools/confluence/retrieve.ts | 42 +- apps/sim/tools/confluence/search_in_space.ts | 144 +++++ apps/sim/tools/confluence/types.ts | 94 +++ apps/sim/tools/confluence/update.ts | 26 +- apps/sim/tools/confluence/utils.ts | 23 +- apps/sim/tools/registry.ts | 26 + 129 files changed, 5074 insertions(+), 556 deletions(-) create mode 100644 apps/sim/app/api/tools/confluence/blogposts/route.ts create mode 100644 apps/sim/app/api/tools/confluence/page-ancestors/route.ts create mode 100644 apps/sim/app/api/tools/confluence/page-children/route.ts create mode 100644 apps/sim/app/api/tools/confluence/page-properties/route.ts create mode 100644 apps/sim/app/api/tools/confluence/page-versions/route.ts create mode 100644 apps/sim/app/api/tools/confluence/search-in-space/route.ts create mode 100644 apps/sim/app/api/tools/confluence/space-blogposts/route.ts create mode 100644 apps/sim/app/api/tools/confluence/space-pages/route.ts create mode 100644 apps/sim/ee/whitelabeling/branding.ts create mode 100644 apps/sim/ee/whitelabeling/index.ts rename apps/sim/{lib/branding => ee/whitelabeling}/inject-theme.ts (96%) rename apps/sim/{lib/branding => ee/whitelabeling}/metadata.ts (98%) delete mode 100644 apps/sim/lib/branding/branding.ts create mode 100644 apps/sim/lib/branding/defaults.ts create mode 100644 apps/sim/lib/branding/index.ts create mode 100644 apps/sim/lib/branding/types.ts create mode 100644 apps/sim/tools/confluence/add_label.ts create mode 100644 apps/sim/tools/confluence/create_blogpost.ts create mode 100644 apps/sim/tools/confluence/create_page_property.ts create mode 100644 apps/sim/tools/confluence/get_blogpost.ts create mode 100644 apps/sim/tools/confluence/get_page_ancestors.ts create mode 100644 apps/sim/tools/confluence/get_page_children.ts create mode 100644 apps/sim/tools/confluence/get_page_version.ts create mode 100644 apps/sim/tools/confluence/list_blogposts.ts create mode 100644 apps/sim/tools/confluence/list_blogposts_in_space.ts create mode 100644 apps/sim/tools/confluence/list_page_properties.ts create mode 100644 apps/sim/tools/confluence/list_page_versions.ts create mode 100644 apps/sim/tools/confluence/list_pages_in_space.ts create mode 100644 apps/sim/tools/confluence/search_in_space.ts diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 308902d64..c7a766f6c 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -163,9 +163,9 @@ export const blockTypeToIconMap: Record = { elevenlabs: ElevenLabsIcon, enrich: EnrichSoIcon, exa: ExaAIIcon, - file_v2: DocumentIcon, + file_v3: DocumentIcon, firecrawl: FirecrawlIcon, - fireflies: FirefliesIcon, + fireflies_v2: FirefliesIcon, github_v2: GithubIcon, gitlab: GitLabIcon, gmail_v2: GmailIcon, @@ -177,7 +177,7 @@ export const blockTypeToIconMap: Record = { google_maps: GoogleMapsIcon, google_search: GoogleIcon, google_sheets_v2: GoogleSheetsIcon, - google_slides: GoogleSlidesIcon, + google_slides_v2: GoogleSlidesIcon, google_vault: GoogleVaultIcon, grafana: GrafanaIcon, grain: GrainIcon, @@ -206,7 +206,7 @@ export const blockTypeToIconMap: Record = { microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, microsoft_teams: MicrosoftTeamsIcon, - mistral_parse_v2: MistralIcon, + mistral_parse_v3: MistralIcon, mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, @@ -221,11 +221,11 @@ export const blockTypeToIconMap: Record = { polymarket: PolymarketIcon, postgresql: PostgresIcon, posthog: PosthogIcon, - pulse: PulseIcon, + pulse_v2: PulseIcon, qdrant: QdrantIcon, rds: RDSIcon, reddit: RedditIcon, - reducto: ReductoIcon, + reducto_v2: ReductoIcon, resend: ResendIcon, s3: S3Icon, salesforce: SalesforceIcon, @@ -244,11 +244,11 @@ export const blockTypeToIconMap: Record = { ssh: SshIcon, stagehand: StagehandIcon, stripe: StripeIcon, - stt: STTIcon, + stt_v2: STTIcon, supabase: SupabaseIcon, tavily: TavilyIcon, telegram: TelegramIcon, - textract: TextractIcon, + textract_v2: TextractIcon, tinybird: TinybirdIcon, translate: TranslateIcon, trello: TrelloIcon, @@ -257,7 +257,7 @@ export const blockTypeToIconMap: Record = { twilio_voice: TwilioIcon, typeform: TypeformIcon, video_generator_v2: VideoIcon, - vision: EyeIcon, + vision_v2: EyeIcon, wealthbox: WealthboxIcon, webflow: WebflowIcon, whatsapp: WhatsAppIcon, diff --git a/apps/docs/content/docs/de/tools/file.mdx b/apps/docs/content/docs/de/tools/file.mdx index 08542aa01..3bea49629 100644 --- a/apps/docs/content/docs/de/tools/file.mdx +++ b/apps/docs/content/docs/de/tools/file.mdx @@ -6,7 +6,7 @@ description: Mehrere Dateien lesen und parsen import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/de/tools/fireflies.mdx b/apps/docs/content/docs/de/tools/fireflies.mdx index 4856b2df7..ab7dce902 100644 --- a/apps/docs/content/docs/de/tools/fireflies.mdx +++ b/apps/docs/content/docs/de/tools/fireflies.mdx @@ -6,7 +6,7 @@ description: Interagieren Sie mit Fireflies.ai-Besprechungstranskripten und -auf import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/de/tools/mistral_parse.mdx b/apps/docs/content/docs/de/tools/mistral_parse.mdx index feff01fe6..2191e1c91 100644 --- a/apps/docs/content/docs/de/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/de/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: Text aus PDF-Dokumenten extrahieren import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 6b1f531f4..b8173f135 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -49,10 +49,25 @@ Retrieve content from Confluence pages using the Confluence API. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp of retrieval | +| `ts` | string | ISO 8601 timestamp of the operation | | `pageId` | string | Confluence page ID | -| `content` | string | Page content with HTML tags stripped | | `title` | string | Page title | +| `content` | string | Page content with HTML tags stripped | +| `status` | string | Page status \(current, archived, trashed, draft\) | +| `spaceId` | string | ID of the space containing the page | +| `parentId` | string | ID of the parent page | +| `authorId` | string | Account ID of the page author | +| `createdAt` | string | ISO 8601 timestamp when the page was created | +| `url` | string | URL to view the page in Confluence | +| `body` | object | Raw page body content in storage format | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| `version` | object | Page version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | ### `confluence_update` @@ -76,6 +91,25 @@ Update a Confluence page using the Confluence API. | `ts` | string | Timestamp of update | | `pageId` | string | Confluence page ID | | `title` | string | Updated page title | +| `status` | string | Page status | +| `spaceId` | string | Space ID | +| `body` | object | Page body content in storage format | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| `version` | object | Page version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `url` | string | URL to view the page in Confluence | | `success` | boolean | Update operation success status | ### `confluence_create_page` @@ -100,11 +134,30 @@ Create a new page in a Confluence space. | `ts` | string | Timestamp of creation | | `pageId` | string | Created page ID | | `title` | string | Page title | +| `status` | string | Page status | +| `spaceId` | string | Space ID | +| `parentId` | string | Parent page ID | +| `body` | object | Page body content | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| `version` | object | Page version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | | `url` | string | Page URL | ### `confluence_delete_page` -Delete a Confluence page (moves it to trash where it can be restored). +Delete a Confluence page. By default moves to trash; use purge=true to permanently delete. #### Input @@ -112,6 +165,7 @@ Delete a Confluence page (moves it to trash where it can be restored). | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to delete | +| `purge` | boolean | No | If true, permanently deletes the page instead of moving to trash \(default: false\) | | `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -122,6 +176,229 @@ Delete a Confluence page (moves it to trash where it can be restored). | `pageId` | string | Deleted page ID | | `deleted` | boolean | Deletion status | +### `confluence_list_pages_in_space` + +List all pages within a specific Confluence space. Supports pagination and filtering by status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | The ID of the Confluence space to list pages from | +| `limit` | number | No | Maximum number of pages to return \(default: 50, max: 250\) | +| `status` | string | No | Filter pages by status: current, archived, trashed, or draft | +| `bodyFormat` | string | No | Format for page body content: storage, atlas_doc_format, or view. If not specified, body is not included. | +| `cursor` | string | No | Pagination cursor from previous response to get the next page of results | +| `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 | +| `pages` | array | Array of pages in the space | +| ↳ `id` | string | Unique page identifier | +| ↳ `title` | string | Page title | +| ↳ `status` | string | Page status \(e.g., current, archived, trashed, draft\) | +| ↳ `spaceId` | string | ID of the space containing the page | +| ↳ `parentId` | string | ID of the parent page \(null if top-level\) | +| ↳ `authorId` | string | Account ID of the page author | +| ↳ `createdAt` | string | ISO 8601 timestamp when the page was created | +| ↳ `version` | object | Page version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| ↳ `body` | object | Page body content \(if bodyFormat was specified\) | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `webUrl` | string | URL to view the page in Confluence | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_page_children` + +Get all child pages of a specific Confluence page. Useful for navigating page hierarchies. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the parent page to get children from | +| `limit` | number | No | Maximum number of child pages to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response to get the next page of results | +| `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 | +| `parentId` | string | ID of the parent page | +| `children` | array | Array of child pages | +| ↳ `id` | string | Child page ID | +| ↳ `title` | string | Child page title | +| ↳ `status` | string | Page status | +| ↳ `spaceId` | string | Space ID | +| ↳ `childPosition` | number | Position among siblings | +| ↳ `webUrl` | string | URL to view the page | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_page_ancestors` + +Get the ancestor (parent) pages of a specific Confluence page. Returns the full hierarchy from the page up to the root. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page to get ancestors for | +| `limit` | number | No | Maximum number of ancestors to return \(default: 25, max: 250\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | ID of the page whose ancestors were retrieved | +| `ancestors` | array | Array of ancestor pages, ordered from direct parent to root | +| ↳ `id` | string | Ancestor page ID | +| ↳ `title` | string | Ancestor page title | +| ↳ `status` | string | Page status | +| ↳ `spaceId` | string | Space ID | +| ↳ `webUrl` | string | URL to view the page | + +### `confluence_list_page_versions` + +List all versions (revision history) of a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page to get versions for | +| `limit` | number | No | Maximum number of versions 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 | +| `pageId` | string | ID of the page | +| `versions` | array | Array of page versions | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_page_version` + +Get details about a specific version of a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page | +| `versionNumber` | number | Yes | The version number to retrieve \(e.g., 1, 2, 3\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | ID of the page | +| `version` | object | Detailed version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| ↳ `contentTypeModified` | boolean | Whether the content type was modified in this version | +| ↳ `collaborators` | array | List of collaborator account IDs for this version | +| ↳ `prevVersion` | number | Previous version number | +| ↳ `nextVersion` | number | Next version number | + +### `confluence_list_page_properties` + +List all custom properties (metadata) attached to a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page to list properties from | +| `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 | +| `pageId` | string | ID of the page | +| `properties` | array | Array of content properties | +| ↳ `id` | string | Property ID | +| ↳ `key` | string | Property key | +| ↳ `value` | json | Property value \(can be any JSON\) | +| ↳ `version` | object | Version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_create_page_property` + +Create a new custom property (metadata) on a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | The ID of the page to add the property to | +| `key` | string | Yes | The key/name for the property | +| `value` | json | Yes | The value for the property \(can be any JSON value\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | ID of the page | +| `propertyId` | string | ID of the created property | +| `key` | string | Property key | +| `value` | json | Property value | +| `version` | object | Version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | + ### `confluence_search` Search for content across Confluence pages, blog posts, and other content. @@ -155,6 +432,211 @@ Search for content across Confluence pages, blog posts, and other content. | ↳ `lastModified` | string | ISO 8601 timestamp of last modification | | ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) | +### `confluence_search_in_space` + +Search for content within a specific Confluence space. Optionally filter by text query and content type. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceKey` | string | Yes | The key of the Confluence space to search in \(e.g., "ENG", "HR"\) | +| `query` | string | No | Text search query. If not provided, returns all content in the space. | +| `contentType` | string | No | Filter by content type: page, blogpost, attachment, or comment | +| `limit` | number | No | Maximum number of results to return \(default: 25, max: 250\) | +| `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 | +| `spaceKey` | string | The space key that was searched | +| `totalSize` | number | Total number of matching results | +| `results` | array | Array of search results | +| ↳ `id` | string | Unique content identifier | +| ↳ `title` | string | Content title | +| ↳ `type` | string | Content type \(e.g., page, blogpost, attachment, comment\) | +| ↳ `status` | string | Content status \(e.g., current\) | +| ↳ `url` | string | URL to view the content in Confluence | +| ↳ `excerpt` | string | Text excerpt matching the search query | +| ↳ `spaceKey` | string | Key of the space containing the content | +| ↳ `space` | object | Space information for the content | +| ↳ `id` | string | Space identifier | +| ↳ `key` | string | Space key | +| ↳ `name` | string | Space name | +| ↳ `lastModified` | string | ISO 8601 timestamp of last modification | +| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) | + +### `confluence_list_blogposts` + +List all blog posts across all accessible Confluence spaces. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) | +| `status` | string | No | Filter by status: current, archived, trashed, or draft | +| `sort` | string | No | Sort order: created-date, -created-date, modified-date, -modified-date, title, -title | +| `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 | +| `blogPosts` | array | Array of blog posts | +| ↳ `id` | string | Blog post ID | +| ↳ `title` | string | Blog post title | +| ↳ `status` | string | Blog post status | +| ↳ `spaceId` | string | Space ID | +| ↳ `authorId` | string | Author account ID | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `version` | object | Version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| ↳ `webUrl` | string | URL to view the blog post | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_blogpost` + +Get a specific Confluence blog post by ID, including its 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 retrieve | +| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view | +| `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 | Blog post ID | +| `title` | string | Blog post title | +| `status` | string | Blog post status | +| `spaceId` | string | Space ID | +| `authorId` | string | Author account ID | +| `createdAt` | string | Creation timestamp | +| `version` | object | Version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `body` | object | Blog post body content in requested format\(s\) | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| `webUrl` | string | URL to view the blog post | + +### `confluence_create_blogpost` + +Create a new blog post in a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | The ID of the space to create the blog post in | +| `title` | string | Yes | Title of the blog post | +| `content` | string | Yes | Blog post content in Confluence storage format \(HTML\) | +| `status` | string | No | Blog post status: current \(default\) or draft | +| `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 | Created blog post ID | +| `title` | string | Blog post title | +| `status` | string | Blog post status | +| `spaceId` | string | Space ID | +| `authorId` | string | Author account ID | +| `body` | object | Blog post body content | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| `version` | object | Blog post version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `webUrl` | string | URL to view the blog post | + +### `confluence_list_blogposts_in_space` + +List all blog posts within a specific Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | The ID of the Confluence space to list blog posts from | +| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) | +| `status` | string | No | Filter by status: current, archived, trashed, or draft | +| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view | +| `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 | +| `blogPosts` | array | Array of blog posts in the space | +| ↳ `id` | string | Blog post ID | +| ↳ `title` | string | Blog post title | +| ↳ `status` | string | Blog post status | +| ↳ `spaceId` | string | Space ID | +| ↳ `authorId` | string | Author account ID | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `version` | object | Version information | +| ↳ `number` | number | Version number | +| ↳ `message` | string | Version message | +| ↳ `minorEdit` | boolean | Whether this is a minor edit | +| ↳ `authorId` | string | Account ID of the version author | +| ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| ↳ `body` | object | Blog post body content | +| ↳ `storage` | object | Body in storage format \(Confluence markup\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `view` | object | Body in view format \(rendered HTML\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) | +| ↳ `value` | string | The content value in the specified format | +| ↳ `representation` | string | Content representation type | +| ↳ `webUrl` | string | URL to view the blog post | +| `nextCursor` | string | Cursor for fetching the next page of results | + ### `confluence_create_comment` Add a comment to a Confluence page. @@ -187,6 +669,8 @@ List all comments on a Confluence page. | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to list comments from | | `limit` | number | No | Maximum number of comments to return \(default: 25\) | +| `bodyFormat` | string | No | Format for the comment body: storage, atlas_doc_format, view, or export_view \(default: storage\) | +| `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 @@ -212,6 +696,7 @@ List all comments on a Confluence page. | ↳ `minorEdit` | boolean | Whether this is a minor edit | | ↳ `authorId` | string | Account ID of the version author | | ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `nextCursor` | string | Cursor for fetching the next page of results | ### `confluence_update_comment` @@ -291,7 +776,8 @@ List all attachments on a Confluence page. | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to list attachments from | -| `limit` | number | No | Maximum number of attachments to return \(default: 25\) | +| `limit` | number | No | Maximum number of attachments 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 @@ -316,6 +802,7 @@ List all attachments on a Confluence page. | ↳ `minorEdit` | boolean | Whether this is a minor edit | | ↳ `authorId` | string | Account ID of the version author | | ↳ `createdAt` | string | ISO 8601 timestamp of version creation | +| `nextCursor` | string | Cursor for fetching the next page of results | ### `confluence_delete_attachment` @@ -347,6 +834,8 @@ List all labels on a Confluence page. | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | | `pageId` | string | Yes | Confluence page ID to list labels from | +| `limit` | number | No | Maximum number of labels to return \(default: 25, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | | `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -358,6 +847,30 @@ List all labels on a Confluence page. | ↳ `id` | string | Unique label identifier | | ↳ `name` | string | Label name | | ↳ `prefix` | string | Label prefix/type \(e.g., global, my, team\) | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_add_label` + +Add a label to a Confluence page for organization and categorization. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to add the label to | +| `labelName` | string | Yes | Name of the label to add | +| `prefix` | string | No | Label prefix: global \(default\), my, team, or system | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `pageId` | string | Page ID that the label was added to | +| `labelName` | string | Name of the added label | +| `labelId` | string | ID of the added label | ### `confluence_get_space` @@ -375,13 +888,19 @@ Get details about a specific Confluence space. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `ts` | string | Timestamp of retrieval | +| `ts` | string | ISO 8601 timestamp of the operation | | `spaceId` | string | Space ID | | `name` | string | Space name | | `key` | string | Space key | -| `type` | string | Space type | -| `status` | string | Space status | -| `url` | string | Space URL | +| `type` | string | Space type \(global, personal\) | +| `status` | string | Space status \(current, archived\) | +| `url` | string | URL to view the space in Confluence | +| `authorId` | string | Account ID of the space creator | +| `createdAt` | string | ISO 8601 timestamp when the space was created | +| `homepageId` | string | ID of the space homepage | +| `description` | object | Space description content | +| ↳ `value` | string | Description text content | +| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | ### `confluence_list_spaces` @@ -392,7 +911,8 @@ List all Confluence spaces accessible to the user. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | -| `limit` | number | No | Maximum number of spaces to return \(default: 25\) | +| `limit` | number | No | Maximum number of spaces to return \(default: 25, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | | `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | #### Output @@ -412,5 +932,6 @@ List all Confluence spaces accessible to the user. | ↳ `description` | object | Space description | | ↳ `value` | string | Description text content | | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | +| `nextCursor` | string | Cursor for fetching the next page of results | diff --git a/apps/docs/content/docs/en/tools/discord.mdx b/apps/docs/content/docs/en/tools/discord.mdx index 84dd318f8..ad3195599 100644 --- a/apps/docs/content/docs/en/tools/discord.mdx +++ b/apps/docs/content/docs/en/tools/discord.mdx @@ -63,6 +63,7 @@ Send a message to a Discord channel | Parameter | Type | Description | | --------- | ---- | ----------- | | `message` | string | Success or error message | +| `files` | file[] | Files attached to the message | | `data` | object | Discord message data | | ↳ `id` | string | Message ID | | ↳ `content` | string | Message content | diff --git a/apps/docs/content/docs/en/tools/dropbox.mdx b/apps/docs/content/docs/en/tools/dropbox.mdx index 54cf3013d..aceae4da0 100644 --- a/apps/docs/content/docs/en/tools/dropbox.mdx +++ b/apps/docs/content/docs/en/tools/dropbox.mdx @@ -43,7 +43,8 @@ Upload a file to Dropbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `path` | string | Yes | The path in Dropbox where the file should be saved \(e.g., /folder/document.pdf\) | -| `fileContent` | string | Yes | The base64 encoded content of the file to upload | +| `file` | file | No | The file to upload \(UserFile object\) | +| `fileContent` | string | No | Legacy: base64 encoded file content | | `fileName` | string | No | Optional filename \(used if path is a folder\) | | `mode` | string | No | Write mode: add \(default\) or overwrite | | `autorename` | boolean | No | If true, rename the file if there is a conflict | @@ -66,7 +67,7 @@ Upload a file to Dropbox ### `dropbox_download` -Download a file from Dropbox and get a temporary link +Download a file from Dropbox with metadata and content #### Input @@ -78,11 +79,8 @@ Download a file from Dropbox and get a temporary link | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | object | The file metadata | -| ↳ `id` | string | Unique identifier for the file | -| ↳ `name` | string | Name of the file | -| ↳ `path_display` | string | Display path of the file | -| ↳ `size` | number | Size of the file in bytes | +| `file` | file | Downloaded file stored in execution files | +| `metadata` | json | The file metadata | | `temporaryLink` | string | Temporary link to download the file \(valid for ~4 hours\) | | `content` | string | Base64 encoded file content \(if fetched\) | diff --git a/apps/docs/content/docs/en/tools/file.mdx b/apps/docs/content/docs/en/tools/file.mdx index 2a0cc1b87..ddc31bd60 100644 --- a/apps/docs/content/docs/en/tools/file.mdx +++ b/apps/docs/content/docs/en/tools/file.mdx @@ -6,7 +6,7 @@ description: Read and parse multiple files import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need ## Usage Instructions -Integrate File into the workflow. Can upload a file manually or insert a file url. +Upload files directly or import from external URLs to get UserFile objects for use in other blocks. @@ -41,14 +41,15 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | Yes | Path to the file\(s\). Can be a single path, URL, or an array of paths. | +| `filePath` | string | No | Path to the file\(s\). Can be a single path, URL, or an array of paths. | +| `file` | file | No | Uploaded file\(s\) to parse | | `fileType` | string | No | Type of file to parse \(auto-detected if not specified\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | array | Array of parsed files with content, metadata, and file properties | -| `combinedContent` | string | All file contents merged into a single text string | +| `files` | file[] | Parsed files as UserFile objects | +| `combinedContent` | string | Combined content of all parsed files | diff --git a/apps/docs/content/docs/en/tools/fireflies.mdx b/apps/docs/content/docs/en/tools/fireflies.mdx index 2445a2167..90c591605 100644 --- a/apps/docs/content/docs/en/tools/fireflies.mdx +++ b/apps/docs/content/docs/en/tools/fireflies.mdx @@ -6,7 +6,7 @@ description: Interact with Fireflies.ai meeting transcripts and recordings import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/en/tools/github.mdx b/apps/docs/content/docs/en/tools/github.mdx index 37b07dc7a..9ea94cb08 100644 --- a/apps/docs/content/docs/en/tools/github.mdx +++ b/apps/docs/content/docs/en/tools/github.mdx @@ -692,6 +692,7 @@ Get the content of a file from a GitHub repository. Supports files up to 1MB. Co | `download_url` | string | Direct download URL | | `git_url` | string | Git blob API URL | | `_links` | json | Related links | +| `file` | file | Downloaded file stored in execution files | ### `github_create_file` diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index f6a5fcb17..08298a9d0 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -291,11 +291,7 @@ Download a file from Google Drive with complete metadata (exports Google Workspa | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | object | Downloaded file data | -| ↳ `name` | string | File name | -| ↳ `mimeType` | string | MIME type of the file | -| ↳ `data` | string | File content as base64-encoded string | -| ↳ `size` | number | File size in bytes | +| `file` | file | Downloaded file stored in execution files | | `metadata` | object | Complete file metadata from Google Drive | | ↳ `id` | string | Google Drive file ID | | ↳ `kind` | string | Resource type identifier | diff --git a/apps/docs/content/docs/en/tools/google_slides.mdx b/apps/docs/content/docs/en/tools/google_slides.mdx index 2aa03f7e8..30d6a17d4 100644 --- a/apps/docs/content/docs/en/tools/google_slides.mdx +++ b/apps/docs/content/docs/en/tools/google_slides.mdx @@ -6,7 +6,7 @@ description: Read, write, and create presentations import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/en/tools/jira.mdx b/apps/docs/content/docs/en/tools/jira.mdx index 921ae9cd9..812752057 100644 --- a/apps/docs/content/docs/en/tools/jira.mdx +++ b/apps/docs/content/docs/en/tools/jira.mdx @@ -333,6 +333,28 @@ Get all attachments from a Jira issue | `issueKey` | string | Issue key | | `attachments` | array | Array of attachments with id, filename, size, mimeType, created, author | +### `jira_add_attachment` + +Add attachments to a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to add attachments to \(e.g., PROJ-123\) | +| `files` | file[] | Yes | Files to attach to the Jira issue | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of the operation | +| `issueKey` | string | Issue key | +| `attachmentIds` | json | IDs of uploaded attachments | +| `files` | file[] | Uploaded attachment files | + ### `jira_delete_attachment` Delete an attachment from a Jira issue diff --git a/apps/docs/content/docs/en/tools/linear.mdx b/apps/docs/content/docs/en/tools/linear.mdx index ec1d24698..b64d9a915 100644 --- a/apps/docs/content/docs/en/tools/linear.mdx +++ b/apps/docs/content/docs/en/tools/linear.mdx @@ -1022,7 +1022,8 @@ Add an attachment to an issue in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `issueId` | string | Yes | Issue ID to attach to | -| `url` | string | Yes | URL of the attachment | +| `url` | string | No | URL of the attachment | +| `file` | file | No | File to attach | | `title` | string | Yes | Attachment title | | `subtitle` | string | No | Attachment subtitle/description | diff --git a/apps/docs/content/docs/en/tools/microsoft_teams.mdx b/apps/docs/content/docs/en/tools/microsoft_teams.mdx index f4964250e..0d86d9aaa 100644 --- a/apps/docs/content/docs/en/tools/microsoft_teams.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_teams.mdx @@ -81,6 +81,7 @@ Write or update content in a Microsoft Teams chat | `createdTime` | string | Timestamp when message was created | | `url` | string | Web URL to the message | | `updatedContent` | boolean | Whether content was successfully updated | +| `files` | file[] | Files attached to the message | ### `microsoft_teams_read_channel` @@ -132,6 +133,7 @@ Write or send a message to a Microsoft Teams channel | `createdTime` | string | Timestamp when message was created | | `url` | string | Web URL to the message | | `updatedContent` | boolean | Whether content was successfully updated | +| `files` | file[] | Files attached to the message | ### `microsoft_teams_update_chat_message` diff --git a/apps/docs/content/docs/en/tools/mistral_parse.mdx b/apps/docs/content/docs/en/tools/mistral_parse.mdx index 711f74391..046620f39 100644 --- a/apps/docs/content/docs/en/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/en/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: Extract text from PDF documents import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -35,13 +35,12 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do ### `mistral_parser` -Parse PDF documents using Mistral OCR API - #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | Yes | URL to a PDF document to be processed | +| `filePath` | string | No | URL to a PDF document to be processed | +| `file` | file | No | Document file to be processed | | `fileUpload` | object | No | File upload data from file-upload component | | `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. | | `includeImageBase64` | boolean | No | Include base64-encoded images in the response | @@ -55,27 +54,8 @@ Parse PDF documents using Mistral OCR API | Parameter | Type | Description | | --------- | ---- | ----------- | | `pages` | array | Array of page objects from Mistral OCR | -| ↳ `index` | number | Page index \(zero-based\) | -| ↳ `markdown` | string | Extracted markdown content | -| ↳ `images` | array | Images extracted from this page with bounding boxes | -| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) | -| ↳ `top_left_x` | number | Top-left X coordinate in pixels | -| ↳ `top_left_y` | number | Top-left Y coordinate in pixels | -| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels | -| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels | -| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) | -| ↳ `dimensions` | object | Page dimensions | -| ↳ `dpi` | number | Dots per inch | -| ↳ `height` | number | Page height in pixels | -| ↳ `width` | number | Page width in pixels | -| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] | -| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \["https://...", "mailto:..."\]\) | -| ↳ `header` | string | Page header content \(when extract_header=true\) | -| ↳ `footer` | string | Page footer content \(when extract_footer=true\) | -| `model` | string | Mistral OCR model identifier \(e.g., mistral-ocr-latest\) | -| `usage_info` | object | Usage and processing statistics | -| ↳ `pages_processed` | number | Total number of pages processed | -| ↳ `doc_size_bytes` | number | Document file size in bytes | -| `document_annotation` | string | Structured annotation data as JSON string \(when applicable\) | +| `model` | string | Mistral OCR model identifier | +| `usage_info` | json | Usage statistics from the API | +| `document_annotation` | string | Structured annotation data | diff --git a/apps/docs/content/docs/en/tools/notion.mdx b/apps/docs/content/docs/en/tools/notion.mdx index 37a663af5..9a7ac9715 100644 --- a/apps/docs/content/docs/en/tools/notion.mdx +++ b/apps/docs/content/docs/en/tools/notion.mdx @@ -113,6 +113,26 @@ Create a new page in Notion | `last_edited_time` | string | ISO 8601 last edit timestamp | | `title` | string | Page title | +### `notion_update_page` + +Update properties of a Notion page + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `pageId` | string | Yes | The UUID of the Notion page to update | +| `properties` | json | Yes | JSON object of properties to update | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Page UUID | +| `url` | string | Notion page URL | +| `last_edited_time` | string | ISO 8601 last edit timestamp | +| `title` | string | Page title | + ### `notion_query_database` Query and filter Notion database entries with advanced filtering diff --git a/apps/docs/content/docs/en/tools/pipedrive.mdx b/apps/docs/content/docs/en/tools/pipedrive.mdx index fcaf58cef..28b8f723b 100644 --- a/apps/docs/content/docs/en/tools/pipedrive.mdx +++ b/apps/docs/content/docs/en/tools/pipedrive.mdx @@ -152,6 +152,7 @@ Retrieve files from Pipedrive with optional filters | `person_id` | string | No | Filter files by person ID \(e.g., "456"\) | | `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) | | `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) | +| `downloadFiles` | boolean | No | Download file contents into file outputs | #### Output @@ -168,6 +169,7 @@ Retrieve files from Pipedrive with optional filters | ↳ `person_id` | number | Associated person ID | | ↳ `org_id` | number | Associated organization ID | | ↳ `url` | string | File download URL | +| `downloadedFiles` | file[] | Downloaded files from Pipedrive | | `total_items` | number | Total number of files returned | | `success` | boolean | Operation success status | diff --git a/apps/docs/content/docs/en/tools/pulse.mdx b/apps/docs/content/docs/en/tools/pulse.mdx index a804d9952..19fd85f4a 100644 --- a/apps/docs/content/docs/en/tools/pulse.mdx +++ b/apps/docs/content/docs/en/tools/pulse.mdx @@ -6,7 +6,7 @@ description: Extract text from documents using Pulse OCR import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -31,7 +31,7 @@ If you need accurate, scalable, and developer-friendly document parsing capabili ## Usage Instructions -Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via URL or upload. +Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via upload or file references. @@ -39,13 +39,12 @@ Integrate Pulse into the workflow. Extract text from PDF documents, images, and ### `pulse_parser` -Parse documents (PDF, images, Office docs) using Pulse OCR API - #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | Yes | URL to a document to be processed | +| `filePath` | string | No | URL to a document to be processed | +| `file` | file | No | Document file to be processed | | `fileUpload` | object | No | File upload data from file-upload component | | `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) | | `extractFigure` | boolean | No | Enable figure extraction from the document | @@ -57,16 +56,6 @@ Parse documents (PDF, images, Office docs) using Pulse OCR API #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `markdown` | string | Extracted content in markdown format | -| `page_count` | number | Number of pages in the document | -| `job_id` | string | Unique job identifier | -| `bounding_boxes` | json | Bounding box layout information | -| `extraction_url` | string | URL for extraction results \(for large documents\) | -| `html` | string | HTML content if requested | -| `structured_output` | json | Structured output if schema was provided | -| `chunks` | json | Chunked content if chunking was enabled | -| `figures` | json | Extracted figures if figure extraction was enabled | +This tool does not produce any outputs. diff --git a/apps/docs/content/docs/en/tools/reducto.mdx b/apps/docs/content/docs/en/tools/reducto.mdx index ef004cf89..11af5add1 100644 --- a/apps/docs/content/docs/en/tools/reducto.mdx +++ b/apps/docs/content/docs/en/tools/reducto.mdx @@ -6,7 +6,7 @@ description: Extract text from PDF documents import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -29,7 +29,7 @@ Looking for reliable and scalable PDF parsing? Reducto is optimized for develope ## Usage Instructions -Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL. +Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents or file references. @@ -37,13 +37,12 @@ Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF do ### `reducto_parser` -Parse PDF documents using Reducto OCR API - #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `filePath` | string | Yes | URL to a PDF document to be processed | +| `filePath` | string | No | URL to a PDF document to be processed | +| `file` | file | No | Document file to be processed | | `fileUpload` | object | No | File upload data from file-upload component | | `pages` | array | No | Specific pages to process \(1-indexed page numbers\) | | `tableOutputFormat` | string | No | Table output format \(html or markdown\). Defaults to markdown. | @@ -51,13 +50,6 @@ Parse PDF documents using Reducto OCR API #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `job_id` | string | Unique identifier for the processing job | -| `duration` | number | Processing time in seconds | -| `usage` | json | Resource consumption data | -| `result` | json | Parsed document content with chunks and blocks | -| `pdf_url` | string | Storage URL of converted PDF | -| `studio_link` | string | Link to Reducto studio interface | +This tool does not produce any outputs. diff --git a/apps/docs/content/docs/en/tools/s3.mdx b/apps/docs/content/docs/en/tools/s3.mdx index b4cf8ffa6..95715f0f1 100644 --- a/apps/docs/content/docs/en/tools/s3.mdx +++ b/apps/docs/content/docs/en/tools/s3.mdx @@ -78,6 +78,7 @@ Retrieve an object from an AWS S3 bucket | Parameter | Type | Description | | --------- | ---- | ----------- | | `url` | string | Pre-signed URL for downloading the S3 object | +| `file` | file | Downloaded file stored in execution files | | `metadata` | object | File metadata including type, size, name, and last modified date | ### `s3_list_objects` diff --git a/apps/docs/content/docs/en/tools/sendgrid.mdx b/apps/docs/content/docs/en/tools/sendgrid.mdx index a8e25a288..d5b9ea999 100644 --- a/apps/docs/content/docs/en/tools/sendgrid.mdx +++ b/apps/docs/content/docs/en/tools/sendgrid.mdx @@ -62,7 +62,7 @@ Send an email using SendGrid API | `bcc` | string | No | BCC email address | | `replyTo` | string | No | Reply-to email address | | `replyToName` | string | No | Reply-to name | -| `attachments` | file[] | No | Files to attach to the email as an array of attachment objects | +| `attachments` | file[] | No | Files to attach to the email \(UserFile objects\) | | `templateId` | string | No | SendGrid template ID to use | | `dynamicTemplateData` | json | No | JSON object of dynamic template data | diff --git a/apps/docs/content/docs/en/tools/sftp.mdx b/apps/docs/content/docs/en/tools/sftp.mdx index 3cb8d0876..fd0ef6aa7 100644 --- a/apps/docs/content/docs/en/tools/sftp.mdx +++ b/apps/docs/content/docs/en/tools/sftp.mdx @@ -97,6 +97,7 @@ Download a file from a remote SFTP server | Parameter | Type | Description | | --------- | ---- | ----------- | | `success` | boolean | Whether the download was successful | +| `file` | file | Downloaded file stored in execution files | | `fileName` | string | Name of the downloaded file | | `content` | string | File content \(text or base64 encoded\) | | `size` | number | File size in bytes | diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 1471c8800..35562a17e 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -144,6 +144,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | `ts` | string | Message timestamp | | `channel` | string | Channel ID where message was sent | | `fileCount` | number | Number of files uploaded \(when files are attached\) | +| `files` | file[] | Files attached to the message | ### `slack_canvas` diff --git a/apps/docs/content/docs/en/tools/ssh.mdx b/apps/docs/content/docs/en/tools/ssh.mdx index 2b0efdd23..a8bd98093 100644 --- a/apps/docs/content/docs/en/tools/ssh.mdx +++ b/apps/docs/content/docs/en/tools/ssh.mdx @@ -170,6 +170,7 @@ Download a file from a remote SSH server | Parameter | Type | Description | | --------- | ---- | ----------- | | `downloaded` | boolean | Whether the file was downloaded successfully | +| `file` | file | Downloaded file stored in execution files | | `fileContent` | string | File content \(base64 encoded for binary files\) | | `fileName` | string | Name of the downloaded file | | `remotePath` | string | Source path on the remote server | diff --git a/apps/docs/content/docs/en/tools/stt.mdx b/apps/docs/content/docs/en/tools/stt.mdx index 3026810a3..956724329 100644 --- a/apps/docs/content/docs/en/tools/stt.mdx +++ b/apps/docs/content/docs/en/tools/stt.mdx @@ -6,7 +6,7 @@ description: Convert speech to text using AI import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -50,8 +50,6 @@ Transcribe audio and video files to text using leading AI providers. Supports mu ### `stt_whisper` -Transcribe audio to text using OpenAI Whisper - #### Input | Parameter | Type | Required | Description | @@ -71,22 +69,10 @@ Transcribe audio to text using OpenAI Whisper #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `transcript` | string | Full transcribed text | -| `segments` | array | Timestamped segments | -| ↳ `text` | string | Transcribed text for this segment | -| ↳ `start` | number | Start time in seconds | -| ↳ `end` | number | End time in seconds | -| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) | -| ↳ `confidence` | number | Confidence score \(0-1\) | -| `language` | string | Detected or specified language | -| `duration` | number | Audio duration in seconds | +This tool does not produce any outputs. ### `stt_deepgram` -Transcribe audio to text using Deepgram - #### Input | Parameter | Type | Required | Description | @@ -103,23 +89,10 @@ Transcribe audio to text using Deepgram #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `transcript` | string | Full transcribed text | -| `segments` | array | Timestamped segments with speaker labels | -| ↳ `text` | string | Transcribed text for this segment | -| ↳ `start` | number | Start time in seconds | -| ↳ `end` | number | End time in seconds | -| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) | -| ↳ `confidence` | number | Confidence score \(0-1\) | -| `language` | string | Detected or specified language | -| `duration` | number | Audio duration in seconds | -| `confidence` | number | Overall confidence score | +This tool does not produce any outputs. ### `stt_elevenlabs` -Transcribe audio to text using ElevenLabs - #### Input | Parameter | Type | Required | Description | @@ -135,18 +108,10 @@ Transcribe audio to text using ElevenLabs #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `transcript` | string | Full transcribed text | -| `segments` | array | Timestamped segments | -| `language` | string | Detected or specified language | -| `duration` | number | Audio duration in seconds | -| `confidence` | number | Overall confidence score | +This tool does not produce any outputs. ### `stt_assemblyai` -Transcribe audio to text using AssemblyAI with advanced NLP features - #### Input | Parameter | Type | Required | Description | @@ -167,35 +132,10 @@ Transcribe audio to text using AssemblyAI with advanced NLP features #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `transcript` | string | Full transcribed text | -| `segments` | array | Timestamped segments with speaker labels | -| ↳ `text` | string | Transcribed text for this segment | -| ↳ `start` | number | Start time in seconds | -| ↳ `end` | number | End time in seconds | -| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) | -| ↳ `confidence` | number | Confidence score \(0-1\) | -| `language` | string | Detected or specified language | -| `duration` | number | Audio duration in seconds | -| `confidence` | number | Overall confidence score | -| `sentiment` | array | Sentiment analysis results | -| ↳ `text` | string | Text that was analyzed | -| ↳ `sentiment` | string | Sentiment \(POSITIVE, NEGATIVE, NEUTRAL\) | -| ↳ `confidence` | number | Confidence score | -| ↳ `start` | number | Start time in milliseconds | -| ↳ `end` | number | End time in milliseconds | -| `entities` | array | Detected entities | -| ↳ `entity_type` | string | Entity type \(e.g., person_name, location, organization\) | -| ↳ `text` | string | Entity text | -| ↳ `start` | number | Start time in milliseconds | -| ↳ `end` | number | End time in milliseconds | -| `summary` | string | Auto-generated summary | +This tool does not produce any outputs. ### `stt_gemini` -Transcribe audio to text using Google Gemini with multimodal capabilities - #### Input | Parameter | Type | Required | Description | @@ -211,12 +151,6 @@ Transcribe audio to text using Google Gemini with multimodal capabilities #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `transcript` | string | Full transcribed text | -| `segments` | array | Timestamped segments | -| `language` | string | Detected or specified language | -| `duration` | number | Audio duration in seconds | -| `confidence` | number | Overall confidence score | +This tool does not produce any outputs. diff --git a/apps/docs/content/docs/en/tools/telegram.mdx b/apps/docs/content/docs/en/tools/telegram.mdx index 0a0301789..26f7ae7e7 100644 --- a/apps/docs/content/docs/en/tools/telegram.mdx +++ b/apps/docs/content/docs/en/tools/telegram.mdx @@ -354,6 +354,7 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T | Parameter | Type | Description | | --------- | ---- | ----------- | | `message` | string | Success or error message | +| `files` | file[] | Files attached to the message | | `data` | object | Telegram message data including document | | ↳ `message_id` | number | Unique Telegram message identifier | | ↳ `from` | object | Information about the sender | diff --git a/apps/docs/content/docs/en/tools/textract.mdx b/apps/docs/content/docs/en/tools/textract.mdx index 624e4e4fe..41a14abfd 100644 --- a/apps/docs/content/docs/en/tools/textract.mdx +++ b/apps/docs/content/docs/en/tools/textract.mdx @@ -6,7 +6,7 @@ description: Extract text, tables, and forms from documents import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -35,8 +35,6 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke ### `textract_parser` -Parse documents using AWS Textract OCR and document analysis - #### Input | Parameter | Type | Required | Description | @@ -46,8 +44,8 @@ Parse documents using AWS Textract OCR and document analysis | `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | | `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | | `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). | +| `file` | file | No | Document file to be processed \(JPEG, PNG, or single-page PDF\). | | `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). | -| `fileUpload` | object | No | File upload data from file-upload component | | `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. | | `items` | string | No | Feature type | | `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. | @@ -58,39 +56,6 @@ Parse documents using AWS Textract OCR and document analysis #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements | -| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) | -| ↳ `Id` | string | Unique identifier for the block | -| ↳ `Text` | string | The text content \(for LINE and WORD blocks\) | -| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) | -| ↳ `Confidence` | number | Confidence score \(0-100\) | -| ↳ `Page` | number | Page number | -| ↳ `Geometry` | object | Location and bounding box information | -| ↳ `BoundingBox` | object | Height as ratio of document height | -| ↳ `Height` | number | Height as ratio of document height | -| ↳ `Left` | number | Left position as ratio of document width | -| ↳ `Top` | number | Top position as ratio of document height | -| ↳ `Width` | number | Width as ratio of document width | -| ↳ `Polygon` | array | Polygon coordinates | -| ↳ `X` | number | X coordinate | -| ↳ `Y` | number | Y coordinate | -| ↳ `Relationships` | array | Relationships to other blocks | -| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) | -| ↳ `Ids` | array | IDs of related blocks | -| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) | -| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED | -| ↳ `RowIndex` | number | Row index for table cells | -| ↳ `ColumnIndex` | number | Column index for table cells | -| ↳ `RowSpan` | number | Row span for merged cells | -| ↳ `ColumnSpan` | number | Column span for merged cells | -| ↳ `Query` | object | Query information for QUERY blocks | -| ↳ `Text` | string | Query text | -| ↳ `Alias` | string | Query alias | -| ↳ `Pages` | array | Pages to search | -| `documentMetadata` | object | Metadata about the analyzed document | -| ↳ `pages` | number | Number of pages in the document | -| `modelVersion` | string | Version of the Textract model used for processing | +This tool does not produce any outputs. diff --git a/apps/docs/content/docs/en/tools/twilio_voice.mdx b/apps/docs/content/docs/en/tools/twilio_voice.mdx index 6eb2e716f..35b70da38 100644 --- a/apps/docs/content/docs/en/tools/twilio_voice.mdx +++ b/apps/docs/content/docs/en/tools/twilio_voice.mdx @@ -122,6 +122,7 @@ Retrieve call recording information and transcription (if enabled via TwiML). | `channels` | number | Number of channels \(1 for mono, 2 for dual\) | | `source` | string | How the recording was created | | `mediaUrl` | string | URL to download the recording media file | +| `file` | file | Downloaded recording media file | | `price` | string | Cost of the recording | | `priceUnit` | string | Currency of the price | | `uri` | string | Relative URI of the recording resource | diff --git a/apps/docs/content/docs/en/tools/typeform.mdx b/apps/docs/content/docs/en/tools/typeform.mdx index 99998842d..dc364b260 100644 --- a/apps/docs/content/docs/en/tools/typeform.mdx +++ b/apps/docs/content/docs/en/tools/typeform.mdx @@ -75,6 +75,7 @@ Download files uploaded in Typeform responses | Parameter | Type | Description | | --------- | ---- | ----------- | | `fileUrl` | string | Direct download URL for the uploaded file | +| `file` | file | Downloaded file stored in execution files | | `contentType` | string | MIME type of the uploaded file | | `filename` | string | Original filename of the uploaded file | diff --git a/apps/docs/content/docs/en/tools/video_generator.mdx b/apps/docs/content/docs/en/tools/video_generator.mdx index 437bb2dd6..a33492c95 100644 --- a/apps/docs/content/docs/en/tools/video_generator.mdx +++ b/apps/docs/content/docs/en/tools/video_generator.mdx @@ -57,14 +57,14 @@ Generate videos using Runway Gen-4 with world consistency and visual references | `duration` | number | No | Video duration in seconds \(5 or 10, default: 5\) | | `aspectRatio` | string | No | Aspect ratio: 16:9 \(landscape\), 9:16 \(portrait\), or 1:1 \(square\) | | `resolution` | string | No | Video resolution \(720p output\). Note: Gen-4 Turbo outputs at 720p natively | -| `visualReference` | json | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation | +| `visualReference` | file | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `videoUrl` | string | Generated video URL | -| `videoFile` | json | Video file object with metadata | +| `videoFile` | file | Video file object with metadata | | `duration` | number | Video duration in seconds | | `width` | number | Video width in pixels | | `height` | number | Video height in pixels | @@ -93,7 +93,7 @@ Generate videos using Google Veo 3/3.1 with native audio generation | Parameter | Type | Description | | --------- | ---- | ----------- | | `videoUrl` | string | Generated video URL | -| `videoFile` | json | Video file object with metadata | +| `videoFile` | file | Video file object with metadata | | `duration` | number | Video duration in seconds | | `width` | number | Video width in pixels | | `height` | number | Video height in pixels | @@ -123,7 +123,7 @@ Generate videos using Luma Dream Machine with advanced camera controls | Parameter | Type | Description | | --------- | ---- | ----------- | | `videoUrl` | string | Generated video URL | -| `videoFile` | json | Video file object with metadata | +| `videoFile` | file | Video file object with metadata | | `duration` | number | Video duration in seconds | | `width` | number | Video width in pixels | | `height` | number | Video height in pixels | @@ -151,7 +151,7 @@ Generate videos using MiniMax Hailuo through MiniMax Platform API with advanced | Parameter | Type | Description | | --------- | ---- | ----------- | | `videoUrl` | string | Generated video URL | -| `videoFile` | json | Video file object with metadata | +| `videoFile` | file | Video file object with metadata | | `duration` | number | Video duration in seconds | | `width` | number | Video width in pixels | | `height` | number | Video height in pixels | @@ -181,7 +181,7 @@ Generate videos using Fal.ai platform with access to multiple models including V | Parameter | Type | Description | | --------- | ---- | ----------- | | `videoUrl` | string | Generated video URL | -| `videoFile` | json | Video file object with metadata | +| `videoFile` | file | Video file object with metadata | | `duration` | number | Video duration in seconds | | `width` | number | Video width in pixels | | `height` | number | Video height in pixels | diff --git a/apps/docs/content/docs/en/tools/vision.mdx b/apps/docs/content/docs/en/tools/vision.mdx index 9ba14b5c4..af3b052f4 100644 --- a/apps/docs/content/docs/en/tools/vision.mdx +++ b/apps/docs/content/docs/en/tools/vision.mdx @@ -6,7 +6,7 @@ description: Analyze images with vision models import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -35,8 +35,6 @@ Integrate Vision into the workflow. Can analyze images with vision models. ### `vision_tool` -Process and analyze images using advanced vision models. Capable of understanding image content, extracting text, identifying objects, and providing detailed visual descriptions. - #### Input | Parameter | Type | Required | Description | @@ -49,14 +47,6 @@ Process and analyze images using advanced vision models. Capable of understandin #### Output -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | The analyzed content and description of the image | -| `model` | string | The vision model that was used for analysis | -| `tokens` | number | Total tokens used for the analysis | -| `usage` | object | Detailed token usage breakdown | -| ↳ `input_tokens` | number | Tokens used for input processing | -| ↳ `output_tokens` | number | Tokens used for response generation | -| ↳ `total_tokens` | number | Total tokens consumed | +This tool does not produce any outputs. diff --git a/apps/docs/content/docs/en/tools/zoom.mdx b/apps/docs/content/docs/en/tools/zoom.mdx index 8e5a4c238..4926fd249 100644 --- a/apps/docs/content/docs/en/tools/zoom.mdx +++ b/apps/docs/content/docs/en/tools/zoom.mdx @@ -335,6 +335,7 @@ Get all recordings for a specific Zoom meeting | `meetingId` | string | Yes | The meeting ID or meeting UUID \(e.g., "1234567890" or "4444AAABBBccccc12345=="\) | | `includeFolderItems` | boolean | No | Include items within a folder | | `ttl` | number | No | Time to live for download URLs in seconds \(max 604800\) | +| `downloadFiles` | boolean | No | Download recording files into file outputs | #### Output @@ -364,6 +365,7 @@ Get all recordings for a specific Zoom meeting | ↳ `download_url` | string | URL to download the recording | | ↳ `status` | string | Recording status | | ↳ `recording_type` | string | Type of recording \(shared_screen, audio_only, etc.\) | +| `files` | file[] | Downloaded recording files | ### `zoom_delete_recording` diff --git a/apps/docs/content/docs/es/tools/file.mdx b/apps/docs/content/docs/es/tools/file.mdx index f6c066645..094f34455 100644 --- a/apps/docs/content/docs/es/tools/file.mdx +++ b/apps/docs/content/docs/es/tools/file.mdx @@ -6,7 +6,7 @@ description: Leer y analizar múltiples archivos import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/es/tools/fireflies.mdx b/apps/docs/content/docs/es/tools/fireflies.mdx index 01aa1fe1d..bbfe70c8e 100644 --- a/apps/docs/content/docs/es/tools/fireflies.mdx +++ b/apps/docs/content/docs/es/tools/fireflies.mdx @@ -6,7 +6,7 @@ description: Interactúa con transcripciones y grabaciones de reuniones de Firef import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/es/tools/mistral_parse.mdx b/apps/docs/content/docs/es/tools/mistral_parse.mdx index 62c0922f2..f0c3ff445 100644 --- a/apps/docs/content/docs/es/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/es/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: Extraer texto de documentos PDF import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/fr/tools/file.mdx b/apps/docs/content/docs/fr/tools/file.mdx index f08cac941..eadc453f2 100644 --- a/apps/docs/content/docs/fr/tools/file.mdx +++ b/apps/docs/content/docs/fr/tools/file.mdx @@ -6,7 +6,7 @@ description: Lire et analyser plusieurs fichiers import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/fr/tools/fireflies.mdx b/apps/docs/content/docs/fr/tools/fireflies.mdx index 4b92f437d..72655a1c4 100644 --- a/apps/docs/content/docs/fr/tools/fireflies.mdx +++ b/apps/docs/content/docs/fr/tools/fireflies.mdx @@ -7,7 +7,7 @@ description: Interagissez avec les transcriptions et enregistrements de réunion import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/fr/tools/mistral_parse.mdx b/apps/docs/content/docs/fr/tools/mistral_parse.mdx index f89f36f64..107e68177 100644 --- a/apps/docs/content/docs/fr/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/fr/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: Extraire du texte à partir de documents PDF import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/ja/tools/file.mdx b/apps/docs/content/docs/ja/tools/file.mdx index 931d5a28b..0afffa948 100644 --- a/apps/docs/content/docs/ja/tools/file.mdx +++ b/apps/docs/content/docs/ja/tools/file.mdx @@ -6,7 +6,7 @@ description: 複数のファイルを読み込んで解析する import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/ja/tools/fireflies.mdx b/apps/docs/content/docs/ja/tools/fireflies.mdx index 03cadf5e2..5fd999bd1 100644 --- a/apps/docs/content/docs/ja/tools/fireflies.mdx +++ b/apps/docs/content/docs/ja/tools/fireflies.mdx @@ -6,7 +6,7 @@ description: Fireflies.aiの会議文字起こしと録画を操作 import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/ja/tools/mistral_parse.mdx b/apps/docs/content/docs/ja/tools/mistral_parse.mdx index c18152932..04f90f12f 100644 --- a/apps/docs/content/docs/ja/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/ja/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: PDFドキュメントからテキストを抽出する import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/zh/tools/file.mdx b/apps/docs/content/docs/zh/tools/file.mdx index 479a8ea88..153c0d31e 100644 --- a/apps/docs/content/docs/zh/tools/file.mdx +++ b/apps/docs/content/docs/zh/tools/file.mdx @@ -6,7 +6,7 @@ description: 读取并解析多个文件 import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/zh/tools/fireflies.mdx b/apps/docs/content/docs/zh/tools/fireflies.mdx index d8a8b6436..8f94b3e83 100644 --- a/apps/docs/content/docs/zh/tools/fireflies.mdx +++ b/apps/docs/content/docs/zh/tools/fireflies.mdx @@ -6,7 +6,7 @@ description: 与 Fireflies.ai 会议转录和录音进行交互 import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/docs/content/docs/zh/tools/mistral_parse.mdx b/apps/docs/content/docs/zh/tools/mistral_parse.mdx index 0f4aaa272..0821e4c09 100644 --- a/apps/docs/content/docs/zh/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/zh/tools/mistral_parse.mdx @@ -6,7 +6,7 @@ description: 从 PDF 文档中提取文本 import { BlockInfoCard } from "@/components/ui/block-info-card" diff --git a/apps/sim/app/(auth)/components/support-footer.tsx b/apps/sim/app/(auth)/components/support-footer.tsx index 057334ee5..46614070b 100644 --- a/apps/sim/app/(auth)/components/support-footer.tsx +++ b/apps/sim/app/(auth)/components/support-footer.tsx @@ -1,7 +1,7 @@ 'use client' -import { useBrandConfig } from '@/lib/branding/branding' import { inter } from '@/app/_styles/fonts/inter/inter' +import { useBrandConfig } from '@/ee/whitelabeling' export interface SupportFooterProps { /** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */ diff --git a/apps/sim/app/(landing)/components/nav/nav.tsx b/apps/sim/app/(landing)/components/nav/nav.tsx index 0a72d5b49..5b60d773d 100644 --- a/apps/sim/app/(landing)/components/nav/nav.tsx +++ b/apps/sim/app/(landing)/components/nav/nav.tsx @@ -7,10 +7,10 @@ import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/navigation' import { GithubIcon } from '@/components/icons' -import { useBrandConfig } from '@/lib/branding/branding' import { isHosted } from '@/lib/core/config/feature-flags' import { soehne } from '@/app/_styles/fonts/soehne/soehne' import { getFormattedGitHubStars } from '@/app/(landing)/actions/github' +import { useBrandConfig } from '@/ee/whitelabeling' import { useBrandedButtonClass } from '@/hooks/use-branded-button-class' const logger = createLogger('nav') diff --git a/apps/sim/app/api/a2a/serve/[agentId]/route.ts b/apps/sim/app/api/a2a/serve/[agentId]/route.ts index f8acda5a8..cfc9f06ff 100644 --- a/apps/sim/app/api/a2a/serve/[agentId]/route.ts +++ b/apps/sim/app/api/a2a/serve/[agentId]/route.ts @@ -14,7 +14,6 @@ import { parseWorkflowSSEChunk, } from '@/lib/a2a/utils' import { checkHybridAuth } from '@/lib/auth/hybrid' -import { getBrandConfig } from '@/lib/branding/branding' import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { SSE_HEADERS } from '@/lib/core/utils/sse' @@ -35,6 +34,7 @@ import { type PushNotificationSetParams, type TaskIdParams, } from '@/app/api/a2a/serve/[agentId]/utils' +import { getBrandConfig } from '@/ee/whitelabeling' const logger = createLogger('A2AServeAPI') diff --git a/apps/sim/app/api/tools/confluence/attachments/route.ts b/apps/sim/app/api/tools/confluence/attachments/route.ts index cba790a10..4d25840bd 100644 --- a/apps/sim/app/api/tools/confluence/attachments/route.ts +++ b/apps/sim/app/api/tools/confluence/attachments/route.ts @@ -21,7 +21,8 @@ export async function GET(request: NextRequest) { const accessToken = searchParams.get('accessToken') const pageId = searchParams.get('pageId') const providedCloudId = searchParams.get('cloudId') - const limit = searchParams.get('limit') || '25' + const limit = searchParams.get('limit') || '50' + const cursor = searchParams.get('cursor') if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) @@ -47,7 +48,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?limit=${limit}` + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?${queryParams.toString()}` const response = await fetch(url, { method: 'GET', @@ -77,9 +83,20 @@ export async function GET(request: NextRequest) { fileSize: attachment.fileSize || 0, mediaType: attachment.mediaType || '', downloadUrl: attachment.downloadLink || attachment._links?.download || '', + status: attachment.status ?? null, + webuiUrl: attachment._links?.webui ?? null, + pageId: attachment.pageId ?? null, + blogPostId: attachment.blogPostId ?? null, + comment: attachment.comment ?? null, + version: attachment.version ?? null, })) - return NextResponse.json({ attachments }) + return NextResponse.json({ + attachments, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) } catch (error) { logger.error('Error listing Confluence attachments:', error) return NextResponse.json( diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts new file mode 100644 index 000000000..c186d5ca5 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -0,0 +1,285 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceBlogPostsAPI') + +export const dynamic = 'force-dynamic' + +const getBlogPostSchema = z + .object({ + domain: z.string().min(1, 'Domain is required'), + accessToken: z.string().min(1, 'Access token is required'), + cloudId: z.string().optional(), + blogPostId: z.string().min(1, 'Blog post ID is required'), + bodyFormat: z.string().optional(), + }) + .refine( + (data) => { + const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255) + return validation.isValid + }, + (data) => { + const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255) + return { message: validation.error || 'Invalid blog post ID', path: ['blogPostId'] } + } + ) + +const createBlogPostSchema = z.object({ + domain: z.string().min(1, 'Domain is required'), + accessToken: z.string().min(1, 'Access token is required'), + cloudId: z.string().optional(), + spaceId: z.string().min(1, 'Space ID is required'), + title: z.string().min(1, 'Title is required'), + content: z.string().min(1, 'Content is required'), + status: z.enum(['current', 'draft']).optional(), +}) + +/** + * List all blog posts or get a specific blog post + */ +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + const status = searchParams.get('status') + const sortOrder = searchParams.get('sort') + const cursor = searchParams.get('cursor') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + + if (status) { + queryParams.append('status', status) + } + + if (sortOrder) { + queryParams.append('sort', sortOrder) + } + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts?${queryParams.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to list blog posts (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const blogPosts = (data.results || []).map((post: any) => ({ + id: post.id, + title: post.title, + status: post.status ?? null, + spaceId: post.spaceId ?? null, + authorId: post.authorId ?? null, + createdAt: post.createdAt ?? null, + version: post.version ?? null, + webUrl: post._links?.webui ?? null, + })) + + return NextResponse.json({ + blogPosts, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing blog posts:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Get a specific blog post by ID + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + // Check if this is a create or get request + if (body.title && body.content && body.spaceId) { + // Create blog post + const validation = createBlogPostSchema.safeParse(body) + if (!validation.success) { + const firstError = validation.error.errors[0] + return NextResponse.json({ error: firstError.message }, { status: 400 }) + } + + const { + domain, + accessToken, + cloudId: providedCloudId, + spaceId, + title, + content, + status, + } = validation.data + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts` + + const createBody = { + spaceId, + status: status || 'current', + title, + body: { + representation: 'storage', + value: content, + }, + } + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(createBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to create blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + id: data.id, + title: data.title, + spaceId: data.spaceId, + webUrl: data._links?.webui ?? null, + }) + } + // Get blog post by ID + const validation = getBlogPostSchema.safeParse(body) + if (!validation.success) { + const firstError = validation.error.errors[0] + return NextResponse.json({ error: firstError.message }, { status: 400 }) + } + + const { + domain, + accessToken, + cloudId: providedCloudId, + blogPostId, + bodyFormat, + } = validation.data + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + if (bodyFormat) { + queryParams.append('body-format', bodyFormat) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}${queryParams.toString() ? `?${queryParams.toString()}` : ''}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + id: data.id, + title: data.title, + status: data.status ?? null, + spaceId: data.spaceId ?? null, + authorId: data.authorId ?? null, + createdAt: data.createdAt ?? null, + version: data.version ?? null, + body: data.body ?? null, + webUrl: data._links?.webui ?? null, + }) + } catch (error) { + logger.error('Error with blog post operation:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/comments/route.ts b/apps/sim/app/api/tools/confluence/comments/route.ts index 1c50aa0c0..8c7b03ac0 100644 --- a/apps/sim/app/api/tools/confluence/comments/route.ts +++ b/apps/sim/app/api/tools/confluence/comments/route.ts @@ -105,6 +105,8 @@ export async function GET(request: NextRequest) { const pageId = searchParams.get('pageId') const providedCloudId = searchParams.get('cloudId') const limit = searchParams.get('limit') || '25' + const bodyFormat = searchParams.get('bodyFormat') || 'storage' + const cursor = searchParams.get('cursor') if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) @@ -130,7 +132,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?limit=${limit}` + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + queryParams.append('body-format', bodyFormat) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?${queryParams.toString()}` const response = await fetch(url, { method: 'GET', @@ -154,14 +162,31 @@ export async function GET(request: NextRequest) { const data = await response.json() - const comments = (data.results || []).map((comment: any) => ({ - id: comment.id, - body: comment.body?.storage?.value || comment.body?.view?.value || '', - createdAt: comment.createdAt || '', - authorId: comment.authorId || '', - })) + const comments = (data.results || []).map((comment: any) => { + const bodyValue = comment.body?.storage?.value || comment.body?.view?.value || '' + return { + id: comment.id, + body: { + value: bodyValue, + representation: bodyFormat, + }, + createdAt: comment.createdAt || '', + authorId: comment.authorId || '', + status: comment.status ?? null, + title: comment.title ?? null, + pageId: comment.pageId ?? null, + blogPostId: comment.blogPostId ?? null, + parentCommentId: comment.parentCommentId ?? null, + version: comment.version ?? null, + } + }) - return NextResponse.json({ comments }) + return NextResponse.json({ + comments, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) } catch (error) { logger.error('Error listing Confluence comments:', error) return NextResponse.json( diff --git a/apps/sim/app/api/tools/confluence/labels/route.ts b/apps/sim/app/api/tools/confluence/labels/route.ts index d008c3d55..ac5eb176a 100644 --- a/apps/sim/app/api/tools/confluence/labels/route.ts +++ b/apps/sim/app/api/tools/confluence/labels/route.ts @@ -22,6 +22,7 @@ export async function POST(request: NextRequest) { cloudId: providedCloudId, pageId, labelName, + prefix: labelPrefix, } = await request.json() if (!domain) { @@ -52,12 +53,14 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label` - const body = { - prefix: 'global', - name: labelName, - } + const body = [ + { + prefix: labelPrefix || 'global', + name: labelName, + }, + ] const response = await fetch(url, { method: 'POST', @@ -82,7 +85,14 @@ export async function POST(request: NextRequest) { } const data = await response.json() - return NextResponse.json({ ...data, pageId, labelName }) + const addedLabel = data.results?.[0] || data[0] || data + return NextResponse.json({ + id: addedLabel.id ?? '', + name: addedLabel.name ?? labelName, + prefix: addedLabel.prefix ?? labelPrefix ?? 'global', + pageId, + labelName, + }) } catch (error) { logger.error('Error adding Confluence label:', error) return NextResponse.json( @@ -105,6 +115,8 @@ export async function GET(request: NextRequest) { const accessToken = searchParams.get('accessToken') const pageId = searchParams.get('pageId') const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + const cursor = searchParams.get('cursor') if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) @@ -130,7 +142,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels?${queryParams.toString()}` const response = await fetch(url, { method: 'GET', @@ -160,7 +177,12 @@ export async function GET(request: NextRequest) { prefix: label.prefix || 'global', })) - return NextResponse.json({ labels }) + return NextResponse.json({ + labels, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) } catch (error) { logger.error('Error listing Confluence labels:', error) return NextResponse.json( diff --git a/apps/sim/app/api/tools/confluence/page-ancestors/route.ts b/apps/sim/app/api/tools/confluence/page-ancestors/route.ts new file mode 100644 index 000000000..743cce75a --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-ancestors/route.ts @@ -0,0 +1,96 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePageAncestorsAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get ancestors (parent pages) of a specific Confluence page. + * Uses GET /wiki/api/v2/pages/{id}/ancestors + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 25 } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/ancestors?${queryParams.toString()}` + + logger.info(`Fetching ancestors for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get page ancestors (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const ancestors = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + webUrl: page._links?.webui ?? null, + })) + + return NextResponse.json({ + ancestors, + pageId, + }) + } catch (error) { + logger.error('Error getting page ancestors:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-children/route.ts b/apps/sim/app/api/tools/confluence/page-children/route.ts new file mode 100644 index 000000000..7cd7a41bd --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-children/route.ts @@ -0,0 +1,104 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePageChildrenAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get child pages of a specific Confluence page. + * Uses GET /wiki/api/v2/pages/{id}/children + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/children?${queryParams.toString()}` + + logger.info(`Fetching child pages for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get child pages (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const children = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + childPosition: page.childPosition ?? null, + webUrl: page._links?.webui ?? null, + })) + + return NextResponse.json({ + children, + parentId: pageId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error getting child pages:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-properties/route.ts b/apps/sim/app/api/tools/confluence/page-properties/route.ts new file mode 100644 index 000000000..f8c3ce0ee --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-properties/route.ts @@ -0,0 +1,365 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePagePropertiesAPI') + +export const dynamic = 'force-dynamic' + +const createPropertySchema = z.object({ + domain: z.string().min(1, 'Domain is required'), + accessToken: z.string().min(1, 'Access token is required'), + cloudId: z.string().optional(), + pageId: z.string().min(1, 'Page ID is required'), + key: z.string().min(1, 'Property key is required'), + value: z.any(), +}) + +const updatePropertySchema = z.object({ + domain: z.string().min(1, 'Domain is required'), + accessToken: z.string().min(1, 'Access token is required'), + cloudId: z.string().optional(), + pageId: z.string().min(1, 'Page ID is required'), + propertyId: z.string().min(1, 'Property ID is required'), + key: z.string().min(1, 'Property key is required'), + value: z.any(), + versionNumber: z.number().min(1, 'Version number is required'), +}) + +const deletePropertySchema = z.object({ + domain: z.string().min(1, 'Domain is required'), + accessToken: z.string().min(1, 'Access token is required'), + cloudId: z.string().optional(), + pageId: z.string().min(1, 'Page ID is required'), + propertyId: z.string().min(1, 'Property ID is required'), +}) + +/** + * List all content properties on a page. + */ +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const pageId = searchParams.get('pageId') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '50' + const cursor = searchParams.get('cursor') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties?${queryParams.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list page properties (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const properties = (data.results || []).map((prop: any) => ({ + id: prop.id, + key: prop.key, + value: prop.value ?? null, + version: prop.version ?? null, + })) + + return NextResponse.json({ + properties, + pageId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing page properties:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Create a new content property on a page. + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + const validation = createPropertySchema.safeParse(body) + if (!validation.success) { + const firstError = validation.error.errors[0] + return NextResponse.json({ error: firstError.message }, { status: 400 }) + } + + const { domain, accessToken, cloudId: providedCloudId, pageId, key, value } = validation.data + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties` + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ key, value }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to create page property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + id: data.id, + key: data.key, + value: data.value, + version: data.version, + pageId, + }) + } catch (error) { + logger.error('Error creating page property:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Update a content property on a page. + */ +export async function PUT(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + const validation = updatePropertySchema.safeParse(body) + if (!validation.success) { + const firstError = validation.error.errors[0] + return NextResponse.json({ error: firstError.message }, { status: 400 }) + } + + const { + domain, + accessToken, + cloudId: providedCloudId, + pageId, + propertyId, + key, + value, + versionNumber, + } = validation.data + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255) + if (!propertyIdValidation.isValid) { + return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}` + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + key, + value, + version: { number: versionNumber }, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to update page property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + id: data.id, + key: data.key, + value: data.value, + version: data.version, + pageId, + }) + } catch (error) { + logger.error('Error updating page property:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Delete a content property from a page. + */ +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + + const validation = deletePropertySchema.safeParse(body) + if (!validation.success) { + const firstError = validation.error.errors[0] + return NextResponse.json({ error: firstError.message }, { status: 400 }) + } + + const { domain, accessToken, cloudId: providedCloudId, pageId, propertyId } = validation.data + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255) + if (!propertyIdValidation.isValid) { + return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties/${propertyId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete page property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ propertyId, pageId, deleted: true }) + } catch (error) { + logger.error('Error deleting page property:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-versions/route.ts b/apps/sim/app/api/tools/confluence/page-versions/route.ts new file mode 100644 index 000000000..9d7c16206 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-versions/route.ts @@ -0,0 +1,151 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePageVersionsAPI') + +export const dynamic = 'force-dynamic' + +/** + * List all versions of a page or get a specific version. + * Uses GET /wiki/api/v2/pages/{id}/versions + * and GET /wiki/api/v2/pages/{page-id}/versions/{version-number} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + pageId, + versionNumber, + cloudId: providedCloudId, + limit = 50, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // If versionNumber is provided, get specific version + if (versionNumber !== undefined && versionNumber !== null) { + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}` + + logger.info(`Fetching version ${versionNumber} for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get page version (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + return NextResponse.json({ + version: { + number: data.number, + message: data.message ?? null, + minorEdit: data.minorEdit ?? false, + authorId: data.authorId ?? null, + createdAt: data.createdAt ?? null, + }, + pageId, + }) + } + // List all versions + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions?${queryParams.toString()}` + + logger.info(`Fetching versions for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to list page versions (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const versions = (data.results || []).map((version: any) => ({ + number: version.number, + message: version.message ?? null, + minorEdit: version.minorEdit ?? false, + authorId: version.authorId ?? null, + createdAt: version.createdAt ?? null, + })) + + return NextResponse.json({ + versions, + pageId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error with page versions:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page/route.ts b/apps/sim/app/api/tools/confluence/page/route.ts index 9cc83914e..232e453a9 100644 --- a/apps/sim/app/api/tools/confluence/page/route.ts +++ b/apps/sim/app/api/tools/confluence/page/route.ts @@ -62,6 +62,7 @@ const deletePageSchema = z accessToken: z.string().min(1, 'Access token is required'), cloudId: z.string().optional(), pageId: z.string().min(1, 'Page ID is required'), + purge: z.boolean().optional(), }) .refine( (data) => { @@ -98,7 +99,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?expand=body.storage,body.view,body.atlas_doc_format` + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage` const response = await fetch(url, { method: 'GET', @@ -130,16 +131,18 @@ export async function POST(request: NextRequest) { id: data.id, title: data.title, body: { - view: { - value: - data.body?.storage?.value || - data.body?.view?.value || - data.body?.atlas_doc_format?.value || - data.content || // try alternative fields - data.description || - `Content for page ${data.title}`, // fallback content + storage: { + value: data.body?.storage?.value ?? null, + representation: 'storage', }, }, + status: data.status ?? null, + spaceId: data.spaceId ?? null, + parentId: data.parentId ?? null, + authorId: data.authorId ?? null, + createdAt: data.createdAt ?? null, + version: data.version ?? null, + _links: data._links ?? null, }) } catch (error) { logger.error('Error fetching Confluence page:', error) @@ -274,7 +277,7 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: firstError.message }, { status: 400 }) } - const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data + const { domain, accessToken, cloudId: providedCloudId, pageId, purge } = validation.data const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) @@ -283,7 +286,12 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}` + const queryParams = new URLSearchParams() + if (purge) { + queryParams.append('purge', 'true') + } + const queryString = queryParams.toString() + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}${queryString ? `?${queryString}` : ''}` const response = await fetch(url, { method: 'DELETE', diff --git a/apps/sim/app/api/tools/confluence/pages/route.ts b/apps/sim/app/api/tools/confluence/pages/route.ts index e225bf34e..739dc0659 100644 --- a/apps/sim/app/api/tools/confluence/pages/route.ts +++ b/apps/sim/app/api/tools/confluence/pages/route.ts @@ -32,7 +32,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } - // Use provided cloudId or fetch it if not provided const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') @@ -40,7 +39,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - // Build the URL with query parameters const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages` const queryParams = new URLSearchParams() @@ -57,7 +55,6 @@ export async function POST(request: NextRequest) { logger.info(`Fetching Confluence pages from: ${url}`) - // Make the request to Confluence API with OAuth Bearer token const response = await fetch(url, { method: 'GET', headers: { @@ -79,7 +76,6 @@ export async function POST(request: NextRequest) { } catch (e) { logger.error('Could not parse error response as JSON:', e) - // Try to get the response text for more context try { const text = await response.text() logger.error('Response text:', text) diff --git a/apps/sim/app/api/tools/confluence/search-in-space/route.ts b/apps/sim/app/api/tools/confluence/search-in-space/route.ts new file mode 100644 index 000000000..b731a6735 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/search-in-space/route.ts @@ -0,0 +1,120 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSearchInSpaceAPI') + +export const dynamic = 'force-dynamic' + +/** + * Search for content within a specific Confluence space using CQL. + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + spaceKey, + query, + cloudId: providedCloudId, + limit = 25, + contentType, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceKey) { + return NextResponse.json({ error: 'Space key is required' }, { status: 400 }) + } + + const spaceKeyValidation = validateAlphanumericId(spaceKey, 'spaceKey', 255) + if (!spaceKeyValidation.isValid) { + return NextResponse.json({ error: spaceKeyValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"') + + let cql = `space = "${escapeCqlValue(spaceKey)}"` + + if (query) { + cql += ` AND text ~ "${escapeCqlValue(query)}"` + } + + if (contentType) { + cql += ` AND type = "${escapeCqlValue(contentType)}"` + } + + const searchParams = new URLSearchParams({ + cql, + limit: String(Math.min(limit, 250)), + }) + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/search?${searchParams.toString()}` + + logger.info(`Searching in space ${spaceKey} with CQL: ${cql}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to search in space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const results = (data.results || []).map((result: any) => ({ + id: result.content?.id ?? result.id, + title: result.content?.title ?? result.title, + type: result.content?.type ?? result.type, + status: result.content?.status ?? null, + url: result.url ?? result._links?.webui ?? '', + excerpt: result.excerpt ?? '', + lastModified: result.lastModified ?? null, + })) + + return NextResponse.json({ + results, + spaceKey, + totalSize: data.totalSize ?? results.length, + }) + } catch (error) { + logger.error('Error searching in space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/search/route.ts b/apps/sim/app/api/tools/confluence/search/route.ts index adeedb392..b8f541332 100644 --- a/apps/sim/app/api/tools/confluence/search/route.ts +++ b/apps/sim/app/api/tools/confluence/search/route.ts @@ -42,8 +42,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } + const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"') + const searchParams = new URLSearchParams({ - cql: `text ~ "${query}"`, + cql: `text ~ "${escapeCqlValue(query)}"`, limit: limit.toString(), }) @@ -70,13 +72,27 @@ export async function POST(request: NextRequest) { const data = await response.json() - const results = (data.results || []).map((result: any) => ({ - id: result.content?.id || result.id, - title: result.content?.title || result.title, - type: result.content?.type || result.type, - url: result.url || result._links?.webui || '', - excerpt: result.excerpt || '', - })) + const results = (data.results || []).map((result: any) => { + const spaceData = result.resultGlobalContainer || result.content?.space + return { + id: result.content?.id || result.id, + title: result.content?.title || result.title, + type: result.content?.type || result.type, + url: result.url || result._links?.webui || '', + excerpt: result.excerpt || '', + status: result.content?.status ?? null, + spaceKey: result.resultGlobalContainer?.key ?? result.content?.space?.key ?? null, + space: spaceData + ? { + id: spaceData.id ?? null, + key: spaceData.key ?? null, + name: spaceData.name ?? spaceData.title ?? null, + } + : null, + lastModified: result.lastModified ?? result.content?.history?.lastUpdated?.when ?? null, + entityType: result.entityType ?? null, + } + }) return NextResponse.json({ results }) } catch (error) { diff --git a/apps/sim/app/api/tools/confluence/space-blogposts/route.ts b/apps/sim/app/api/tools/confluence/space-blogposts/route.ts new file mode 100644 index 000000000..4607f9f57 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-blogposts/route.ts @@ -0,0 +1,124 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpaceBlogPostsAPI') + +export const dynamic = 'force-dynamic' + +/** + * List all blog posts in a specific Confluence space. + * Uses GET /wiki/api/v2/spaces/{id}/blogposts + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + spaceId, + cloudId: providedCloudId, + limit = 25, + status, + bodyFormat, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (status) { + queryParams.append('status', status) + } + + if (bodyFormat) { + queryParams.append('body-format', bodyFormat) + } + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/blogposts?${queryParams.toString()}` + + logger.info(`Fetching blog posts in space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list blog posts in space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const blogPosts = (data.results || []).map((post: any) => ({ + id: post.id, + title: post.title, + status: post.status ?? null, + spaceId: post.spaceId ?? null, + authorId: post.authorId ?? null, + createdAt: post.createdAt ?? null, + version: post.version ?? null, + body: post.body ?? null, + webUrl: post._links?.webui ?? null, + })) + + return NextResponse.json({ + blogPosts, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing blog posts in space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space-pages/route.ts b/apps/sim/app/api/tools/confluence/space-pages/route.ts new file mode 100644 index 000000000..fcf17efa0 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-pages/route.ts @@ -0,0 +1,125 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpacePagesAPI') + +export const dynamic = 'force-dynamic' + +/** + * List all pages in a specific Confluence space. + * Uses GET /wiki/api/v2/spaces/{id}/pages + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + spaceId, + cloudId: providedCloudId, + limit = 50, + status, + bodyFormat, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (status) { + queryParams.append('status', status) + } + + if (bodyFormat) { + queryParams.append('body-format', bodyFormat) + } + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/pages?${queryParams.toString()}` + + logger.info(`Fetching pages in space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list pages in space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const pages = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + parentId: page.parentId ?? null, + authorId: page.authorId ?? null, + createdAt: page.createdAt ?? null, + version: page.version ?? null, + body: page.body ?? null, + webUrl: page._links?.webui ?? null, + })) + + return NextResponse.json({ + pages, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing pages in space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/spaces/route.ts b/apps/sim/app/api/tools/confluence/spaces/route.ts index 53daafe0e..0ce8dd0ee 100644 --- a/apps/sim/app/api/tools/confluence/spaces/route.ts +++ b/apps/sim/app/api/tools/confluence/spaces/route.ts @@ -21,6 +21,7 @@ export async function GET(request: NextRequest) { const accessToken = searchParams.get('accessToken') const providedCloudId = searchParams.get('cloudId') const limit = searchParams.get('limit') || '25' + const cursor = searchParams.get('cursor') if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) @@ -37,7 +38,12 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=${limit}` + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(Number(limit), 250))) + if (cursor) { + queryParams.append('cursor', cursor) + } + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?${queryParams.toString()}` const response = await fetch(url, { method: 'GET', @@ -67,9 +73,18 @@ export async function GET(request: NextRequest) { key: space.key, type: space.type, status: space.status, + authorId: space.authorId ?? null, + createdAt: space.createdAt ?? null, + homepageId: space.homepageId ?? null, + description: space.description ?? null, })) - return NextResponse.json({ spaces }) + return NextResponse.json({ + spaces, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) } catch (error) { logger.error('Error listing Confluence spaces:', error) return NextResponse.json( diff --git a/apps/sim/app/chat/components/header/header.tsx b/apps/sim/app/chat/components/header/header.tsx index 55fa058b9..68411916f 100644 --- a/apps/sim/app/chat/components/header/header.tsx +++ b/apps/sim/app/chat/components/header/header.tsx @@ -3,8 +3,8 @@ import Image from 'next/image' import Link from 'next/link' import { GithubIcon } from '@/components/icons' -import { useBrandConfig } from '@/lib/branding/branding' import { inter } from '@/app/_styles/fonts/inter/inter' +import { useBrandConfig } from '@/ee/whitelabeling' interface ChatHeaderProps { chatConfig: { diff --git a/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx b/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx index d882fbcf9..a647776bc 100644 --- a/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx +++ b/apps/sim/app/form/[identifier]/components/powered-by-sim.tsx @@ -1,8 +1,8 @@ 'use client' import Image from 'next/image' -import { useBrandConfig } from '@/lib/branding/branding' import { inter } from '@/app/_styles/fonts/inter/inter' +import { useBrandConfig } from '@/ee/whitelabeling' export function PoweredBySim() { const brandConfig = useBrandConfig() diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 6ab3aae35..11d1b3036 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -2,9 +2,12 @@ import type { Metadata, Viewport } from 'next' import Script from 'next/script' import { PublicEnvScript } from 'next-runtime-env' import { BrandedLayout } from '@/components/branded-layout' -import { generateThemeCSS } from '@/lib/branding/inject-theme' -import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata' import { PostHogProvider } from '@/app/_shell/providers/posthog-provider' +import { + generateBrandedMetadata, + generateStructuredData, + generateThemeCSS, +} from '@/ee/whitelabeling' import '@/app/_styles/globals.css' import { OneDollarStats } from '@/components/analytics/onedollarstats' import { isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/feature-flags' diff --git a/apps/sim/app/manifest.ts b/apps/sim/app/manifest.ts index 2ec404966..9ea650743 100644 --- a/apps/sim/app/manifest.ts +++ b/apps/sim/app/manifest.ts @@ -1,5 +1,5 @@ import type { MetadataRoute } from 'next' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' export default function manifest(): MetadataRoute.Manifest { const brand = getBrandConfig() diff --git a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx index 3edd059aa..5b1ace2ea 100644 --- a/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx +++ b/apps/sim/app/resume/[workflowId]/[executionId]/resume-page-client.tsx @@ -24,8 +24,8 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { useBrandConfig } from '@/lib/branding/branding' import Nav from '@/app/(landing)/components/nav/nav' +import { useBrandConfig } from '@/ee/whitelabeling' import type { ResumeStatus } from '@/executor/types' interface ResumeLinks { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index b6e7aa4cb..4888a9684 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -74,6 +74,12 @@ const SCOPE_DESCRIPTIONS: Record = { 'write:label:confluence': 'Add and remove labels', 'search:confluence': 'Search Confluence content', 'readonly:content.attachment:confluence': 'View attachments', + 'read:blogpost:confluence': 'View Confluence blog posts', + 'write:blogpost:confluence': 'Create and update Confluence blog posts', + 'read:content.property:confluence': 'View properties on Confluence content', + 'write:content.property:confluence': 'Create and manage content properties', + 'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)', + 'read:content.metadata:confluence': 'View content metadata (required for ancestors)', 'read:me': 'Read profile information', 'database.read': 'Read database', 'database.write': 'Write to database', @@ -358,6 +364,7 @@ export function OAuthRequiredModal({ logger.info('Linking OAuth2:', { providerId, requiredScopes, + hasNewScopes: newScopes.length > 0, }) if (providerId === 'trello') { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 540f97bba..12f4cb510 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -100,7 +100,7 @@ const BlockRow = memo(function BlockRow({ >
{BlockIcon && } @@ -276,7 +276,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({ >
{BlockIcon && } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx index 7ab1d0737..2893557be 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/general/general.tsx @@ -19,11 +19,11 @@ import { import { Input, Skeleton } from '@/components/ui' import { signOut, useSession } from '@/lib/auth/auth-client' import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' -import { useBrandConfig } from '@/lib/branding/branding' import { getEnv, isTruthy } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' import { getBaseUrl } from '@/lib/core/utils/urls' import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/hooks/use-profile-picture-upload' +import { useBrandConfig } from '@/ee/whitelabeling' import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { useUpdateUserProfile, useUserProfile } from '@/hooks/queries/user-profile' import { clearUserData } from '@/stores' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx index 71ef8060c..742865fb7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/usage-indicator/usage-indicator.tsx @@ -397,7 +397,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) { return () => window.clearInterval(interval) }, [isHovered, pillCount, startAnimationIndex]) - if (isLoading) { + if (isLoading && !subscriptionData) { return (
diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 5bdb21e5e..f3197bf27 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -75,6 +75,12 @@ export const ConfluenceBlock: BlockConfig = { 'search:confluence', 'read:me', 'offline_access', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'read:content.property:confluence', + 'write:content.property:confluence', + 'read:hierarchical-content:confluence', + 'read:content.metadata:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -334,6 +340,7 @@ export const ConfluenceBlock: BlockConfig = { ts: { type: 'string', description: 'Timestamp' }, pageId: { type: 'string', description: 'Page identifier' }, content: { type: 'string', description: 'Page content' }, + body: { type: 'json', description: 'Page body with storage format' }, title: { type: 'string', description: 'Page title' }, url: { type: 'string', description: 'Page or resource URL' }, success: { type: 'boolean', description: 'Operation success status' }, @@ -371,31 +378,46 @@ export const ConfluenceV2Block: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ + // Page Operations { label: 'Read Page', id: 'read' }, { label: 'Create Page', id: 'create' }, { label: 'Update Page', id: 'update' }, { label: 'Delete Page', id: 'delete' }, + { label: 'List Pages in Space', id: 'list_pages_in_space' }, + { label: 'Get Page Children', id: 'get_page_children' }, + { label: 'Get Page Ancestors', id: 'get_page_ancestors' }, + // Version Operations + { label: 'List Page Versions', id: 'list_page_versions' }, + { label: 'Get Page Version', id: 'get_page_version' }, + // Page Property Operations + { label: 'List Page Properties', id: 'list_page_properties' }, + { label: 'Create Page Property', id: 'create_page_property' }, + // Search Operations { label: 'Search Content', id: 'search' }, + { label: 'Search in Space', id: 'search_in_space' }, + // Blog Post Operations + { label: 'List Blog Posts', id: 'list_blogposts' }, + { label: 'Get Blog Post', id: 'get_blogpost' }, + { label: 'Create Blog Post', id: 'create_blogpost' }, + { label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' }, + // Comment Operations { label: 'Create Comment', id: 'create_comment' }, { label: 'List Comments', id: 'list_comments' }, { label: 'Update Comment', id: 'update_comment' }, { label: 'Delete Comment', id: 'delete_comment' }, + // Attachment Operations { label: 'Upload Attachment', id: 'upload_attachment' }, { label: 'List Attachments', id: 'list_attachments' }, { label: 'Delete Attachment', id: 'delete_attachment' }, + // Label Operations { label: 'List Labels', id: 'list_labels' }, + { label: 'Add Label', id: 'add_label' }, + // Space Operations { label: 'Get Space', id: 'get_space' }, { label: 'List Spaces', id: 'list_spaces' }, ], value: () => 'read', }, - { - id: 'domain', - title: 'Domain', - type: 'short-input', - placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)', - required: true, - }, { id: 'credential', title: 'Confluence Account', @@ -424,10 +446,23 @@ export const ConfluenceV2Block: BlockConfig = { 'search:confluence', 'read:me', 'offline_access', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'read:content.property:confluence', + 'write:content.property:confluence', + 'read:hierarchical-content:confluence', + 'read:content.metadata:confluence', ], placeholder: 'Select Confluence account', required: true, }, + { + id: 'domain', + title: 'Domain', + type: 'short-input', + placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)', + required: true, + }, { id: 'pageId', title: 'Select Page', @@ -437,6 +472,20 @@ export const ConfluenceV2Block: BlockConfig = { placeholder: 'Select Confluence page', dependsOn: ['credential', 'domain'], mode: 'basic', + condition: { + field: 'operation', + value: [ + 'list_pages_in_space', + 'list_blogposts', + 'get_blogpost', + 'list_blogposts_in_space', + 'search', + 'search_in_space', + 'get_space', + 'list_spaces', + ], + not: true, + }, }, { id: 'manualPageId', @@ -445,6 +494,20 @@ export const ConfluenceV2Block: BlockConfig = { canonicalParamId: 'pageId', placeholder: 'Enter Confluence page ID', mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_pages_in_space', + 'list_blogposts', + 'get_blogpost', + 'list_blogposts_in_space', + 'search', + 'search_in_space', + 'get_space', + 'list_spaces', + ], + not: true, + }, }, { id: 'spaceId', @@ -452,21 +515,63 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter Confluence space ID', required: true, - condition: { field: 'operation', value: ['create', 'get_space'] }, + condition: { + field: 'operation', + value: [ + 'create', + 'get_space', + 'list_pages_in_space', + 'search_in_space', + 'create_blogpost', + 'list_blogposts_in_space', + ], + }, + }, + { + id: 'blogPostId', + title: 'Blog Post ID', + type: 'short-input', + placeholder: 'Enter blog post ID', + required: true, + condition: { field: 'operation', value: 'get_blogpost' }, + }, + { + id: 'versionNumber', + title: 'Version Number', + type: 'short-input', + placeholder: 'Enter version number', + required: true, + condition: { field: 'operation', value: 'get_page_version' }, + }, + { + id: 'propertyKey', + title: 'Property Key', + type: 'short-input', + placeholder: 'Enter property key/name', + required: true, + condition: { field: 'operation', value: 'create_page_property' }, + }, + { + id: 'propertyValue', + title: 'Property Value', + type: 'long-input', + placeholder: 'Enter property value (JSON supported)', + required: true, + condition: { field: 'operation', value: 'create_page_property' }, }, { id: 'title', title: 'Title', type: 'short-input', - placeholder: 'Enter title for the page', - condition: { field: 'operation', value: ['create', 'update'] }, + placeholder: 'Enter title', + condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, }, { id: 'content', title: 'Content', type: 'long-input', - placeholder: 'Enter content for the page', - condition: { field: 'operation', value: ['create', 'update'] }, + placeholder: 'Enter content', + condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, }, { id: 'parentId', @@ -481,7 +586,7 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter search query', required: true, - condition: { field: 'operation', value: 'search' }, + condition: { field: 'operation', value: ['search', 'search_in_space'] }, }, { id: 'comment', @@ -545,40 +650,140 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter label name', required: true, - condition: { field: 'operation', value: ['add_label', 'remove_label'] }, + condition: { field: 'operation', value: 'add_label' }, + }, + { + id: 'labelPrefix', + title: 'Label Prefix', + type: 'dropdown', + options: [ + { label: 'Global (default)', id: 'global' }, + { label: 'My', id: 'my' }, + { label: 'Team', id: 'team' }, + { label: 'System', id: 'system' }, + ], + value: () => 'global', + condition: { field: 'operation', value: 'add_label' }, + }, + { + id: 'blogPostStatus', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'Published (current)', id: 'current' }, + { label: 'Draft', id: 'draft' }, + ], + value: () => 'current', + condition: { field: 'operation', value: 'create_blogpost' }, + }, + { + id: 'purge', + title: 'Permanently Delete', + type: 'switch', + condition: { field: 'operation', value: 'delete' }, + }, + { + id: 'bodyFormat', + title: 'Body Format', + type: 'dropdown', + options: [ + { label: 'Storage (default)', id: 'storage' }, + { label: 'Atlas Doc Format', id: 'atlas_doc_format' }, + { label: 'View', id: 'view' }, + { label: 'Export View', id: 'export_view' }, + ], + value: () => 'storage', + condition: { field: 'operation', value: 'list_comments' }, }, { id: 'limit', title: 'Limit', type: 'short-input', - placeholder: 'Enter maximum number of results (default: 25)', + placeholder: 'Enter maximum number of results (default: 50, max: 250)', condition: { field: 'operation', - value: ['search', 'list_comments', 'list_attachments', 'list_spaces'], + value: [ + 'search', + 'search_in_space', + 'list_comments', + 'list_attachments', + 'list_spaces', + 'list_pages_in_space', + 'list_blogposts', + 'list_blogposts_in_space', + 'get_page_children', + 'list_page_versions', + 'list_page_properties', + 'list_labels', + ], + }, + }, + { + id: 'cursor', + title: 'Pagination Cursor', + type: 'short-input', + placeholder: 'Enter cursor from previous response (optional)', + condition: { + field: 'operation', + value: [ + 'list_comments', + 'list_attachments', + 'list_spaces', + 'list_pages_in_space', + 'list_blogposts', + 'list_blogposts_in_space', + 'get_page_children', + 'list_page_versions', + 'list_page_properties', + 'list_labels', + ], }, }, ], tools: { access: [ + // Page Tools 'confluence_retrieve', 'confluence_update', 'confluence_create_page', 'confluence_delete_page', + 'confluence_list_pages_in_space', + 'confluence_get_page_children', + 'confluence_get_page_ancestors', + // Version Tools + 'confluence_list_page_versions', + 'confluence_get_page_version', + // Property Tools + 'confluence_list_page_properties', + 'confluence_create_page_property', + // Search Tools 'confluence_search', + 'confluence_search_in_space', + // Blog Post Tools + 'confluence_list_blogposts', + 'confluence_get_blogpost', + 'confluence_create_blogpost', + 'confluence_list_blogposts_in_space', + // Comment Tools 'confluence_create_comment', 'confluence_list_comments', 'confluence_update_comment', 'confluence_delete_comment', + // Attachment Tools 'confluence_upload_attachment', 'confluence_list_attachments', 'confluence_delete_attachment', + // Label Tools 'confluence_list_labels', + 'confluence_add_label', + // Space Tools 'confluence_get_space', 'confluence_list_spaces', ], config: { tool: (params) => { switch (params.operation) { + // Page Operations case 'read': return 'confluence_retrieve' case 'create': @@ -587,8 +792,37 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_update' case 'delete': return 'confluence_delete_page' + case 'list_pages_in_space': + return 'confluence_list_pages_in_space' + case 'get_page_children': + return 'confluence_get_page_children' + case 'get_page_ancestors': + return 'confluence_get_page_ancestors' + // Version Operations + case 'list_page_versions': + return 'confluence_list_page_versions' + case 'get_page_version': + return 'confluence_get_page_version' + // Property Operations + case 'list_page_properties': + return 'confluence_list_page_properties' + case 'create_page_property': + return 'confluence_create_page_property' + // Search Operations case 'search': return 'confluence_search' + case 'search_in_space': + return 'confluence_search_in_space' + // Blog Post Operations + case 'list_blogposts': + return 'confluence_list_blogposts' + case 'get_blogpost': + return 'confluence_get_blogpost' + case 'create_blogpost': + return 'confluence_create_blogpost' + case 'list_blogposts_in_space': + return 'confluence_list_blogposts_in_space' + // Comment Operations case 'create_comment': return 'confluence_create_comment' case 'list_comments': @@ -597,14 +831,19 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_update_comment' case 'delete_comment': return 'confluence_delete_comment' + // Attachment Operations case 'upload_attachment': return 'confluence_upload_attachment' case 'list_attachments': return 'confluence_list_attachments' case 'delete_attachment': return 'confluence_delete_attachment' + // Label Operations case 'list_labels': return 'confluence_list_labels' + case 'add_label': + return 'confluence_add_label' + // Space Operations case 'get_space': return 'confluence_get_space' case 'list_spaces': @@ -624,6 +863,15 @@ export const ConfluenceV2Block: BlockConfig = { attachmentFile, attachmentFileName, attachmentComment, + blogPostId, + versionNumber, + propertyKey, + propertyValue, + labelPrefix, + blogPostStatus, + purge, + bodyFormat, + cursor, ...rest } = params @@ -638,9 +886,23 @@ export const ConfluenceV2Block: BlockConfig = { 'list_attachments', 'list_labels', 'upload_attachment', + 'add_label', + 'get_page_children', + 'get_page_ancestors', + 'list_page_versions', + 'get_page_version', + 'list_page_properties', + 'create_page_property', ] - const requiresSpaceId = ['create', 'get_space'] + const requiresSpaceId = [ + 'create', + 'get_space', + 'list_pages_in_space', + 'search_in_space', + 'create_blogpost', + 'list_blogposts_in_space', + ] if (requiresPageId.includes(operation) && !effectivePageId) { throw new Error('Page ID is required. Please select a page or enter a page ID manually.') @@ -650,6 +912,91 @@ export const ConfluenceV2Block: BlockConfig = { throw new Error('Space ID is required for this operation.') } + if (operation === 'get_blogpost' && !blogPostId) { + throw new Error('Blog Post ID is required for this operation.') + } + + if (operation === 'get_page_version' && !versionNumber) { + throw new Error('Version number is required for this operation.') + } + + if (operation === 'add_label') { + return { + credential, + pageId: effectivePageId, + operation, + prefix: labelPrefix || 'global', + ...rest, + } + } + + if (operation === 'create_blogpost') { + return { + credential, + operation, + status: blogPostStatus || 'current', + ...rest, + } + } + + if (operation === 'delete') { + return { + credential, + pageId: effectivePageId, + operation, + purge: purge || false, + ...rest, + } + } + + if (operation === 'list_comments') { + return { + credential, + pageId: effectivePageId, + operation, + bodyFormat: bodyFormat || 'storage', + cursor: cursor || undefined, + ...rest, + } + } + + // Operations that support cursor pagination + const supportsCursor = [ + 'list_attachments', + 'list_spaces', + 'list_pages_in_space', + 'list_blogposts', + 'list_blogposts_in_space', + 'get_page_children', + 'list_page_versions', + 'list_page_properties', + 'list_labels', + ] + + if (supportsCursor.includes(operation) && cursor) { + return { + credential, + pageId: effectivePageId || undefined, + operation, + cursor, + ...rest, + } + } + + if (operation === 'create_page_property') { + if (!propertyKey) { + throw new Error('Property key is required for this operation.') + } + return { + credential, + pageId: effectivePageId, + operation, + key: propertyKey, + value: propertyValue, + ...rest, + } + } + if (operation === 'upload_attachment') { const fileInput = attachmentFileUpload || attachmentFileReference || attachmentFile const normalizedFile = normalizeFileInput(fileInput, { single: true }) @@ -670,6 +1017,8 @@ export const ConfluenceV2Block: BlockConfig = { return { credential, pageId: effectivePageId || undefined, + blogPostId: blogPostId || undefined, + versionNumber: versionNumber ? Number.parseInt(String(versionNumber), 10) : undefined, operation, ...rest, } @@ -683,8 +1032,12 @@ export const ConfluenceV2Block: BlockConfig = { pageId: { type: 'string', description: 'Page identifier' }, manualPageId: { type: 'string', description: 'Manual page identifier' }, spaceId: { type: 'string', description: 'Space identifier' }, - title: { type: 'string', description: 'Page title' }, - content: { type: 'string', description: 'Page content' }, + blogPostId: { type: 'string', description: 'Blog post identifier' }, + versionNumber: { type: 'number', description: 'Page version number' }, + propertyKey: { type: 'string', description: 'Property key/name' }, + propertyValue: { type: 'json', description: 'Property value (JSON)' }, + title: { type: 'string', description: 'Page or blog post title' }, + content: { type: 'string', description: 'Page or blog post content' }, parentId: { type: 'string', description: 'Parent page identifier' }, query: { type: 'string', description: 'Search query' }, comment: { type: 'string', description: 'Comment text' }, @@ -696,6 +1049,62 @@ export const ConfluenceV2Block: BlockConfig = { attachmentFileName: { type: 'string', description: 'Custom file name for attachment' }, attachmentComment: { type: 'string', description: 'Comment for the attachment' }, labelName: { type: 'string', description: 'Label name' }, + labelPrefix: { type: 'string', description: 'Label prefix (global, my, team, system)' }, + blogPostStatus: { type: 'string', description: 'Blog post status (current or draft)' }, + purge: { type: 'boolean', description: 'Permanently delete instead of moving to trash' }, + bodyFormat: { type: 'string', description: 'Body format for comments' }, limit: { type: 'number', description: 'Maximum number of results' }, + cursor: { type: 'string', description: 'Pagination cursor from previous response' }, + }, + outputs: { + ts: { type: 'string', description: 'Timestamp' }, + pageId: { type: 'string', description: 'Page identifier' }, + content: { type: 'string', description: 'Page content' }, + body: { type: 'json', description: 'Page body with storage format' }, + title: { type: 'string', description: 'Page title' }, + url: { type: 'string', description: 'Page or resource URL' }, + success: { type: 'boolean', description: 'Operation success status' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + added: { type: 'boolean', description: 'Addition status' }, + removed: { type: 'boolean', description: 'Removal status' }, + updated: { type: 'boolean', description: 'Update status' }, + // Search & List Results + results: { type: 'array', description: 'Search results' }, + pages: { type: 'array', description: 'List of pages' }, + children: { type: 'array', description: 'List of child pages' }, + ancestors: { type: 'array', description: 'List of ancestor pages' }, + // Comment Results + comments: { type: 'array', description: 'List of comments' }, + commentId: { type: 'string', description: 'Comment identifier' }, + // Attachment Results + attachments: { type: 'array', description: 'List of attachments' }, + attachmentId: { type: 'string', description: 'Attachment identifier' }, + fileSize: { type: 'number', description: 'Attachment file size in bytes' }, + mediaType: { type: 'string', description: 'Attachment MIME type' }, + downloadUrl: { type: 'string', description: 'Attachment download URL' }, + // Label Results + labels: { type: 'array', description: 'List of labels' }, + labelName: { type: 'string', description: 'Label name' }, + // Space Results + spaces: { type: 'array', description: 'List of spaces' }, + spaceId: { type: 'string', description: 'Space identifier' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space or content type' }, + status: { type: 'string', description: 'Space status' }, + // Blog Post Results + blogPosts: { type: 'array', description: 'List of blog posts' }, + blogPostId: { type: 'string', description: 'Blog post identifier' }, + // Version Results + versions: { type: 'array', description: 'List of page versions' }, + version: { type: 'json', description: 'Version information' }, + versionNumber: { type: 'number', description: 'Version number' }, + // Property Results + properties: { type: 'array', description: 'List of page properties' }, + propertyId: { type: 'string', description: 'Property identifier' }, + propertyKey: { type: 'string', description: 'Property key' }, + propertyValue: { type: 'json', description: 'Property value' }, + // Pagination + nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' }, }, } diff --git a/apps/sim/components/branded-layout.tsx b/apps/sim/components/branded-layout.tsx index 84e4fd3eb..a42e712f3 100644 --- a/apps/sim/components/branded-layout.tsx +++ b/apps/sim/components/branded-layout.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect } from 'react' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface BrandedLayoutProps { children: React.ReactNode diff --git a/apps/sim/components/emails/auth/otp-verification-email.tsx b/apps/sim/components/emails/auth/otp-verification-email.tsx index d6ec6dc63..21dc1159a 100644 --- a/apps/sim/components/emails/auth/otp-verification-email.tsx +++ b/apps/sim/components/emails/auth/otp-verification-email.tsx @@ -1,7 +1,7 @@ import { Section, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface OTPVerificationEmailProps { otp: string diff --git a/apps/sim/components/emails/auth/reset-password-email.tsx b/apps/sim/components/emails/auth/reset-password-email.tsx index fa5e031b2..e86effe0f 100644 --- a/apps/sim/components/emails/auth/reset-password-email.tsx +++ b/apps/sim/components/emails/auth/reset-password-email.tsx @@ -1,7 +1,7 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface ResetPasswordEmailProps { username?: string diff --git a/apps/sim/components/emails/auth/welcome-email.tsx b/apps/sim/components/emails/auth/welcome-email.tsx index ba3e16b9a..3333e9bb7 100644 --- a/apps/sim/components/emails/auth/welcome-email.tsx +++ b/apps/sim/components/emails/auth/welcome-email.tsx @@ -1,8 +1,8 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface WelcomeEmailProps { userName?: string diff --git a/apps/sim/components/emails/billing/credit-purchase-email.tsx b/apps/sim/components/emails/billing/credit-purchase-email.tsx index 581f9dbc3..532add56c 100644 --- a/apps/sim/components/emails/billing/credit-purchase-email.tsx +++ b/apps/sim/components/emails/billing/credit-purchase-email.tsx @@ -1,8 +1,8 @@ import { Link, Section, Text } from '@react-email/components' import { baseStyles, colors } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface CreditPurchaseEmailProps { userName?: string diff --git a/apps/sim/components/emails/billing/enterprise-subscription-email.tsx b/apps/sim/components/emails/billing/enterprise-subscription-email.tsx index d3f237349..32f524126 100644 --- a/apps/sim/components/emails/billing/enterprise-subscription-email.tsx +++ b/apps/sim/components/emails/billing/enterprise-subscription-email.tsx @@ -1,8 +1,8 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface EnterpriseSubscriptionEmailProps { userName?: string diff --git a/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx b/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx index 9f42559d2..57c288bc8 100644 --- a/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx +++ b/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx @@ -1,7 +1,7 @@ import { Link, Section, Text } from '@react-email/components' import { baseStyles, colors, typography } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface FreeTierUpgradeEmailProps { userName?: string diff --git a/apps/sim/components/emails/billing/payment-failed-email.tsx b/apps/sim/components/emails/billing/payment-failed-email.tsx index 58d747410..d8aa23d14 100644 --- a/apps/sim/components/emails/billing/payment-failed-email.tsx +++ b/apps/sim/components/emails/billing/payment-failed-email.tsx @@ -1,7 +1,7 @@ import { Link, Section, Text } from '@react-email/components' import { baseStyles, colors } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface PaymentFailedEmailProps { userName?: string diff --git a/apps/sim/components/emails/billing/plan-welcome-email.tsx b/apps/sim/components/emails/billing/plan-welcome-email.tsx index 295f4a01c..13c0b7588 100644 --- a/apps/sim/components/emails/billing/plan-welcome-email.tsx +++ b/apps/sim/components/emails/billing/plan-welcome-email.tsx @@ -1,8 +1,8 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface PlanWelcomeEmailProps { planName: 'Pro' | 'Team' diff --git a/apps/sim/components/emails/billing/usage-threshold-email.tsx b/apps/sim/components/emails/billing/usage-threshold-email.tsx index be31ec0a6..1dec47536 100644 --- a/apps/sim/components/emails/billing/usage-threshold-email.tsx +++ b/apps/sim/components/emails/billing/usage-threshold-email.tsx @@ -1,7 +1,7 @@ import { Link, Section, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface UsageThresholdEmailProps { userName?: string diff --git a/apps/sim/components/emails/careers/careers-confirmation-email.tsx b/apps/sim/components/emails/careers/careers-confirmation-email.tsx index 1cdda08ef..42d5df3f1 100644 --- a/apps/sim/components/emails/careers/careers-confirmation-email.tsx +++ b/apps/sim/components/emails/careers/careers-confirmation-email.tsx @@ -2,8 +2,8 @@ import { Text } from '@react-email/components' import { format } from 'date-fns' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface CareersConfirmationEmailProps { name: string diff --git a/apps/sim/components/emails/components/email-footer.tsx b/apps/sim/components/emails/components/email-footer.tsx index a892d70a2..20ce2143d 100644 --- a/apps/sim/components/emails/components/email-footer.tsx +++ b/apps/sim/components/emails/components/email-footer.tsx @@ -1,8 +1,8 @@ import { Container, Img, Link, Section } from '@react-email/components' import { baseStyles, colors, spacing, typography } from '@/components/emails/_styles' -import { getBrandConfig } from '@/lib/branding/branding' import { isHosted } from '@/lib/core/config/feature-flags' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface EmailFooterProps { baseUrl?: string diff --git a/apps/sim/components/emails/components/email-layout.tsx b/apps/sim/components/emails/components/email-layout.tsx index f55249576..deb1eba9b 100644 --- a/apps/sim/components/emails/components/email-layout.tsx +++ b/apps/sim/components/emails/components/email-layout.tsx @@ -1,8 +1,8 @@ import { Body, Container, Head, Html, Img, Preview, Section } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailFooter } from '@/components/emails/components/email-footer' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface EmailLayoutProps { /** Preview text shown in email client list view */ diff --git a/apps/sim/components/emails/invitations/batch-invitation-email.tsx b/apps/sim/components/emails/invitations/batch-invitation-email.tsx index 53651044e..430a9fa18 100644 --- a/apps/sim/components/emails/invitations/batch-invitation-email.tsx +++ b/apps/sim/components/emails/invitations/batch-invitation-email.tsx @@ -1,7 +1,7 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface WorkspaceInvitation { workspaceId: string diff --git a/apps/sim/components/emails/invitations/invitation-email.tsx b/apps/sim/components/emails/invitations/invitation-email.tsx index 285901a32..f3ce41dcf 100644 --- a/apps/sim/components/emails/invitations/invitation-email.tsx +++ b/apps/sim/components/emails/invitations/invitation-email.tsx @@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components' import { createLogger } from '@sim/logger' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' interface InvitationEmailProps { inviterName?: string diff --git a/apps/sim/components/emails/invitations/polling-group-invitation-email.tsx b/apps/sim/components/emails/invitations/polling-group-invitation-email.tsx index b0f8e239b..f918dcc5e 100644 --- a/apps/sim/components/emails/invitations/polling-group-invitation-email.tsx +++ b/apps/sim/components/emails/invitations/polling-group-invitation-email.tsx @@ -1,7 +1,7 @@ import { Link, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' interface PollingGroupInvitationEmailProps { inviterName?: string diff --git a/apps/sim/components/emails/invitations/workspace-invitation-email.tsx b/apps/sim/components/emails/invitations/workspace-invitation-email.tsx index fb64cdfe3..f71e0fbb8 100644 --- a/apps/sim/components/emails/invitations/workspace-invitation-email.tsx +++ b/apps/sim/components/emails/invitations/workspace-invitation-email.tsx @@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components' import { createLogger } from '@sim/logger' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling' const logger = createLogger('WorkspaceInvitationEmail') diff --git a/apps/sim/components/emails/notifications/workflow-notification-email.tsx b/apps/sim/components/emails/notifications/workflow-notification-email.tsx index 860688c66..fd49f710f 100644 --- a/apps/sim/components/emails/notifications/workflow-notification-email.tsx +++ b/apps/sim/components/emails/notifications/workflow-notification-email.tsx @@ -1,7 +1,7 @@ import { Link, Section, Text } from '@react-email/components' import { baseStyles } from '@/components/emails/_styles' import { EmailLayout } from '@/components/emails/components' -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' /** * Serialized rate limit status for email payloads. diff --git a/apps/sim/components/emails/subjects.ts b/apps/sim/components/emails/subjects.ts index bf8b9197b..fb7984509 100644 --- a/apps/sim/components/emails/subjects.ts +++ b/apps/sim/components/emails/subjects.ts @@ -1,4 +1,4 @@ -import { getBrandConfig } from '@/lib/branding/branding' +import { getBrandConfig } from '@/ee/whitelabeling' /** Email subject type for all supported email templates */ export type EmailSubjectType = diff --git a/apps/sim/ee/LICENSE b/apps/sim/ee/LICENSE index ba5405dbf..3a492f83f 100644 --- a/apps/sim/ee/LICENSE +++ b/apps/sim/ee/LICENSE @@ -27,7 +27,7 @@ under the following terms: 3. ENTERPRISE SUBSCRIPTION Production deployment of enterprise features requires an active Sim Enterprise - subscription. Contact sales@simstudio.ai for licensing information. + subscription. Contact sales@sim.ai for licensing information. 4. DISCLAIMER @@ -40,4 +40,4 @@ under the following terms: IN NO EVENT SHALL SIM STUDIO, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. -For questions about enterprise licensing, contact: sales@simstudio.ai +For questions about enterprise licensing, contact: sales@sim.ai diff --git a/apps/sim/ee/README.md b/apps/sim/ee/README.md index d9e91afaf..f0377161e 100644 --- a/apps/sim/ee/README.md +++ b/apps/sim/ee/README.md @@ -7,7 +7,7 @@ for production use. - **SSO (Single Sign-On)**: OIDC and SAML authentication integration - **Access Control**: Permission groups for fine-grained user access management -- **Credential Sets**: Shared credential pools for email polling workflows +- **Whitelabeling**: Custom branding and theming for enterprise deployments ## Licensing diff --git a/apps/sim/ee/whitelabeling/branding.ts b/apps/sim/ee/whitelabeling/branding.ts new file mode 100644 index 000000000..49af8592f --- /dev/null +++ b/apps/sim/ee/whitelabeling/branding.ts @@ -0,0 +1,45 @@ +import { type BrandConfig, defaultBrandConfig, type ThemeColors } from '@/lib/branding' +import { getEnv } from '@/lib/core/config/env' + +export type { BrandConfig, ThemeColors } + +const getThemeColors = (): ThemeColors => { + return { + primaryColor: + getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultBrandConfig.theme?.primaryColor, + primaryHoverColor: + getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') || + defaultBrandConfig.theme?.primaryHoverColor, + accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultBrandConfig.theme?.accentColor, + accentHoverColor: + getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultBrandConfig.theme?.accentHoverColor, + backgroundColor: + getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultBrandConfig.theme?.backgroundColor, + } +} + +/** + * Get branding configuration from environment variables + * Supports runtime configuration via Docker/Kubernetes + */ +export const getBrandConfig = (): BrandConfig => { + return { + name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultBrandConfig.name, + logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultBrandConfig.logoUrl, + faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultBrandConfig.faviconUrl, + customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultBrandConfig.customCssUrl, + supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultBrandConfig.supportEmail, + documentationUrl: + getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultBrandConfig.documentationUrl, + termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultBrandConfig.termsUrl, + privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultBrandConfig.privacyUrl, + theme: getThemeColors(), + } +} + +/** + * Hook to use brand configuration in React components + */ +export const useBrandConfig = () => { + return getBrandConfig() +} diff --git a/apps/sim/ee/whitelabeling/index.ts b/apps/sim/ee/whitelabeling/index.ts new file mode 100644 index 000000000..1fe5b9487 --- /dev/null +++ b/apps/sim/ee/whitelabeling/index.ts @@ -0,0 +1,4 @@ +export type { BrandConfig, ThemeColors } from './branding' +export { getBrandConfig, useBrandConfig } from './branding' +export { generateThemeCSS } from './inject-theme' +export { generateBrandedMetadata, generateStructuredData } from './metadata' diff --git a/apps/sim/lib/branding/inject-theme.ts b/apps/sim/ee/whitelabeling/inject-theme.ts similarity index 96% rename from apps/sim/lib/branding/inject-theme.ts rename to apps/sim/ee/whitelabeling/inject-theme.ts index ecb34b1b7..4b44cd25d 100644 --- a/apps/sim/lib/branding/inject-theme.ts +++ b/apps/sim/ee/whitelabeling/inject-theme.ts @@ -1,4 +1,6 @@ -// Helper to detect if background is dark +/** + * Helper to detect if background is dark + */ function isDarkBackground(hexColor: string): boolean { const hex = hexColor.replace('#', '') const r = Number.parseInt(hex.substr(0, 2), 16) diff --git a/apps/sim/lib/branding/metadata.ts b/apps/sim/ee/whitelabeling/metadata.ts similarity index 98% rename from apps/sim/lib/branding/metadata.ts rename to apps/sim/ee/whitelabeling/metadata.ts index ae06860d8..2b3a8fd15 100644 --- a/apps/sim/lib/branding/metadata.ts +++ b/apps/sim/ee/whitelabeling/metadata.ts @@ -1,6 +1,6 @@ import type { Metadata } from 'next' -import { getBrandConfig } from '@/lib/branding/branding' import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBrandConfig } from '@/ee/whitelabeling/branding' /** * Generate dynamic metadata based on brand configuration diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index d5ac1a8c2..be5b961f0 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -151,7 +151,8 @@ export const auth = betterAuth({ create: { before: async (account) => { // Only one credential per (userId, providerId) is allowed - // If user reconnects (even with a different external account), replace the existing one + // If user reconnects (even with a different external account), delete the old one + // and let Better Auth create the new one (returning false breaks account linking flow) const existing = await db.query.account.findFirst({ where: and( eq(schema.account.userId, account.userId), @@ -159,101 +160,59 @@ export const auth = betterAuth({ ), }) - if (existing) { - let scopeToStore = account.scope + const modifiedAccount = { ...account } - if (account.providerId === 'salesforce' && account.accessToken) { - try { - const response = await fetch( - 'https://login.salesforce.com/services/oauth2/userinfo', - { - headers: { - Authorization: `Bearer ${account.accessToken}`, - }, - } - ) - - if (response.ok) { - const data = await response.json() - - if (data.profile) { - const match = data.profile.match(/^(https:\/\/[^/]+)/) - if (match && match[1] !== 'https://login.salesforce.com') { - const instanceUrl = match[1] - scopeToStore = `__sf_instance__:${instanceUrl} ${account.scope}` - } - } + if (account.providerId === 'salesforce' && account.accessToken) { + try { + const response = await fetch( + 'https://login.salesforce.com/services/oauth2/userinfo', + { + headers: { + Authorization: `Bearer ${account.accessToken}`, + }, } - } catch (error) { - logger.error('Failed to fetch Salesforce instance URL', { error }) - } - } - - const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId) - ? getMicrosoftRefreshTokenExpiry() - : account.refreshTokenExpiresAt - - await db - .update(schema.account) - .set({ - accountId: account.accountId, - accessToken: account.accessToken, - refreshToken: account.refreshToken, - idToken: account.idToken, - accessTokenExpiresAt: account.accessTokenExpiresAt, - refreshTokenExpiresAt, - scope: scopeToStore, - updatedAt: new Date(), - }) - .where(eq(schema.account.id, existing.id)) - - // Sync webhooks for credential sets after reconnecting - const requestId = crypto.randomUUID().slice(0, 8) - const userMemberships = await db - .select({ - credentialSetId: schema.credentialSetMember.credentialSetId, - providerId: schema.credentialSet.providerId, - }) - .from(schema.credentialSetMember) - .innerJoin( - schema.credentialSet, - eq(schema.credentialSetMember.credentialSetId, schema.credentialSet.id) - ) - .where( - and( - eq(schema.credentialSetMember.userId, account.userId), - eq(schema.credentialSetMember.status, 'active') - ) ) - for (const membership of userMemberships) { - if (membership.providerId === account.providerId) { - try { - await syncAllWebhooksForCredentialSet(membership.credentialSetId, requestId) - logger.info( - '[account.create.before] Synced webhooks after credential reconnect', - { - credentialSetId: membership.credentialSetId, - providerId: account.providerId, - } - ) - } catch (error) { - logger.error( - '[account.create.before] Failed to sync webhooks after credential reconnect', - { - credentialSetId: membership.credentialSetId, - providerId: account.providerId, - error, - } - ) + if (response.ok) { + const data = await response.json() + + if (data.profile) { + const match = data.profile.match(/^(https:\/\/[^/]+)/) + if (match && match[1] !== 'https://login.salesforce.com') { + const instanceUrl = match[1] + modifiedAccount.scope = `__sf_instance__:${instanceUrl} ${account.scope}` + } } } + } catch (error) { + logger.error('Failed to fetch Salesforce instance URL', { error }) } - - return false } - return { data: account } + // Handle Microsoft refresh token expiry + if (isMicrosoftProvider(account.providerId)) { + modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry() + } + + if (existing) { + // Delete the existing account so Better Auth can create the new one + // This allows account linking/re-authorization to succeed + await db.delete(schema.account).where(eq(schema.account.id, existing.id)) + + // Preserve the existing account ID so references (like workspace notifications) continue to work + modifiedAccount.id = existing.id + + logger.info('[account.create.before] Deleted existing account for re-authorization', { + userId: account.userId, + providerId: account.providerId, + existingAccountId: existing.id, + preservingId: true, + }) + + // Sync webhooks for credential sets after reconnecting (in after hook) + } + + return { data: modifiedAccount } }, after: async (account) => { try { @@ -1687,6 +1646,12 @@ export const auth = betterAuth({ 'search:confluence', 'read:me', 'offline_access', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'read:content.property:confluence', + 'write:content.property:confluence', + 'read:hierarchical-content:confluence', + 'read:content.metadata:confluence', ], responseType: 'code', pkce: true, diff --git a/apps/sim/lib/branding/branding.ts b/apps/sim/lib/branding/branding.ts deleted file mode 100644 index e6964c05c..000000000 --- a/apps/sim/lib/branding/branding.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getEnv } from '@/lib/core/config/env' - -export interface ThemeColors { - primaryColor?: string - primaryHoverColor?: string - accentColor?: string - accentHoverColor?: string - backgroundColor?: string -} - -export interface BrandConfig { - name: string - logoUrl?: string - faviconUrl?: string - customCssUrl?: string - supportEmail?: string - documentationUrl?: string - termsUrl?: string - privacyUrl?: string - theme?: ThemeColors -} - -/** - * Default brand configuration values - */ -const defaultConfig: BrandConfig = { - name: 'Sim', - logoUrl: undefined, - faviconUrl: '/favicon/favicon.ico', - customCssUrl: undefined, - supportEmail: 'help@sim.ai', - documentationUrl: undefined, - termsUrl: undefined, - privacyUrl: undefined, - theme: { - primaryColor: '#701ffc', - primaryHoverColor: '#802fff', - accentColor: '#9d54ff', - accentHoverColor: '#a66fff', - backgroundColor: '#0c0c0c', - }, -} - -const getThemeColors = (): ThemeColors => { - return { - primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.theme?.primaryColor, - primaryHoverColor: - getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') || defaultConfig.theme?.primaryHoverColor, - accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.theme?.accentColor, - accentHoverColor: - getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultConfig.theme?.accentHoverColor, - backgroundColor: - getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultConfig.theme?.backgroundColor, - } -} - -/** - * Get branding configuration from environment variables - * Supports runtime configuration via Docker/Kubernetes - */ -export const getBrandConfig = (): BrandConfig => { - return { - name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultConfig.name, - logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultConfig.logoUrl, - faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultConfig.faviconUrl, - customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultConfig.customCssUrl, - supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultConfig.supportEmail, - documentationUrl: getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultConfig.documentationUrl, - termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultConfig.termsUrl, - privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultConfig.privacyUrl, - theme: getThemeColors(), - } -} - -/** - * Hook to use brand configuration in React components - */ -export const useBrandConfig = () => { - return getBrandConfig() -} diff --git a/apps/sim/lib/branding/defaults.ts b/apps/sim/lib/branding/defaults.ts new file mode 100644 index 000000000..8ce6d1491 --- /dev/null +++ b/apps/sim/lib/branding/defaults.ts @@ -0,0 +1,22 @@ +import type { BrandConfig } from './types' + +/** + * Default brand configuration values + */ +export const defaultBrandConfig: BrandConfig = { + name: 'Sim', + logoUrl: undefined, + faviconUrl: '/favicon/favicon.ico', + customCssUrl: undefined, + supportEmail: 'help@sim.ai', + documentationUrl: undefined, + termsUrl: undefined, + privacyUrl: undefined, + theme: { + primaryColor: '#701ffc', + primaryHoverColor: '#802fff', + accentColor: '#9d54ff', + accentHoverColor: '#a66fff', + backgroundColor: '#0c0c0c', + }, +} diff --git a/apps/sim/lib/branding/index.ts b/apps/sim/lib/branding/index.ts new file mode 100644 index 000000000..495fcde7c --- /dev/null +++ b/apps/sim/lib/branding/index.ts @@ -0,0 +1,2 @@ +export { defaultBrandConfig } from './defaults' +export type { BrandConfig, ThemeColors } from './types' diff --git a/apps/sim/lib/branding/types.ts b/apps/sim/lib/branding/types.ts new file mode 100644 index 000000000..cd286de78 --- /dev/null +++ b/apps/sim/lib/branding/types.ts @@ -0,0 +1,19 @@ +export interface ThemeColors { + primaryColor?: string + primaryHoverColor?: string + accentColor?: string + accentHoverColor?: string + backgroundColor?: string +} + +export interface BrandConfig { + name: string + logoUrl?: string + faviconUrl?: string + customCssUrl?: string + supportEmail?: string + documentationUrl?: string + termsUrl?: string + privacyUrl?: string + theme?: ThemeColors +} diff --git a/apps/sim/tools/confluence/add_label.ts b/apps/sim/tools/confluence/add_label.ts new file mode 100644 index 000000000..db931b3cf --- /dev/null +++ b/apps/sim/tools/confluence/add_label.ts @@ -0,0 +1,123 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceAddLabelParams { + accessToken: string + domain: string + pageId: string + labelName: string + prefix?: string + cloudId?: string +} + +export interface ConfluenceAddLabelResponse { + success: boolean + output: { + ts: string + pageId: string + labelName: string + labelId: string + } +} + +export const confluenceAddLabelTool: ToolConfig< + ConfluenceAddLabelParams, + ConfluenceAddLabelResponse +> = { + id: 'confluence_add_label', + name: 'Confluence Add Label', + description: 'Add a label to a Confluence page for organization and categorization.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to add the label to', + }, + labelName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the label to add', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Label prefix: global (default), my, team, or system', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/labels', + method: 'POST', + headers: (params: ConfluenceAddLabelParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceAddLabelParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + labelName: params.labelName?.trim(), + prefix: params.prefix || 'global', + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + labelName: data.labelName ?? data.name ?? '', + labelId: data.id ?? '', + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { + type: 'string', + description: 'Page ID that the label was added to', + }, + labelName: { + type: 'string', + description: 'Name of the added label', + }, + labelId: { + type: 'string', + description: 'ID of the added label', + }, + }, +} diff --git a/apps/sim/tools/confluence/create_blogpost.ts b/apps/sim/tools/confluence/create_blogpost.ts new file mode 100644 index 000000000..b39e91b7f --- /dev/null +++ b/apps/sim/tools/confluence/create_blogpost.ts @@ -0,0 +1,151 @@ +import { + CONTENT_BODY_OUTPUT_PROPERTIES, + TIMESTAMP_OUTPUT, + VERSION_OUTPUT_PROPERTIES, +} from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateBlogPostParams { + accessToken: string + domain: string + spaceId: string + title: string + content: string + status?: string + cloudId?: string +} + +export interface ConfluenceCreateBlogPostResponse { + success: boolean + output: { + ts: string + id: string + title: string + status: string | null + spaceId: string + authorId: string | null + body: Record | null + version: Record | null + webUrl: string | null + } +} + +export const confluenceCreateBlogPostTool: ToolConfig< + ConfluenceCreateBlogPostParams, + ConfluenceCreateBlogPostResponse +> = { + id: 'confluence_create_blogpost', + name: 'Confluence Create Blog Post', + description: 'Create a new blog post in a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the space to create the blog post in', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Title of the blog post', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Blog post content in Confluence storage format (HTML)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Blog post status: current (default) or draft', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'POST', + headers: (params: ConfluenceCreateBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreateBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + spaceId: params.spaceId?.trim(), + title: params.title, + content: params.content, + status: params.status || 'current', + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + id: data.id ?? '', + title: data.title ?? '', + status: data.status ?? null, + spaceId: data.spaceId ?? '', + authorId: data.authorId ?? null, + body: data.body ?? null, + version: data.version ?? null, + webUrl: data.webUrl ?? data._links?.webui ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Created blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID' }, + authorId: { type: 'string', description: 'Author account ID', optional: true }, + body: { + type: 'object', + description: 'Blog post body content', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, + }, + version: { + type: 'object', + description: 'Blog post version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + webUrl: { type: 'string', description: 'URL to view the blog post', optional: true }, + }, +} diff --git a/apps/sim/tools/confluence/create_page.ts b/apps/sim/tools/confluence/create_page.ts index 69b83dc1e..7a4fec8a8 100644 --- a/apps/sim/tools/confluence/create_page.ts +++ b/apps/sim/tools/confluence/create_page.ts @@ -1,3 +1,4 @@ +import { CONTENT_BODY_OUTPUT_PROPERTIES, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' import type { ToolConfig } from '@/tools/types' export interface ConfluenceCreatePageParams { @@ -16,6 +17,11 @@ export interface ConfluenceCreatePageResponse { ts: string pageId: string title: string + status: string | null + spaceId: string | null + parentId: string | null + body: Record | null + version: Record | null url: string } } @@ -109,8 +115,13 @@ export const confluenceCreatePageTool: ToolConfig< success: true, output: { ts: new Date().toISOString(), - pageId: data.id, - title: data.title, + pageId: data.id ?? '', + title: data.title ?? '', + status: data.status ?? null, + spaceId: data.spaceId ?? null, + parentId: data.parentId ?? null, + body: data.body ?? null, + version: data.version ?? null, url: data.url || data._links?.webui || '', }, } @@ -120,6 +131,21 @@ export const confluenceCreatePageTool: ToolConfig< ts: { type: 'string', description: 'Timestamp of creation' }, pageId: { type: 'string', description: 'Created page ID' }, title: { type: 'string', description: 'Page title' }, + status: { type: 'string', description: 'Page status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + parentId: { type: 'string', description: 'Parent page ID', optional: true }, + body: { + type: 'object', + description: 'Page body content', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, + }, + version: { + type: 'object', + description: 'Page version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, url: { type: 'string', description: 'Page URL' }, }, } diff --git a/apps/sim/tools/confluence/create_page_property.ts b/apps/sim/tools/confluence/create_page_property.ts new file mode 100644 index 000000000..36ebfb04a --- /dev/null +++ b/apps/sim/tools/confluence/create_page_property.ts @@ -0,0 +1,127 @@ +import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreatePagePropertyParams { + accessToken: string + domain: string + pageId: string + key: string + value: any + cloudId?: string +} + +export interface ConfluenceCreatePagePropertyResponse { + success: boolean + output: { + ts: string + pageId: string + propertyId: string + key: string + value: any + version: { + number: number + } | null + } +} + +export const confluenceCreatePagePropertyTool: ToolConfig< + ConfluenceCreatePagePropertyParams, + ConfluenceCreatePagePropertyResponse +> = { + id: 'confluence_create_page_property', + name: 'Confluence Create Page Property', + description: 'Create a new custom property (metadata) on a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to add the property to', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The key/name for the property', + }, + value: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'The value for the property (can be any JSON value)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-properties', + method: 'POST', + headers: (params: ConfluenceCreatePagePropertyParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreatePagePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + key: params.key, + value: params.value, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + propertyId: data.id ?? '', + key: data.key ?? '', + value: data.value ?? null, + version: data.version ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { type: 'string', description: 'ID of the page' }, + propertyId: { type: 'string', description: 'ID of the created property' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value' }, + version: { + type: 'object', + description: 'Version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/delete_page.ts b/apps/sim/tools/confluence/delete_page.ts index a9b35c33f..a648a2b37 100644 --- a/apps/sim/tools/confluence/delete_page.ts +++ b/apps/sim/tools/confluence/delete_page.ts @@ -4,6 +4,7 @@ export interface ConfluenceDeletePageParams { accessToken: string domain: string pageId: string + purge?: boolean cloudId?: string } @@ -22,7 +23,8 @@ export const confluenceDeletePageTool: ToolConfig< > = { id: 'confluence_delete_page', name: 'Confluence Delete Page', - description: 'Delete a Confluence page (moves it to trash where it can be restored).', + description: + 'Delete a Confluence page. By default moves to trash; use purge=true to permanently delete.', version: '1.0.0', oauth: { @@ -49,6 +51,13 @@ export const confluenceDeletePageTool: ToolConfig< visibility: 'user-or-llm', description: 'Confluence page ID to delete', }, + purge: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'If true, permanently deletes the page instead of moving to trash (default: false)', + }, cloudId: { type: 'string', required: false, @@ -72,6 +81,7 @@ export const confluenceDeletePageTool: ToolConfig< domain: params.domain, accessToken: params.accessToken, pageId: params.pageId, + purge: params.purge || false, cloudId: params.cloudId, } }, diff --git a/apps/sim/tools/confluence/get_blogpost.ts b/apps/sim/tools/confluence/get_blogpost.ts new file mode 100644 index 000000000..94c9b02de --- /dev/null +++ b/apps/sim/tools/confluence/get_blogpost.ts @@ -0,0 +1,144 @@ +import { + CONTENT_BODY_OUTPUT_PROPERTIES, + TIMESTAMP_OUTPUT, + VERSION_OUTPUT_PROPERTIES, +} from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetBlogPostParams { + accessToken: string + domain: string + blogPostId: string + bodyFormat?: string + cloudId?: string +} + +export interface ConfluenceGetBlogPostResponse { + success: boolean + output: { + ts: string + id: string + title: string + status: string | null + spaceId: string | null + authorId: string | null + createdAt: string | null + version: { + number: number + message?: string + createdAt?: string + } | null + body: { + storage?: { value: string } + } | null + webUrl: string | null + } +} + +export const confluenceGetBlogPostTool: ToolConfig< + ConfluenceGetBlogPostParams, + ConfluenceGetBlogPostResponse +> = { + id: 'confluence_get_blogpost', + name: 'Confluence Get Blog Post', + description: 'Get a specific Confluence blog post by ID, including its content.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + blogPostId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the blog post to retrieve', + }, + bodyFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Format for blog post body: storage, atlas_doc_format, or view', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'POST', + headers: (params: ConfluenceGetBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + blogPostId: params.blogPostId?.trim(), + bodyFormat: params.bodyFormat || 'storage', + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + id: data.id ?? '', + title: data.title ?? '', + status: data.status ?? null, + spaceId: data.spaceId ?? null, + authorId: data.authorId ?? null, + createdAt: data.createdAt ?? null, + version: data.version ?? null, + body: data.body ?? null, + webUrl: data.webUrl ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + authorId: { type: 'string', description: 'Author account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + version: { + type: 'object', + description: 'Version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + body: { + type: 'object', + description: 'Blog post body content in requested format(s)', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, + }, + webUrl: { type: 'string', description: 'URL to view the blog post', optional: true }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_ancestors.ts b/apps/sim/tools/confluence/get_page_ancestors.ts new file mode 100644 index 000000000..20b7be3ca --- /dev/null +++ b/apps/sim/tools/confluence/get_page_ancestors.ts @@ -0,0 +1,126 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPageAncestorsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cloudId?: string +} + +export interface ConfluenceGetPageAncestorsResponse { + success: boolean + output: { + ts: string + pageId: string + ancestors: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + webUrl: string | null + }> + } +} + +export const confluenceGetPageAncestorsTool: ToolConfig< + ConfluenceGetPageAncestorsParams, + ConfluenceGetPageAncestorsResponse +> = { + id: 'confluence_get_page_ancestors', + name: 'Confluence Get Page Ancestors', + description: + 'Get the ancestor (parent) pages of a specific Confluence page. Returns the full hierarchy from the page up to the root.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to get ancestors for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of ancestors to return (default: 25, max: 250)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-ancestors', + method: 'POST', + headers: (params: ConfluenceGetPageAncestorsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetPageAncestorsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + limit: params.limit ? Number(params.limit) : 25, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + ancestors: data.ancestors ?? [], + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { + type: 'string', + description: 'ID of the page whose ancestors were retrieved', + }, + ancestors: { + type: 'array', + description: 'Array of ancestor pages, ordered from direct parent to root', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Ancestor page ID' }, + title: { type: 'string', description: 'Ancestor page title' }, + status: { type: 'string', description: 'Page status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + webUrl: { type: 'string', description: 'URL to view the page', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_children.ts b/apps/sim/tools/confluence/get_page_children.ts new file mode 100644 index 000000000..7ca7ca10e --- /dev/null +++ b/apps/sim/tools/confluence/get_page_children.ts @@ -0,0 +1,143 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPageChildrenParams { + accessToken: string + domain: string + pageId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceGetPageChildrenResponse { + success: boolean + output: { + ts: string + parentId: string + children: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + childPosition: number | null + webUrl: string | null + }> + nextCursor: string | null + } +} + +export const confluenceGetPageChildrenTool: ToolConfig< + ConfluenceGetPageChildrenParams, + ConfluenceGetPageChildrenResponse +> = { + id: 'confluence_get_page_children', + name: 'Confluence Get Page Children', + description: + 'Get all child pages of a specific Confluence page. Useful for navigating page hierarchies.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the parent page to get children from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of child pages to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response to get the next page of results', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-children', + method: 'POST', + headers: (params: ConfluenceGetPageChildrenParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetPageChildrenParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + limit: params.limit ? Number(params.limit) : 50, + cursor: params.cursor, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + parentId: data.parentId ?? '', + children: data.children ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + parentId: { + type: 'string', + description: 'ID of the parent page', + }, + children: { + type: 'array', + description: 'Array of child pages', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Child page ID' }, + title: { type: 'string', description: 'Child page title' }, + status: { type: 'string', description: 'Page status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + childPosition: { type: 'number', description: 'Position among siblings', optional: true }, + webUrl: { type: 'string', description: 'URL to view the page', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_version.ts b/apps/sim/tools/confluence/get_page_version.ts new file mode 100644 index 000000000..c162e2546 --- /dev/null +++ b/apps/sim/tools/confluence/get_page_version.ts @@ -0,0 +1,123 @@ +import { DETAILED_VERSION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPageVersionParams { + accessToken: string + domain: string + pageId: string + versionNumber: number + cloudId?: string +} + +export interface ConfluenceGetPageVersionResponse { + success: boolean + output: { + ts: string + pageId: string + version: { + number: number + message: string | null + minorEdit: boolean + authorId: string | null + createdAt: string | null + contentTypeModified: boolean | null + collaborators: string[] | null + prevVersion: number | null + nextVersion: number | null + } + } +} + +export const confluenceGetPageVersionTool: ToolConfig< + ConfluenceGetPageVersionParams, + ConfluenceGetPageVersionResponse +> = { + id: 'confluence_get_page_version', + name: 'Confluence Get Page Version', + description: 'Get details about a specific version of a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page', + }, + versionNumber: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The version number to retrieve (e.g., 1, 2, 3)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-versions', + method: 'POST', + headers: (params: ConfluenceGetPageVersionParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetPageVersionParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + versionNumber: Number(params.versionNumber), + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + version: data.version ?? { + number: 0, + message: null, + minorEdit: false, + authorId: null, + createdAt: null, + }, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { type: 'string', description: 'ID of the page' }, + version: { + type: 'object', + description: 'Detailed version information', + properties: DETAILED_VERSION_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/confluence/get_space.ts b/apps/sim/tools/confluence/get_space.ts index a6608f143..fbadd7a65 100644 --- a/apps/sim/tools/confluence/get_space.ts +++ b/apps/sim/tools/confluence/get_space.ts @@ -1,3 +1,4 @@ +import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' import type { ToolConfig } from '@/tools/types' export interface ConfluenceGetSpaceParams { @@ -17,6 +18,13 @@ export interface ConfluenceGetSpaceResponse { type: string status: string url: string + authorId: string | null + createdAt: string | null + homepageId: string | null + description: { + value: string + representation: string + } | null } } @@ -95,17 +103,34 @@ export const confluenceGetSpaceTool: ToolConfig< type: data.type, status: data.status, url: data._links?.webui || '', + authorId: data.authorId ?? null, + createdAt: data.createdAt ?? null, + homepageId: data.homepageId ?? null, + description: data.description ?? null, }, } }, outputs: { - ts: { type: 'string', description: 'Timestamp of retrieval' }, + ts: TIMESTAMP_OUTPUT, spaceId: { type: 'string', description: 'Space ID' }, name: { type: 'string', description: 'Space name' }, key: { type: 'string', description: 'Space key' }, - type: { type: 'string', description: 'Space type' }, - status: { type: 'string', description: 'Space status' }, - url: { type: 'string', description: 'Space URL' }, + type: { type: 'string', description: 'Space type (global, personal)' }, + status: { type: 'string', description: 'Space status (current, archived)' }, + url: { type: 'string', description: 'URL to view the space in Confluence' }, + authorId: { type: 'string', description: 'Account ID of the space creator', optional: true }, + createdAt: { + type: 'string', + description: 'ISO 8601 timestamp when the space was created', + optional: true, + }, + homepageId: { type: 'string', description: 'ID of the space homepage', optional: true }, + description: { + type: 'object', + description: 'Space description content', + properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES, + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index 33f5f2b96..d78645b15 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -1,24 +1,42 @@ +import { confluenceAddLabelTool } from '@/tools/confluence/add_label' +import { confluenceCreateBlogPostTool } from '@/tools/confluence/create_blogpost' import { confluenceCreateCommentTool } from '@/tools/confluence/create_comment' import { confluenceCreatePageTool } from '@/tools/confluence/create_page' +import { confluenceCreatePagePropertyTool } from '@/tools/confluence/create_page_property' import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment' import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment' import { confluenceDeletePageTool } from '@/tools/confluence/delete_page' +import { confluenceGetBlogPostTool } from '@/tools/confluence/get_blogpost' +import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ancestors' +import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children' +import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version' import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' +import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts' +import { confluenceListBlogPostsInSpaceTool } from '@/tools/confluence/list_blogposts_in_space' import { confluenceListCommentsTool } from '@/tools/confluence/list_comments' import { confluenceListLabelsTool } from '@/tools/confluence/list_labels' +import { confluenceListPagePropertiesTool } from '@/tools/confluence/list_page_properties' +import { confluenceListPageVersionsTool } from '@/tools/confluence/list_page_versions' +import { confluenceListPagesInSpaceTool } from '@/tools/confluence/list_pages_in_space' import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces' import { confluenceRetrieveTool } from '@/tools/confluence/retrieve' import { confluenceSearchTool } from '@/tools/confluence/search' +import { confluenceSearchInSpaceTool } from '@/tools/confluence/search_in_space' import { ATTACHMENT_ITEM_PROPERTIES, ATTACHMENT_OUTPUT, ATTACHMENTS_OUTPUT, + BODY_FORMAT_PROPERTIES, COMMENT_BODY_OUTPUT_PROPERTIES, COMMENT_ITEM_PROPERTIES, COMMENT_OUTPUT, COMMENTS_OUTPUT, + CONTENT_BODY_OUTPUT, + CONTENT_BODY_OUTPUT_PROPERTIES, DELETED_OUTPUT, + DETAILED_VERSION_OUTPUT, + DETAILED_VERSION_OUTPUT_PROPERTIES, LABEL_ITEM_PROPERTIES, LABEL_OUTPUT, LABELS_OUTPUT, @@ -46,20 +64,41 @@ import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment' import { confluenceUploadAttachmentTool } from '@/tools/confluence/upload_attachment' export { - // Tools + // Page Tools confluenceRetrieveTool, confluenceUpdateTool, confluenceCreatePageTool, confluenceDeletePageTool, + confluenceListPagesInSpaceTool, + confluenceGetPageChildrenTool, + confluenceGetPageAncestorsTool, + // Page Version Tools + confluenceListPageVersionsTool, + confluenceGetPageVersionTool, + // Page Properties Tools + confluenceListPagePropertiesTool, + confluenceCreatePagePropertyTool, + // Blog Post Tools + confluenceListBlogPostsTool, + confluenceGetBlogPostTool, + confluenceCreateBlogPostTool, + confluenceListBlogPostsInSpaceTool, + // Search Tools confluenceSearchTool, + confluenceSearchInSpaceTool, + // Comment Tools confluenceCreateCommentTool, confluenceListCommentsTool, confluenceUpdateCommentTool, confluenceDeleteCommentTool, + // Attachment Tools confluenceListAttachmentsTool, confluenceDeleteAttachmentTool, confluenceUploadAttachmentTool, + // Label Tools confluenceListLabelsTool, + confluenceAddLabelTool, + // Space Tools confluenceGetSpaceTool, confluenceListSpacesTool, // Item property constants (for use in outputs) @@ -70,7 +109,10 @@ export { SEARCH_RESULT_ITEM_PROPERTIES, SPACE_ITEM_PROPERTIES, VERSION_OUTPUT_PROPERTIES, + DETAILED_VERSION_OUTPUT_PROPERTIES, COMMENT_BODY_OUTPUT_PROPERTIES, + CONTENT_BODY_OUTPUT_PROPERTIES, + BODY_FORMAT_PROPERTIES, SPACE_DESCRIPTION_OUTPUT_PROPERTIES, SEARCH_RESULT_SPACE_PROPERTIES, PAGINATION_LINKS_PROPERTIES, @@ -79,6 +121,8 @@ export { ATTACHMENTS_OUTPUT, COMMENT_OUTPUT, COMMENTS_OUTPUT, + CONTENT_BODY_OUTPUT, + DETAILED_VERSION_OUTPUT, LABEL_OUTPUT, LABELS_OUTPUT, PAGE_OUTPUT, diff --git a/apps/sim/tools/confluence/list_attachments.ts b/apps/sim/tools/confluence/list_attachments.ts index 103869617..932aa9b68 100644 --- a/apps/sim/tools/confluence/list_attachments.ts +++ b/apps/sim/tools/confluence/list_attachments.ts @@ -6,6 +6,7 @@ export interface ConfluenceListAttachmentsParams { domain: string pageId: string limit?: number + cursor?: string cloudId?: string } @@ -20,6 +21,7 @@ export interface ConfluenceListAttachmentsResponse { mediaType: string downloadUrl: string }> + nextCursor: string | null } } @@ -60,7 +62,13 @@ export const confluenceListAttachmentsTool: ToolConfig< type: 'number', required: false, visibility: 'user-or-llm', - description: 'Maximum number of attachments to return (default: 25)', + description: 'Maximum number of attachments to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', }, cloudId: { type: 'string', @@ -77,8 +85,11 @@ export const confluenceListAttachmentsTool: ToolConfig< domain: params.domain, accessToken: params.accessToken, pageId: params.pageId, - limit: String(params.limit || 25), + limit: String(params.limit || 50), }) + if (params.cursor) { + query.set('cursor', params.cursor) + } if (params.cloudId) { query.set('cloudId', params.cloudId) } @@ -91,15 +102,6 @@ export const confluenceListAttachmentsTool: ToolConfig< Authorization: `Bearer ${params.accessToken}`, } }, - body: (params: ConfluenceListAttachmentsParams) => { - return { - domain: params.domain, - accessToken: params.accessToken, - cloudId: params.cloudId, - pageId: params.pageId, - limit: params.limit ? Number(params.limit) : 25, - } - }, }, transformResponse: async (response: Response) => { @@ -109,6 +111,7 @@ export const confluenceListAttachmentsTool: ToolConfig< output: { ts: new Date().toISOString(), attachments: data.attachments || [], + nextCursor: data.nextCursor ?? null, }, } }, @@ -116,5 +119,10 @@ export const confluenceListAttachmentsTool: ToolConfig< outputs: { ts: TIMESTAMP_OUTPUT, attachments: ATTACHMENTS_OUTPUT, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/list_blogposts.ts b/apps/sim/tools/confluence/list_blogposts.ts new file mode 100644 index 000000000..a6b78e2b5 --- /dev/null +++ b/apps/sim/tools/confluence/list_blogposts.ts @@ -0,0 +1,167 @@ +import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListBlogPostsParams { + accessToken: string + domain: string + limit?: number + status?: string + sort?: string + cursor?: string + cloudId?: string +} + +export interface ConfluenceListBlogPostsResponse { + success: boolean + output: { + ts: string + blogPosts: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + authorId: string | null + createdAt: string | null + version: { + number: number + message?: string + createdAt?: string + } | null + webUrl: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListBlogPostsTool: ToolConfig< + ConfluenceListBlogPostsParams, + ConfluenceListBlogPostsResponse +> = { + id: 'confluence_list_blogposts', + name: 'Confluence List Blog Posts', + description: 'List all blog posts across all accessible Confluence spaces.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of blog posts to return (default: 25, max: 250)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by status: current, archived, trashed, or draft', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Sort order: created-date, -created-date, modified-date, -modified-date, title, -title', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceListBlogPostsParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + limit: String(params.limit || 25), + }) + if (params.status) { + query.set('status', params.status) + } + if (params.sort) { + query.set('sort', params.sort) + } + if (params.cursor) { + query.set('cursor', params.cursor) + } + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/blogposts?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceListBlogPostsParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPosts: data.blogPosts ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPosts: { + type: 'array', + description: 'Array of blog posts', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + authorId: { type: 'string', description: 'Author account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + version: { + type: 'object', + description: 'Version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + webUrl: { type: 'string', description: 'URL to view the blog post', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_blogposts_in_space.ts b/apps/sim/tools/confluence/list_blogposts_in_space.ts new file mode 100644 index 000000000..d32fcd9f4 --- /dev/null +++ b/apps/sim/tools/confluence/list_blogposts_in_space.ts @@ -0,0 +1,178 @@ +import { + CONTENT_BODY_OUTPUT_PROPERTIES, + TIMESTAMP_OUTPUT, + VERSION_OUTPUT_PROPERTIES, +} from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListBlogPostsInSpaceParams { + accessToken: string + domain: string + spaceId: string + limit?: number + status?: string + bodyFormat?: string + cursor?: string + cloudId?: string +} + +export interface ConfluenceListBlogPostsInSpaceResponse { + success: boolean + output: { + ts: string + blogPosts: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + authorId: string | null + createdAt: string | null + version: { + number: number + message?: string + createdAt?: string + } | null + body: { + storage?: { value: string } + } | null + webUrl: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListBlogPostsInSpaceTool: ToolConfig< + ConfluenceListBlogPostsInSpaceParams, + ConfluenceListBlogPostsInSpaceResponse +> = { + id: 'confluence_list_blogposts_in_space', + name: 'Confluence List Blog Posts in Space', + description: 'List all blog posts within a specific Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Confluence space to list blog posts from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of blog posts to return (default: 25, max: 250)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by status: current, archived, trashed, or draft', + }, + bodyFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Format for blog post body: storage, atlas_doc_format, or view', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-blogposts', + method: 'POST', + headers: (params: ConfluenceListBlogPostsInSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListBlogPostsInSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + spaceId: params.spaceId?.trim(), + limit: params.limit ? Number(params.limit) : 25, + status: params.status, + bodyFormat: params.bodyFormat, + cursor: params.cursor, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPosts: data.blogPosts ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPosts: { + type: 'array', + description: 'Array of blog posts in the space', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + authorId: { type: 'string', description: 'Author account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + version: { + type: 'object', + description: 'Version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + body: { + type: 'object', + description: 'Blog post body content', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, + }, + webUrl: { type: 'string', description: 'URL to view the blog post', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_comments.ts b/apps/sim/tools/confluence/list_comments.ts index d4876b4b6..d841dd660 100644 --- a/apps/sim/tools/confluence/list_comments.ts +++ b/apps/sim/tools/confluence/list_comments.ts @@ -6,6 +6,8 @@ export interface ConfluenceListCommentsParams { domain: string pageId: string limit?: number + bodyFormat?: string + cursor?: string cloudId?: string } @@ -19,6 +21,7 @@ export interface ConfluenceListCommentsResponse { createdAt: string authorId: string }> + nextCursor: string | null } } @@ -61,6 +64,19 @@ export const confluenceListCommentsTool: ToolConfig< visibility: 'user-or-llm', description: 'Maximum number of comments to return (default: 25)', }, + bodyFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Format for the comment body: storage, atlas_doc_format, view, or export_view (default: storage)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, cloudId: { type: 'string', required: false, @@ -78,6 +94,12 @@ export const confluenceListCommentsTool: ToolConfig< pageId: params.pageId, limit: String(params.limit || 25), }) + if (params.bodyFormat) { + query.set('bodyFormat', params.bodyFormat) + } + if (params.cursor) { + query.set('cursor', params.cursor) + } if (params.cloudId) { query.set('cloudId', params.cloudId) } @@ -90,15 +112,6 @@ export const confluenceListCommentsTool: ToolConfig< Authorization: `Bearer ${params.accessToken}`, } }, - body: (params: ConfluenceListCommentsParams) => { - return { - domain: params.domain, - accessToken: params.accessToken, - cloudId: params.cloudId, - pageId: params.pageId, - limit: params.limit ? Number(params.limit) : 25, - } - }, }, transformResponse: async (response: Response) => { @@ -108,6 +121,7 @@ export const confluenceListCommentsTool: ToolConfig< output: { ts: new Date().toISOString(), comments: data.comments || [], + nextCursor: data.nextCursor ?? null, }, } }, @@ -115,5 +129,10 @@ export const confluenceListCommentsTool: ToolConfig< outputs: { ts: TIMESTAMP_OUTPUT, comments: COMMENTS_OUTPUT, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/list_labels.ts b/apps/sim/tools/confluence/list_labels.ts index 4adb838ad..c28bac10b 100644 --- a/apps/sim/tools/confluence/list_labels.ts +++ b/apps/sim/tools/confluence/list_labels.ts @@ -5,6 +5,8 @@ export interface ConfluenceListLabelsParams { accessToken: string domain: string pageId: string + limit?: number + cursor?: string cloudId?: string } @@ -17,6 +19,7 @@ export interface ConfluenceListLabelsResponse { name: string prefix: string }> + nextCursor: string | null } } @@ -53,6 +56,18 @@ export const confluenceListLabelsTool: ToolConfig< visibility: 'user-or-llm', description: 'Confluence page ID to list labels from', }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of labels to return (default: 25, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, cloudId: { type: 'string', required: false, @@ -68,7 +83,11 @@ export const confluenceListLabelsTool: ToolConfig< domain: params.domain, accessToken: params.accessToken, pageId: params.pageId, + limit: String(params.limit || 25), }) + if (params.cursor) { + query.set('cursor', params.cursor) + } if (params.cloudId) { query.set('cloudId', params.cloudId) } @@ -90,6 +109,7 @@ export const confluenceListLabelsTool: ToolConfig< output: { ts: new Date().toISOString(), labels: data.labels || [], + nextCursor: data.nextCursor ?? null, }, } }, @@ -104,5 +124,10 @@ export const confluenceListLabelsTool: ToolConfig< properties: LABEL_ITEM_PROPERTIES, }, }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/list_page_properties.ts b/apps/sim/tools/confluence/list_page_properties.ts new file mode 100644 index 000000000..cd26739c4 --- /dev/null +++ b/apps/sim/tools/confluence/list_page_properties.ts @@ -0,0 +1,149 @@ +import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListPagePropertiesParams { + accessToken: string + domain: string + pageId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListPagePropertiesResponse { + success: boolean + output: { + ts: string + pageId: string + properties: Array<{ + id: string + key: string + value: any + version: { + number: number + message?: string + createdAt?: string + } | null + }> + nextCursor: string | null + } +} + +export const confluenceListPagePropertiesTool: ToolConfig< + ConfluenceListPagePropertiesParams, + ConfluenceListPagePropertiesResponse +> = { + id: 'confluence_list_page_properties', + name: 'Confluence List Page Properties', + description: 'List all custom properties (metadata) attached to a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to list properties from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of properties to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceListPagePropertiesParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId, + limit: String(params.limit || 50), + }) + if (params.cursor) { + query.set('cursor', params.cursor) + } + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/page-properties?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceListPagePropertiesParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + properties: data.properties ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { type: 'string', description: 'ID of the page' }, + properties: { + type: 'array', + description: 'Array of content properties', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Property ID' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value (can be any JSON)' }, + version: { + type: 'object', + description: 'Version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_page_versions.ts b/apps/sim/tools/confluence/list_page_versions.ts new file mode 100644 index 000000000..8e97f9fde --- /dev/null +++ b/apps/sim/tools/confluence/list_page_versions.ts @@ -0,0 +1,131 @@ +import { TIMESTAMP_OUTPUT, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListPageVersionsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListPageVersionsResponse { + success: boolean + output: { + ts: string + pageId: string + versions: Array<{ + number: number + message: string | null + minorEdit: boolean + authorId: string | null + createdAt: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListPageVersionsTool: ToolConfig< + ConfluenceListPageVersionsParams, + ConfluenceListPageVersionsResponse +> = { + id: 'confluence_list_page_versions', + name: 'Confluence List Page Versions', + description: 'List all versions (revision history) of a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to get versions for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of versions to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-versions', + method: 'POST', + headers: (params: ConfluenceListPageVersionsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListPageVersionsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId?.trim(), + limit: params.limit ? Number(params.limit) : 50, + cursor: params.cursor, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId ?? '', + versions: data.versions ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pageId: { type: 'string', description: 'ID of the page' }, + versions: { + type: 'array', + description: 'Array of page versions', + items: { + type: 'object', + properties: VERSION_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_pages_in_space.ts b/apps/sim/tools/confluence/list_pages_in_space.ts new file mode 100644 index 000000000..558a8c50e --- /dev/null +++ b/apps/sim/tools/confluence/list_pages_in_space.ts @@ -0,0 +1,174 @@ +import { + CONTENT_BODY_OUTPUT_PROPERTIES, + PAGE_ITEM_PROPERTIES, + TIMESTAMP_OUTPUT, +} from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListPagesInSpaceParams { + accessToken: string + domain: string + spaceId: string + limit?: number + status?: string + bodyFormat?: string + cursor?: string + cloudId?: string +} + +export interface ConfluenceListPagesInSpaceResponse { + success: boolean + output: { + ts: string + pages: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + parentId: string | null + authorId: string | null + createdAt: string | null + version: { + number: number + message?: string + createdAt?: string + } | null + body: { + storage?: { value: string } + } | null + webUrl: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListPagesInSpaceTool: ToolConfig< + ConfluenceListPagesInSpaceParams, + ConfluenceListPagesInSpaceResponse +> = { + id: 'confluence_list_pages_in_space', + name: 'Confluence List Pages in Space', + description: + 'List all pages within a specific Confluence space. Supports pagination and filtering by status.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Confluence space to list pages from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of pages to return (default: 50, max: 250)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter pages by status: current, archived, trashed, or draft', + }, + bodyFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Format for page body content: storage, atlas_doc_format, or view. If not specified, body is not included.', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response to get the next page of results', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-pages', + method: 'POST', + headers: (params: ConfluenceListPagesInSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListPagesInSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + spaceId: params.spaceId?.trim(), + limit: params.limit ? Number(params.limit) : 50, + status: params.status, + bodyFormat: params.bodyFormat, + cursor: params.cursor, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pages: data.pages ?? [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + pages: { + type: 'array', + description: 'Array of pages in the space', + items: { + type: 'object', + properties: { + ...PAGE_ITEM_PROPERTIES, + body: { + type: 'object', + description: 'Page body content (if bodyFormat was specified)', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, + }, + webUrl: { + type: 'string', + description: 'URL to view the page in Confluence', + optional: true, + }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_spaces.ts b/apps/sim/tools/confluence/list_spaces.ts index 77fcbfe0e..3859aad2b 100644 --- a/apps/sim/tools/confluence/list_spaces.ts +++ b/apps/sim/tools/confluence/list_spaces.ts @@ -5,6 +5,7 @@ export interface ConfluenceListSpacesParams { accessToken: string domain: string limit?: number + cursor?: string cloudId?: string } @@ -19,6 +20,7 @@ export interface ConfluenceListSpacesResponse { type: string status: string }> + nextCursor: string | null } } @@ -53,7 +55,13 @@ export const confluenceListSpacesTool: ToolConfig< type: 'number', required: false, visibility: 'user-or-llm', - description: 'Maximum number of spaces to return (default: 25)', + description: 'Maximum number of spaces to return (default: 25, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', }, cloudId: { type: 'string', @@ -71,6 +79,9 @@ export const confluenceListSpacesTool: ToolConfig< accessToken: params.accessToken, limit: String(params.limit || 25), }) + if (params.cursor) { + query.set('cursor', params.cursor) + } if (params.cloudId) { query.set('cloudId', params.cloudId) } @@ -83,14 +94,6 @@ export const confluenceListSpacesTool: ToolConfig< Authorization: `Bearer ${params.accessToken}`, } }, - body: (params: ConfluenceListSpacesParams) => { - return { - domain: params.domain, - accessToken: params.accessToken, - cloudId: params.cloudId, - limit: params.limit ? Number(params.limit) : 25, - } - }, }, transformResponse: async (response: Response) => { @@ -100,6 +103,7 @@ export const confluenceListSpacesTool: ToolConfig< output: { ts: new Date().toISOString(), spaces: data.spaces || [], + nextCursor: data.nextCursor ?? null, }, } }, @@ -107,5 +111,10 @@ export const confluenceListSpacesTool: ToolConfig< outputs: { ts: TIMESTAMP_OUTPUT, spaces: SPACES_OUTPUT, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/retrieve.ts b/apps/sim/tools/confluence/retrieve.ts index 9af6af7e0..ded0fda90 100644 --- a/apps/sim/tools/confluence/retrieve.ts +++ b/apps/sim/tools/confluence/retrieve.ts @@ -1,4 +1,9 @@ import type { ConfluenceRetrieveParams, ConfluenceRetrieveResponse } from '@/tools/confluence/types' +import { + BODY_FORMAT_PROPERTIES, + TIMESTAMP_OUTPUT, + VERSION_OUTPUT_PROPERTIES, +} from '@/tools/confluence/types' import { transformPageData } from '@/tools/confluence/utils' import type { ToolConfig } from '@/tools/types' @@ -71,9 +76,42 @@ export const confluenceRetrieveTool: ToolConfig< }, outputs: { - ts: { type: 'string', description: 'Timestamp of retrieval' }, + ts: TIMESTAMP_OUTPUT, pageId: { type: 'string', description: 'Confluence page ID' }, - content: { type: 'string', description: 'Page content with HTML tags stripped' }, title: { type: 'string', description: 'Page title' }, + content: { type: 'string', description: 'Page content with HTML tags stripped' }, + status: { + type: 'string', + description: 'Page status (current, archived, trashed, draft)', + optional: true, + }, + spaceId: { type: 'string', description: 'ID of the space containing the page', optional: true }, + parentId: { type: 'string', description: 'ID of the parent page', optional: true }, + authorId: { type: 'string', description: 'Account ID of the page author', optional: true }, + createdAt: { + type: 'string', + description: 'ISO 8601 timestamp when the page was created', + optional: true, + }, + url: { type: 'string', description: 'URL to view the page in Confluence', optional: true }, + body: { + type: 'object', + description: 'Raw page body content in storage format', + properties: { + storage: { + type: 'object', + description: 'Body in storage format (Confluence markup)', + properties: BODY_FORMAT_PROPERTIES, + optional: true, + }, + }, + optional: true, + }, + version: { + type: 'object', + description: 'Page version information', + properties: VERSION_OUTPUT_PROPERTIES, + optional: true, + }, }, } diff --git a/apps/sim/tools/confluence/search_in_space.ts b/apps/sim/tools/confluence/search_in_space.ts new file mode 100644 index 000000000..5b10a5c62 --- /dev/null +++ b/apps/sim/tools/confluence/search_in_space.ts @@ -0,0 +1,144 @@ +import { SEARCH_RESULT_ITEM_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceSearchInSpaceParams { + accessToken: string + domain: string + spaceKey: string + query?: string + contentType?: string + limit?: number + cloudId?: string +} + +export interface ConfluenceSearchInSpaceResponse { + success: boolean + output: { + ts: string + spaceKey: string + totalSize: number + results: Array<{ + id: string + title: string + type: string + status: string | null + url: string + excerpt: string + lastModified: string | null + }> + } +} + +export const confluenceSearchInSpaceTool: ToolConfig< + ConfluenceSearchInSpaceParams, + ConfluenceSearchInSpaceResponse +> = { + id: 'confluence_search_in_space', + name: 'Confluence Search in Space', + description: + 'Search for content within a specific Confluence space. Optionally filter by text query and content type.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The key of the Confluence space to search in (e.g., "ENG", "HR")', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text search query. If not provided, returns all content in the space.', + }, + contentType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by content type: page, blogpost, attachment, or comment', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (default: 25, max: 250)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/search-in-space', + method: 'POST', + headers: (params: ConfluenceSearchInSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceSearchInSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + spaceKey: params.spaceKey?.trim(), + query: params.query, + contentType: params.contentType, + limit: params.limit ? Number(params.limit) : 25, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceKey: data.spaceKey ?? '', + totalSize: data.totalSize ?? 0, + results: data.results ?? [], + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceKey: { + type: 'string', + description: 'The space key that was searched', + }, + totalSize: { + type: 'number', + description: 'Total number of matching results', + }, + results: { + type: 'array', + description: 'Array of search results', + items: { + type: 'object', + properties: SEARCH_RESULT_ITEM_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/confluence/types.ts b/apps/sim/tools/confluence/types.ts index 5ac68d69c..a786e9924 100644 --- a/apps/sim/tools/confluence/types.ts +++ b/apps/sim/tools/confluence/types.ts @@ -26,6 +26,52 @@ export const VERSION_OUTPUT_PROPERTIES = { }, } as const satisfies Record +/** + * Detailed version object properties for get_page_version endpoint. + * Based on Confluence API v2 DetailedVersion schema. + */ +export const DETAILED_VERSION_OUTPUT_PROPERTIES = { + number: { type: 'number', description: 'Version number' }, + message: { type: 'string', description: 'Version message', optional: true }, + minorEdit: { type: 'boolean', description: 'Whether this is a minor edit' }, + authorId: { type: 'string', description: 'Account ID of the version author', optional: true }, + createdAt: { + type: 'string', + description: 'ISO 8601 timestamp of version creation', + optional: true, + }, + contentTypeModified: { + type: 'boolean', + description: 'Whether the content type was modified in this version', + optional: true, + }, + collaborators: { + type: 'array', + description: 'List of collaborator account IDs for this version', + items: { type: 'string' }, + optional: true, + }, + prevVersion: { + type: 'number', + description: 'Previous version number', + optional: true, + }, + nextVersion: { + type: 'number', + description: 'Next version number', + optional: true, + }, +} as const satisfies Record + +/** + * Complete detailed version object output definition. + */ +export const DETAILED_VERSION_OUTPUT: OutputProperty = { + type: 'object', + description: 'Detailed version information', + properties: DETAILED_VERSION_OUTPUT_PROPERTIES, +} + /** * Complete version object output definition. */ @@ -137,6 +183,54 @@ export const SPACES_OUTPUT: OutputProperty = { }, } +/** + * Body format inner object properties (storage, view, atlas_doc_format). + * Based on Confluence API v2 body structure. + */ +export const BODY_FORMAT_PROPERTIES = { + value: { type: 'string', description: 'The content value in the specified format' }, + representation: { + type: 'string', + description: 'Content representation type', + optional: true, + }, +} as const satisfies Record + +/** + * Page/Blog post body object properties. + * Based on Confluence API v2 body structure with multiple format options. + */ +export const CONTENT_BODY_OUTPUT_PROPERTIES = { + storage: { + type: 'object', + description: 'Body in storage format (Confluence markup)', + properties: BODY_FORMAT_PROPERTIES, + optional: true, + }, + view: { + type: 'object', + description: 'Body in view format (rendered HTML)', + properties: BODY_FORMAT_PROPERTIES, + optional: true, + }, + atlas_doc_format: { + type: 'object', + description: 'Body in Atlassian Document Format (ADF)', + properties: BODY_FORMAT_PROPERTIES, + optional: true, + }, +} as const satisfies Record + +/** + * Complete body object output definition for pages and blog posts. + */ +export const CONTENT_BODY_OUTPUT: OutputProperty = { + type: 'object', + description: 'Page or blog post body content in requested format(s)', + properties: CONTENT_BODY_OUTPUT_PROPERTIES, + optional: true, +} + /** * Comment body object properties. * Based on Confluence API v2 comment body structure. diff --git a/apps/sim/tools/confluence/update.ts b/apps/sim/tools/confluence/update.ts index b5c5847f0..f9dcd0107 100644 --- a/apps/sim/tools/confluence/update.ts +++ b/apps/sim/tools/confluence/update.ts @@ -1,4 +1,5 @@ import type { ConfluenceUpdateParams, ConfluenceUpdateResponse } from '@/tools/confluence/types' +import { CONTENT_BODY_OUTPUT_PROPERTIES, VERSION_OUTPUT_PROPERTIES } from '@/tools/confluence/types' import type { ToolConfig } from '@/tools/types' export const confluenceUpdateTool: ToolConfig = { @@ -98,9 +99,13 @@ export const confluenceUpdateTool: ToolConfig = { confluence_update: confluenceUpdateTool, confluence_create_page: confluenceCreatePageTool, confluence_delete_page: confluenceDeletePageTool, + confluence_list_pages_in_space: confluenceListPagesInSpaceTool, + confluence_get_page_children: confluenceGetPageChildrenTool, + confluence_get_page_ancestors: confluenceGetPageAncestorsTool, + confluence_list_page_versions: confluenceListPageVersionsTool, + confluence_get_page_version: confluenceGetPageVersionTool, + confluence_list_page_properties: confluenceListPagePropertiesTool, + confluence_create_page_property: confluenceCreatePagePropertyTool, + confluence_list_blogposts: confluenceListBlogPostsTool, + confluence_get_blogpost: confluenceGetBlogPostTool, + confluence_create_blogpost: confluenceCreateBlogPostTool, + confluence_list_blogposts_in_space: confluenceListBlogPostsInSpaceTool, confluence_search: confluenceSearchTool, + confluence_search_in_space: confluenceSearchInSpaceTool, confluence_create_comment: confluenceCreateCommentTool, confluence_list_comments: confluenceListCommentsTool, confluence_update_comment: confluenceUpdateCommentTool, @@ -2617,6 +2642,7 @@ export const tools: Record = { confluence_upload_attachment: confluenceUploadAttachmentTool, confluence_delete_attachment: confluenceDeleteAttachmentTool, confluence_list_labels: confluenceListLabelsTool, + confluence_add_label: confluenceAddLabelTool, confluence_get_space: confluenceGetSpaceTool, confluence_list_spaces: confluenceListSpacesTool, cursor_list_agents: cursorListAgentsTool,