From 1cf58a9c418db94a6dfffa58190a4d4f50ca18da Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 11:29:36 -0800 Subject: [PATCH 01/11] feat(tools): added zoom, elasticsearch, dropbox, box, datadog, ahrefs, gitlab, shopify, ssh, wordpress --- apps/docs/components/icons.tsx | 268 ++++- apps/docs/components/ui/icon-mapping.ts | 22 + apps/docs/content/docs/en/tools/ahrefs.mdx | 197 ++++ apps/docs/content/docs/en/tools/box.mdx | 276 +++++ apps/docs/content/docs/en/tools/datadog.mdx | 310 ++++++ apps/docs/content/docs/en/tools/dropbox.mdx | 220 ++++ .../content/docs/en/tools/elasticsearch.mdx | 370 +++++++ apps/docs/content/docs/en/tools/gitlab.mdx | 434 ++++++++ apps/docs/content/docs/en/tools/grafana.mdx | 613 +++++++++++ apps/docs/content/docs/en/tools/meta.json | 13 +- apps/docs/content/docs/en/tools/shopify.mdx | 352 +++++++ apps/docs/content/docs/en/tools/ssh.mdx | 399 +++++++ apps/docs/content/docs/en/tools/wordpress.mdx | 629 +++++++++++ apps/docs/content/docs/en/tools/zoom.mdx | 178 ++++ .../tools/ssh/check-command-exists/route.ts | 105 ++ .../api/tools/ssh/check-file-exists/route.ts | 134 +++ .../api/tools/ssh/create-directory/route.ts | 101 ++ .../app/api/tools/ssh/delete-file/route.ts | 94 ++ .../app/api/tools/ssh/download-file/route.ts | 127 +++ .../api/tools/ssh/execute-command/route.ts | 84 ++ .../app/api/tools/ssh/execute-script/route.ts | 103 ++ .../api/tools/ssh/get-system-info/route.ts | 125 +++ .../app/api/tools/ssh/list-directory/route.ts | 132 +++ .../app/api/tools/ssh/move-rename/route.ts | 111 ++ .../api/tools/ssh/read-file-content/route.ts | 135 +++ .../app/api/tools/ssh/upload-file/route.ts | 137 +++ apps/sim/app/api/tools/ssh/utils.ts | 155 +++ .../api/tools/ssh/write-file-content/route.ts | 152 +++ apps/sim/blocks/blocks/ahrefs.ts | 408 ++++++++ apps/sim/blocks/blocks/box.ts | 513 +++++++++ apps/sim/blocks/blocks/datadog.ts | 560 ++++++++++ apps/sim/blocks/blocks/dropbox.ts | 368 +++++++ apps/sim/blocks/blocks/elasticsearch.ts | 431 ++++++++ apps/sim/blocks/blocks/gitlab.ts | 698 +++++++++++++ apps/sim/blocks/blocks/grafana.ts | 508 +++++++++ apps/sim/blocks/blocks/shopify.ts | 674 ++++++++++++ apps/sim/blocks/blocks/ssh.ts | 518 +++++++++ apps/sim/blocks/blocks/wordpress.ts | 979 ++++++++++++++++++ apps/sim/blocks/blocks/zoom.ts | 453 ++++++++ apps/sim/blocks/registry.ts | 22 + apps/sim/components/icons.tsx | 268 ++++- apps/sim/lib/auth/auth.ts | 60 ++ apps/sim/lib/core/config/env.ts | 8 + apps/sim/lib/oauth/oauth.ts | 175 ++++ apps/sim/package.json | 2 + apps/sim/tools/ahrefs/backlinks.ts | 107 ++ apps/sim/tools/ahrefs/backlinks_stats.ts | 86 ++ apps/sim/tools/ahrefs/broken_backlinks.ts | 112 ++ apps/sim/tools/ahrefs/domain_rating.ts | 62 ++ apps/sim/tools/ahrefs/index.ts | 17 + apps/sim/tools/ahrefs/keyword_overview.ts | 101 ++ apps/sim/tools/ahrefs/organic_keywords.ts | 118 +++ apps/sim/tools/ahrefs/referring_domains.ts | 114 ++ apps/sim/tools/ahrefs/top_pages.ts | 110 ++ apps/sim/tools/ahrefs/types.ts | 198 ++++ apps/sim/tools/box/copy_file.ts | 92 ++ apps/sim/tools/box/create_collaboration.ts | 138 +++ apps/sim/tools/box/create_folder.ts | 81 ++ apps/sim/tools/box/create_shared_link.ts | 142 +++ apps/sim/tools/box/delete_file.ts | 82 ++ apps/sim/tools/box/delete_folder.ts | 76 ++ apps/sim/tools/box/download_file.ts | 120 +++ apps/sim/tools/box/get_current_user.ts | 75 ++ apps/sim/tools/box/get_file.ts | 84 ++ apps/sim/tools/box/index.ts | 16 + apps/sim/tools/box/list_folder.ts | 112 ++ apps/sim/tools/box/search.ts | 145 +++ apps/sim/tools/box/types.ts | 619 +++++++++++ apps/sim/tools/box/update_file.ts | 111 ++ apps/sim/tools/datadog/cancel_downtime.ts | 76 ++ apps/sim/tools/datadog/create_downtime.ts | 178 ++++ apps/sim/tools/datadog/create_event.ts | 169 +++ apps/sim/tools/datadog/create_monitor.ts | 169 +++ apps/sim/tools/datadog/get_monitor.ts | 120 +++ apps/sim/tools/datadog/index.ts | 25 + apps/sim/tools/datadog/list_downtimes.ts | 116 +++ apps/sim/tools/datadog/list_monitors.ts | 150 +++ apps/sim/tools/datadog/mute_monitor.ts | 94 ++ apps/sim/tools/datadog/query_logs.ts | 169 +++ apps/sim/tools/datadog/query_timeseries.ts | 110 ++ apps/sim/tools/datadog/send_logs.ts | 100 ++ apps/sim/tools/datadog/submit_metrics.ts | 107 ++ apps/sim/tools/datadog/types.ts | 778 ++++++++++++++ apps/sim/tools/dropbox/copy.ts | 87 ++ apps/sim/tools/dropbox/create_folder.ts | 82 ++ apps/sim/tools/dropbox/create_shared_link.ts | 131 +++ apps/sim/tools/dropbox/delete.ts | 76 ++ apps/sim/tools/dropbox/download.ts | 82 ++ apps/sim/tools/dropbox/get_metadata.ts | 95 ++ apps/sim/tools/dropbox/index.ts | 10 + apps/sim/tools/dropbox/list_folder.ts | 115 ++ apps/sim/tools/dropbox/move.ts | 87 ++ apps/sim/tools/dropbox/search.ts | 135 +++ apps/sim/tools/dropbox/types.ts | 244 +++++ apps/sim/tools/dropbox/upload.ts | 119 +++ apps/sim/tools/elasticsearch/bulk.ts | 181 ++++ .../sim/tools/elasticsearch/cluster_health.ts | 212 ++++ apps/sim/tools/elasticsearch/cluster_stats.ts | 177 ++++ apps/sim/tools/elasticsearch/count.ts | 164 +++ apps/sim/tools/elasticsearch/create_index.ts | 185 ++++ .../tools/elasticsearch/delete_document.ts | 186 ++++ apps/sim/tools/elasticsearch/delete_index.ts | 144 +++ apps/sim/tools/elasticsearch/get_document.ts | 203 ++++ apps/sim/tools/elasticsearch/get_index.ts | 140 +++ apps/sim/tools/elasticsearch/index.ts | 27 + .../sim/tools/elasticsearch/index_document.ts | 188 ++++ apps/sim/tools/elasticsearch/search.ts | 254 +++++ apps/sim/tools/elasticsearch/types.ts | 278 +++++ .../tools/elasticsearch/update_document.ts | 187 ++++ apps/sim/tools/gitlab/cancel_pipeline.ts | 73 ++ apps/sim/tools/gitlab/create_issue.ts | 116 +++ apps/sim/tools/gitlab/create_issue_note.ts | 82 ++ apps/sim/tools/gitlab/create_merge_request.ts | 141 +++ .../tools/gitlab/create_merge_request_note.ts | 85 ++ apps/sim/tools/gitlab/create_pipeline.ts | 91 ++ apps/sim/tools/gitlab/delete_issue.ts | 69 ++ apps/sim/tools/gitlab/get_issue.ts | 70 ++ apps/sim/tools/gitlab/get_merge_request.ts | 76 ++ apps/sim/tools/gitlab/get_pipeline.ts | 71 ++ apps/sim/tools/gitlab/get_project.ts | 65 ++ apps/sim/tools/gitlab/index.ts | 45 + apps/sim/tools/gitlab/list_issues.ts | 129 +++ apps/sim/tools/gitlab/list_merge_requests.ts | 129 +++ apps/sim/tools/gitlab/list_pipelines.ts | 115 ++ apps/sim/tools/gitlab/list_projects.ts | 119 +++ apps/sim/tools/gitlab/merge_merge_request.ts | 115 ++ apps/sim/tools/gitlab/retry_pipeline.ts | 73 ++ apps/sim/tools/gitlab/types.ts | 651 ++++++++++++ apps/sim/tools/gitlab/update_issue.ts | 126 +++ apps/sim/tools/gitlab/update_merge_request.ts | 144 +++ apps/sim/tools/grafana/alerts.ts | 768 ++++++++++++++ apps/sim/tools/grafana/annotations.ts | 491 +++++++++ apps/sim/tools/grafana/dashboards.ts | 663 ++++++++++++ apps/sim/tools/grafana/data_sources.ts | 220 ++++ apps/sim/tools/grafana/folders.ts | 225 ++++ apps/sim/tools/grafana/health.ts | 155 +++ apps/sim/tools/grafana/index.ts | 57 + apps/sim/tools/grafana/types.ts | 451 ++++++++ apps/sim/tools/index.ts | 16 +- apps/sim/tools/registry.ts | 332 ++++++ apps/sim/tools/shopify/adjust_inventory.ts | 159 +++ apps/sim/tools/shopify/cancel_order.ts | 146 +++ apps/sim/tools/shopify/create_customer.ts | 209 ++++ apps/sim/tools/shopify/create_product.ts | 208 ++++ apps/sim/tools/shopify/delete_customer.ts | 113 ++ apps/sim/tools/shopify/delete_product.ts | 113 ++ apps/sim/tools/shopify/get_customer.ts | 133 +++ apps/sim/tools/shopify/get_inventory_level.ts | 128 +++ apps/sim/tools/shopify/get_order.ts | 202 ++++ apps/sim/tools/shopify/get_product.ts | 130 +++ apps/sim/tools/shopify/index.ts | 21 + apps/sim/tools/shopify/list_customers.ts | 140 +++ apps/sim/tools/shopify/list_orders.ts | 169 +++ apps/sim/tools/shopify/list_products.ts | 150 +++ apps/sim/tools/shopify/types.ts | 377 +++++++ apps/sim/tools/shopify/update_customer.ts | 200 ++++ apps/sim/tools/shopify/update_order.ts | 164 +++ apps/sim/tools/shopify/update_product.ts | 203 ++++ apps/sim/tools/ssh/check_command_exists.ts | 96 ++ apps/sim/tools/ssh/check_file_exists.ts | 107 ++ apps/sim/tools/ssh/create_directory.ts | 110 ++ apps/sim/tools/ssh/delete_file.ts | 108 ++ apps/sim/tools/ssh/download_file.ts | 100 ++ apps/sim/tools/ssh/execute_command.ts | 105 ++ apps/sim/tools/ssh/execute_script.ts | 114 ++ apps/sim/tools/ssh/get_system_info.ts | 101 ++ apps/sim/tools/ssh/index.ts | 14 + apps/sim/tools/ssh/list_directory.ts | 123 +++ apps/sim/tools/ssh/move_rename.ts | 110 ++ apps/sim/tools/ssh/read_file_content.ts | 112 ++ apps/sim/tools/ssh/types.ts | 229 ++++ apps/sim/tools/ssh/upload_file.ts | 124 +++ apps/sim/tools/ssh/write_file_content.ts | 117 +++ apps/sim/tools/types.ts | 2 +- apps/sim/tools/wordpress/create_category.ts | 127 +++ apps/sim/tools/wordpress/create_comment.ts | 147 +++ apps/sim/tools/wordpress/create_page.ts | 164 +++ apps/sim/tools/wordpress/create_post.ts | 176 ++++ apps/sim/tools/wordpress/create_tag.ts | 115 ++ apps/sim/tools/wordpress/delete_comment.ts | 116 +++ apps/sim/tools/wordpress/delete_media.ts | 116 +++ apps/sim/tools/wordpress/delete_page.ts | 119 +++ apps/sim/tools/wordpress/delete_post.ts | 119 +++ apps/sim/tools/wordpress/get_current_user.ts | 100 ++ apps/sim/tools/wordpress/get_media.ts | 103 ++ apps/sim/tools/wordpress/get_page.ts | 107 ++ apps/sim/tools/wordpress/get_post.ts | 107 ++ apps/sim/tools/wordpress/get_user.ts | 103 ++ apps/sim/tools/wordpress/index.ts | 69 ++ apps/sim/tools/wordpress/list_categories.ts | 160 +++ apps/sim/tools/wordpress/list_comments.ts | 166 +++ apps/sim/tools/wordpress/list_media.ts | 165 +++ apps/sim/tools/wordpress/list_pages.ts | 169 +++ apps/sim/tools/wordpress/list_posts.ts | 197 ++++ apps/sim/tools/wordpress/list_tags.ts | 148 +++ apps/sim/tools/wordpress/list_users.ts | 158 +++ apps/sim/tools/wordpress/search_content.ts | 139 +++ apps/sim/tools/wordpress/types.ts | 630 +++++++++++ apps/sim/tools/wordpress/update_comment.ts | 124 +++ apps/sim/tools/wordpress/update_page.ts | 169 +++ apps/sim/tools/wordpress/update_post.ts | 181 ++++ apps/sim/tools/wordpress/upload_media.ts | 160 +++ apps/sim/tools/zoom/create_meeting.ts | 235 +++++ apps/sim/tools/zoom/delete_meeting.ts | 98 ++ apps/sim/tools/zoom/get_meeting.ts | 131 +++ apps/sim/tools/zoom/get_meeting_invitation.ts | 71 ++ apps/sim/tools/zoom/index.ts | 26 + apps/sim/tools/zoom/list_meetings.ts | 137 +++ apps/sim/tools/zoom/types.ts | 186 ++++ apps/sim/tools/zoom/update_meeting.ts | 195 ++++ bun.lock | 20 + 211 files changed, 37706 insertions(+), 36 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/ahrefs.mdx create mode 100644 apps/docs/content/docs/en/tools/box.mdx create mode 100644 apps/docs/content/docs/en/tools/datadog.mdx create mode 100644 apps/docs/content/docs/en/tools/dropbox.mdx create mode 100644 apps/docs/content/docs/en/tools/elasticsearch.mdx create mode 100644 apps/docs/content/docs/en/tools/gitlab.mdx create mode 100644 apps/docs/content/docs/en/tools/grafana.mdx create mode 100644 apps/docs/content/docs/en/tools/shopify.mdx create mode 100644 apps/docs/content/docs/en/tools/ssh.mdx create mode 100644 apps/docs/content/docs/en/tools/wordpress.mdx create mode 100644 apps/docs/content/docs/en/tools/zoom.mdx create mode 100644 apps/sim/app/api/tools/ssh/check-command-exists/route.ts create mode 100644 apps/sim/app/api/tools/ssh/check-file-exists/route.ts create mode 100644 apps/sim/app/api/tools/ssh/create-directory/route.ts create mode 100644 apps/sim/app/api/tools/ssh/delete-file/route.ts create mode 100644 apps/sim/app/api/tools/ssh/download-file/route.ts create mode 100644 apps/sim/app/api/tools/ssh/execute-command/route.ts create mode 100644 apps/sim/app/api/tools/ssh/execute-script/route.ts create mode 100644 apps/sim/app/api/tools/ssh/get-system-info/route.ts create mode 100644 apps/sim/app/api/tools/ssh/list-directory/route.ts create mode 100644 apps/sim/app/api/tools/ssh/move-rename/route.ts create mode 100644 apps/sim/app/api/tools/ssh/read-file-content/route.ts create mode 100644 apps/sim/app/api/tools/ssh/upload-file/route.ts create mode 100644 apps/sim/app/api/tools/ssh/utils.ts create mode 100644 apps/sim/app/api/tools/ssh/write-file-content/route.ts create mode 100644 apps/sim/blocks/blocks/ahrefs.ts create mode 100644 apps/sim/blocks/blocks/box.ts create mode 100644 apps/sim/blocks/blocks/datadog.ts create mode 100644 apps/sim/blocks/blocks/dropbox.ts create mode 100644 apps/sim/blocks/blocks/elasticsearch.ts create mode 100644 apps/sim/blocks/blocks/gitlab.ts create mode 100644 apps/sim/blocks/blocks/grafana.ts create mode 100644 apps/sim/blocks/blocks/shopify.ts create mode 100644 apps/sim/blocks/blocks/ssh.ts create mode 100644 apps/sim/blocks/blocks/wordpress.ts create mode 100644 apps/sim/blocks/blocks/zoom.ts create mode 100644 apps/sim/tools/ahrefs/backlinks.ts create mode 100644 apps/sim/tools/ahrefs/backlinks_stats.ts create mode 100644 apps/sim/tools/ahrefs/broken_backlinks.ts create mode 100644 apps/sim/tools/ahrefs/domain_rating.ts create mode 100644 apps/sim/tools/ahrefs/index.ts create mode 100644 apps/sim/tools/ahrefs/keyword_overview.ts create mode 100644 apps/sim/tools/ahrefs/organic_keywords.ts create mode 100644 apps/sim/tools/ahrefs/referring_domains.ts create mode 100644 apps/sim/tools/ahrefs/top_pages.ts create mode 100644 apps/sim/tools/ahrefs/types.ts create mode 100644 apps/sim/tools/box/copy_file.ts create mode 100644 apps/sim/tools/box/create_collaboration.ts create mode 100644 apps/sim/tools/box/create_folder.ts create mode 100644 apps/sim/tools/box/create_shared_link.ts create mode 100644 apps/sim/tools/box/delete_file.ts create mode 100644 apps/sim/tools/box/delete_folder.ts create mode 100644 apps/sim/tools/box/download_file.ts create mode 100644 apps/sim/tools/box/get_current_user.ts create mode 100644 apps/sim/tools/box/get_file.ts create mode 100644 apps/sim/tools/box/index.ts create mode 100644 apps/sim/tools/box/list_folder.ts create mode 100644 apps/sim/tools/box/search.ts create mode 100644 apps/sim/tools/box/types.ts create mode 100644 apps/sim/tools/box/update_file.ts create mode 100644 apps/sim/tools/datadog/cancel_downtime.ts create mode 100644 apps/sim/tools/datadog/create_downtime.ts create mode 100644 apps/sim/tools/datadog/create_event.ts create mode 100644 apps/sim/tools/datadog/create_monitor.ts create mode 100644 apps/sim/tools/datadog/get_monitor.ts create mode 100644 apps/sim/tools/datadog/index.ts create mode 100644 apps/sim/tools/datadog/list_downtimes.ts create mode 100644 apps/sim/tools/datadog/list_monitors.ts create mode 100644 apps/sim/tools/datadog/mute_monitor.ts create mode 100644 apps/sim/tools/datadog/query_logs.ts create mode 100644 apps/sim/tools/datadog/query_timeseries.ts create mode 100644 apps/sim/tools/datadog/send_logs.ts create mode 100644 apps/sim/tools/datadog/submit_metrics.ts create mode 100644 apps/sim/tools/datadog/types.ts create mode 100644 apps/sim/tools/dropbox/copy.ts create mode 100644 apps/sim/tools/dropbox/create_folder.ts create mode 100644 apps/sim/tools/dropbox/create_shared_link.ts create mode 100644 apps/sim/tools/dropbox/delete.ts create mode 100644 apps/sim/tools/dropbox/download.ts create mode 100644 apps/sim/tools/dropbox/get_metadata.ts create mode 100644 apps/sim/tools/dropbox/index.ts create mode 100644 apps/sim/tools/dropbox/list_folder.ts create mode 100644 apps/sim/tools/dropbox/move.ts create mode 100644 apps/sim/tools/dropbox/search.ts create mode 100644 apps/sim/tools/dropbox/types.ts create mode 100644 apps/sim/tools/dropbox/upload.ts create mode 100644 apps/sim/tools/elasticsearch/bulk.ts create mode 100644 apps/sim/tools/elasticsearch/cluster_health.ts create mode 100644 apps/sim/tools/elasticsearch/cluster_stats.ts create mode 100644 apps/sim/tools/elasticsearch/count.ts create mode 100644 apps/sim/tools/elasticsearch/create_index.ts create mode 100644 apps/sim/tools/elasticsearch/delete_document.ts create mode 100644 apps/sim/tools/elasticsearch/delete_index.ts create mode 100644 apps/sim/tools/elasticsearch/get_document.ts create mode 100644 apps/sim/tools/elasticsearch/get_index.ts create mode 100644 apps/sim/tools/elasticsearch/index.ts create mode 100644 apps/sim/tools/elasticsearch/index_document.ts create mode 100644 apps/sim/tools/elasticsearch/search.ts create mode 100644 apps/sim/tools/elasticsearch/types.ts create mode 100644 apps/sim/tools/elasticsearch/update_document.ts create mode 100644 apps/sim/tools/gitlab/cancel_pipeline.ts create mode 100644 apps/sim/tools/gitlab/create_issue.ts create mode 100644 apps/sim/tools/gitlab/create_issue_note.ts create mode 100644 apps/sim/tools/gitlab/create_merge_request.ts create mode 100644 apps/sim/tools/gitlab/create_merge_request_note.ts create mode 100644 apps/sim/tools/gitlab/create_pipeline.ts create mode 100644 apps/sim/tools/gitlab/delete_issue.ts create mode 100644 apps/sim/tools/gitlab/get_issue.ts create mode 100644 apps/sim/tools/gitlab/get_merge_request.ts create mode 100644 apps/sim/tools/gitlab/get_pipeline.ts create mode 100644 apps/sim/tools/gitlab/get_project.ts create mode 100644 apps/sim/tools/gitlab/index.ts create mode 100644 apps/sim/tools/gitlab/list_issues.ts create mode 100644 apps/sim/tools/gitlab/list_merge_requests.ts create mode 100644 apps/sim/tools/gitlab/list_pipelines.ts create mode 100644 apps/sim/tools/gitlab/list_projects.ts create mode 100644 apps/sim/tools/gitlab/merge_merge_request.ts create mode 100644 apps/sim/tools/gitlab/retry_pipeline.ts create mode 100644 apps/sim/tools/gitlab/types.ts create mode 100644 apps/sim/tools/gitlab/update_issue.ts create mode 100644 apps/sim/tools/gitlab/update_merge_request.ts create mode 100644 apps/sim/tools/grafana/alerts.ts create mode 100644 apps/sim/tools/grafana/annotations.ts create mode 100644 apps/sim/tools/grafana/dashboards.ts create mode 100644 apps/sim/tools/grafana/data_sources.ts create mode 100644 apps/sim/tools/grafana/folders.ts create mode 100644 apps/sim/tools/grafana/health.ts create mode 100644 apps/sim/tools/grafana/index.ts create mode 100644 apps/sim/tools/grafana/types.ts create mode 100644 apps/sim/tools/shopify/adjust_inventory.ts create mode 100644 apps/sim/tools/shopify/cancel_order.ts create mode 100644 apps/sim/tools/shopify/create_customer.ts create mode 100644 apps/sim/tools/shopify/create_product.ts create mode 100644 apps/sim/tools/shopify/delete_customer.ts create mode 100644 apps/sim/tools/shopify/delete_product.ts create mode 100644 apps/sim/tools/shopify/get_customer.ts create mode 100644 apps/sim/tools/shopify/get_inventory_level.ts create mode 100644 apps/sim/tools/shopify/get_order.ts create mode 100644 apps/sim/tools/shopify/get_product.ts create mode 100644 apps/sim/tools/shopify/index.ts create mode 100644 apps/sim/tools/shopify/list_customers.ts create mode 100644 apps/sim/tools/shopify/list_orders.ts create mode 100644 apps/sim/tools/shopify/list_products.ts create mode 100644 apps/sim/tools/shopify/types.ts create mode 100644 apps/sim/tools/shopify/update_customer.ts create mode 100644 apps/sim/tools/shopify/update_order.ts create mode 100644 apps/sim/tools/shopify/update_product.ts create mode 100644 apps/sim/tools/ssh/check_command_exists.ts create mode 100644 apps/sim/tools/ssh/check_file_exists.ts create mode 100644 apps/sim/tools/ssh/create_directory.ts create mode 100644 apps/sim/tools/ssh/delete_file.ts create mode 100644 apps/sim/tools/ssh/download_file.ts create mode 100644 apps/sim/tools/ssh/execute_command.ts create mode 100644 apps/sim/tools/ssh/execute_script.ts create mode 100644 apps/sim/tools/ssh/get_system_info.ts create mode 100644 apps/sim/tools/ssh/index.ts create mode 100644 apps/sim/tools/ssh/list_directory.ts create mode 100644 apps/sim/tools/ssh/move_rename.ts create mode 100644 apps/sim/tools/ssh/read_file_content.ts create mode 100644 apps/sim/tools/ssh/types.ts create mode 100644 apps/sim/tools/ssh/upload_file.ts create mode 100644 apps/sim/tools/ssh/write_file_content.ts create mode 100644 apps/sim/tools/wordpress/create_category.ts create mode 100644 apps/sim/tools/wordpress/create_comment.ts create mode 100644 apps/sim/tools/wordpress/create_page.ts create mode 100644 apps/sim/tools/wordpress/create_post.ts create mode 100644 apps/sim/tools/wordpress/create_tag.ts create mode 100644 apps/sim/tools/wordpress/delete_comment.ts create mode 100644 apps/sim/tools/wordpress/delete_media.ts create mode 100644 apps/sim/tools/wordpress/delete_page.ts create mode 100644 apps/sim/tools/wordpress/delete_post.ts create mode 100644 apps/sim/tools/wordpress/get_current_user.ts create mode 100644 apps/sim/tools/wordpress/get_media.ts create mode 100644 apps/sim/tools/wordpress/get_page.ts create mode 100644 apps/sim/tools/wordpress/get_post.ts create mode 100644 apps/sim/tools/wordpress/get_user.ts create mode 100644 apps/sim/tools/wordpress/index.ts create mode 100644 apps/sim/tools/wordpress/list_categories.ts create mode 100644 apps/sim/tools/wordpress/list_comments.ts create mode 100644 apps/sim/tools/wordpress/list_media.ts create mode 100644 apps/sim/tools/wordpress/list_pages.ts create mode 100644 apps/sim/tools/wordpress/list_posts.ts create mode 100644 apps/sim/tools/wordpress/list_tags.ts create mode 100644 apps/sim/tools/wordpress/list_users.ts create mode 100644 apps/sim/tools/wordpress/search_content.ts create mode 100644 apps/sim/tools/wordpress/types.ts create mode 100644 apps/sim/tools/wordpress/update_comment.ts create mode 100644 apps/sim/tools/wordpress/update_page.ts create mode 100644 apps/sim/tools/wordpress/update_post.ts create mode 100644 apps/sim/tools/wordpress/upload_media.ts create mode 100644 apps/sim/tools/zoom/create_meeting.ts create mode 100644 apps/sim/tools/zoom/delete_meeting.ts create mode 100644 apps/sim/tools/zoom/get_meeting.ts create mode 100644 apps/sim/tools/zoom/get_meeting_invitation.ts create mode 100644 apps/sim/tools/zoom/index.ts create mode 100644 apps/sim/tools/zoom/list_meetings.ts create mode 100644 apps/sim/tools/zoom/types.ts create mode 100644 apps/sim/tools/zoom/update_meeting.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index b85680a25bc..5cce8179a97 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -473,6 +473,30 @@ export function GithubIcon(props: SVGProps) { ) } +export function GitLabIcon(props: SVGProps) { + return ( + + + + + + + + + + + ) +} + export function SerperIcon(props: SVGProps) { return ( @@ -649,6 +673,37 @@ export function GmailIcon(props: SVGProps) { ) } +export function GrafanaIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function GoogleDriveIcon(props: SVGProps) { return ( ) { export function TwilioIcon(props: SVGProps) { return ( - + ) { export function LinkupIcon(props: SVGProps) { return ( - - + + + + ) } @@ -3694,6 +3741,22 @@ export function ZendeskIcon(props: SVGProps) { ) } +export function ZoomIcon(props: SVGProps) { + return ( + + + + ) +} + export function PylonIcon(props: SVGProps) { return ( ) { ) } +export function SshIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function ApifyIcon(props: SVGProps) { return ( ) { ) } + +export function WordpressIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +export function AhRefsIcon(props: SVGProps) { + return ( + + + + ) +} + +export function ShopifyIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function BoxCompanyIcon(props: SVGProps) { + return ( + + + + ) +} + +export function DropboxIcon(props: SVGProps) { + return ( + + + + ) +} + +export function ElasticsearchIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function GitlabIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +export function SSHIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function DatadogIcon(props: SVGProps) { + return ( + + + + + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 0ba630bdfcd..09525077ddf 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -4,24 +4,30 @@ import type { ComponentType, SVGProps } from 'react' import { + AhRefsIcon, AirtableIcon, ApifyIcon, ApolloIcon, ArxivIcon, AsanaIcon, + BoxCompanyIcon, BrainIcon, BrowserUseIcon, CalendlyIcon, ClayIcon, ConfluenceIcon, + DatadogIcon, DiscordIcon, DocumentIcon, + DropboxIcon, DynamoDBIcon, + ElasticsearchIcon, ElevenLabsIcon, ExaAIIcon, EyeIcon, FirecrawlIcon, GithubIcon, + GitLabIcon, GmailIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -30,6 +36,7 @@ import { GoogleIcon, GoogleSheetsIcon, GoogleVaultIcon, + GrafanaIcon, HubspotIcon, HuggingFaceIcon, HunterIOIcon, @@ -74,8 +81,10 @@ import { SendgridIcon, SentryIcon, SerperIcon, + ShopifyIcon, SlackIcon, SmtpIcon, + SshIcon, STTIcon, StagehandIcon, StripeIcon, @@ -92,19 +101,23 @@ import { WebflowIcon, WhatsAppIcon, WikipediaIcon, + WordpressIcon, xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, + ZoomIcon, } from '@/components/icons' type IconComponent = ComponentType> export const blockTypeToIconMap: Record = { + zoom: ZoomIcon, zep: ZepIcon, zendesk: ZendeskIcon, youtube: YouTubeIcon, x: xIcon, + wordpress: WordpressIcon, wikipedia: WikipediaIcon, whatsapp: WhatsAppIcon, webflow: WebflowIcon, @@ -125,8 +138,10 @@ export const blockTypeToIconMap: Record = { stripe: StripeIcon, stagehand_agent: StagehandIcon, stagehand: StagehandIcon, + ssh: SshIcon, smtp: SmtpIcon, slack: SlackIcon, + shopify: ShopifyIcon, sharepoint: MicrosoftSharepointIcon, serper: SerperIcon, sentry: SentryIcon, @@ -172,6 +187,7 @@ export const blockTypeToIconMap: Record = { hunter: HunterIOIcon, huggingface: HuggingFaceIcon, hubspot: HubspotIcon, + grafana: GrafanaIcon, google_vault: GoogleVaultIcon, google_sheets: GoogleSheetsIcon, google_forms: GoogleFormsIcon, @@ -180,20 +196,26 @@ export const blockTypeToIconMap: Record = { google_calendar: GoogleCalendarIcon, google_search: GoogleIcon, gmail: GmailIcon, + gitlab: GitLabIcon, github: GithubIcon, firecrawl: FirecrawlIcon, file: DocumentIcon, exa: ExaAIIcon, elevenlabs: ElevenLabsIcon, + elasticsearch: ElasticsearchIcon, dynamodb: DynamoDBIcon, + dropbox: DropboxIcon, discord: DiscordIcon, + datadog: DatadogIcon, confluence: ConfluenceIcon, clay: ClayIcon, calendly: CalendlyIcon, browser_use: BrowserUseIcon, + box: BoxCompanyIcon, asana: AsanaIcon, arxiv: ArxivIcon, apollo: ApolloIcon, apify: ApifyIcon, airtable: AirtableIcon, + ahrefs: AhRefsIcon, } diff --git a/apps/docs/content/docs/en/tools/ahrefs.mdx b/apps/docs/content/docs/en/tools/ahrefs.mdx new file mode 100644 index 00000000000..d21e29b8c48 --- /dev/null +++ b/apps/docs/content/docs/en/tools/ahrefs.mdx @@ -0,0 +1,197 @@ +--- +title: Ahrefs +description: SEO analysis with Ahrefs +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Ahrefs](https://ahrefs.com/) is a leading SEO toolset for analyzing websites, tracking rankings, monitoring backlinks, and researching keywords. It provides detailed insights into your own website as well as your competitors, helping you make data-driven decisions to improve your search visibility. + +With the Ahrefs integration in Sim, you can: + +- **Analyze Domain Rating & Authority**: Instantly check the Domain Rating (DR) and Ahrefs Rank of any website to gauge its authority. +- **Fetch Backlinks**: Retrieve a list of backlinks pointing to a site or specific URL, with details like anchor text, referring page DR, and more. +- **Get Backlink Statistics**: Access metrics on backlink types (dofollow, nofollow, text, image, redirect, etc.) for a domain or URL. +- **Explore Organic Keywords** *(planned)*: View keywords a domain ranks for and their positions in Google search results. +- **Discover Top Pages** *(planned)*: Identify the highest-performing pages by organic traffic and links. + +These tools let your agents automate SEO research, monitor competitors, and generate reports—all as part of your workflow automations. To use the Ahrefs integration, you’ll need an Ahrefs Enterprise subscription with API access. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Ahrefs SEO tools into your workflow. Analyze domain ratings, backlinks, organic keywords, top pages, and more. Requires an Ahrefs Enterprise plan with API access. + + + +## Tools + +### `ahrefs_domain_rating` + +Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain to analyze \(e.g., example.com\) | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `domainRating` | number | Domain Rating score \(0-100\) | +| `ahrefsRank` | number | Ahrefs Rank - global ranking based on backlink profile strength | + +### `ahrefs_backlinks` + +Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain or URL to analyze | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `limit` | number | No | Maximum number of results to return \(default: 100\) | +| `offset` | number | No | Number of results to skip for pagination | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `backlinks` | array | List of backlinks pointing to the target | + +### `ahrefs_backlinks_stats` + +Get backlink statistics for a target domain or URL. Returns totals for different backlink types including dofollow, nofollow, text, image, and redirect links. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain or URL to analyze | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stats` | object | Backlink statistics summary | + +### `ahrefs_referring_domains` + +Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain or URL to analyze | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `limit` | number | No | Maximum number of results to return \(default: 100\) | +| `offset` | number | No | Number of results to skip for pagination | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `referringDomains` | array | List of domains linking to the target | + +### `ahrefs_organic_keywords` + +Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain or URL to analyze | +| `country` | string | No | Country code for search results \(e.g., us, gb, de\). Default: us | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `limit` | number | No | Maximum number of results to return \(default: 100\) | +| `offset` | number | No | Number of results to skip for pagination | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `keywords` | array | List of organic keywords the target ranks for | + +### `ahrefs_top_pages` + +Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain to analyze | +| `country` | string | No | Country code for traffic data \(e.g., us, gb, de\). Default: us | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\) | +| `limit` | number | No | Maximum number of results to return \(default: 100\) | +| `offset` | number | No | Number of results to skip for pagination | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pages` | array | List of top pages by organic traffic | + +### `ahrefs_keyword_overview` + +Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `keyword` | string | Yes | The keyword to analyze | +| `country` | string | No | Country code for keyword data \(e.g., us, gb, de\). Default: us | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `overview` | object | Keyword metrics overview | + +### `ahrefs_broken_backlinks` + +Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | The target domain or URL to analyze | +| `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `limit` | number | No | Maximum number of results to return \(default: 100\) | +| `offset` | number | No | Number of results to skip for pagination | +| `apiKey` | string | Yes | Ahrefs API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `brokenBacklinks` | array | List of broken backlinks | + + + +## Notes + +- Category: `tools` +- Type: `ahrefs` diff --git a/apps/docs/content/docs/en/tools/box.mdx b/apps/docs/content/docs/en/tools/box.mdx new file mode 100644 index 00000000000..1b2268e428e --- /dev/null +++ b/apps/docs/content/docs/en/tools/box.mdx @@ -0,0 +1,276 @@ +--- +title: Box +description: Manage files and folders in Box cloud storage +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Box](https://box.com/) is a leading cloud content management and collaboration platform that enables users and organizations to securely store, access, and collaborate on files and folders from anywhere. + +With Box, you can: + +- **Manage files and folders**: Upload, organize, move, copy, rename, or delete content in your Box cloud storage +- **Collaborate with others**: Share files and folders, set permissions, and invite collaborators for secure teamwork +- **Search content**: Find files or folders by name or content within your Box account +- **Create shared links**: Generate secure, shareable links for files and folders + +In Sim, the Box integration allows your agents to interact programmatically with your Box account, automating the management of your files and folders. Using the tools below, you can: + +- Retrieve information about specific files (`box_get_file`) +- List the contents of any Box folder (`box_list_folder`) +- Create new folders (`box_create_folder`) +- Search for files or folders +- And more — enabling workflow automation involving your Box data. + +These capabilities let your agents manage content, streamline collaboration, and automate file operations as part of your workflow execution in Sim. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Box into the workflow. Can manage files and folders, search content, create shared links, and collaborate with users. Supports file operations like copy, move, delete, and download. + + + +## Tools + +### `box_get_file` + +Get information about a file in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to retrieve | +| `fields` | string | No | Comma-separated list of fields to include \(e.g., "id,name,size,modified_at,shared_link"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | The file information | + +### `box_list_folder` + +List items in a Box folder + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `folderId` | string | Yes | The ID of the folder to list \(use "0" for root folder\) | +| `limit` | number | No | Maximum number of items to return \(default: 100, max: 1000\) | +| `offset` | number | No | Offset for pagination \(default: 0\) | +| `fields` | string | No | Comma-separated list of fields to include | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of files and folders in the folder | + +### `box_create_folder` + +Create a new folder in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `parentFolderId` | string | Yes | The ID of the parent folder \(use "0" for root folder\) | +| `folderName` | string | Yes | The name of the new folder | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `folder` | object | The created folder | + +### `box_delete_file` + +Delete a file from Box. Standard delete moves to trash. Use permanent flag for files already in trash. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to delete | +| `permanent` | boolean | No | If true, permanently delete a file that is already in trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the file was successfully deleted | +| `fileId` | string | The ID of the deleted file | + +### `box_delete_folder` + +Delete a folder from Box (moves to trash by default) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `folderId` | string | Yes | The ID of the folder to delete | +| `recursive` | boolean | No | Delete folder contents recursively \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the folder was successfully deleted | +| `folderId` | string | The ID of the deleted folder | + +### `box_copy_file` + +Create a copy of a file in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to copy | +| `parentFolderId` | string | Yes | The ID of the destination folder | +| `newName` | string | No | Optional new name for the copy | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | The copied file | + +### `box_update_file` + +Update file information in Box (rename, move, or update description) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to update | +| `name` | string | No | New name for the file | +| `description` | string | No | New description for the file | +| `parentId` | string | No | ID of the new parent folder \(to move the file\) | +| `tags` | array | No | Array of tags to set on the file | +| `items` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | The updated file | + +### `box_search` + +Search for files and folders in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | The search query string | +| `scope` | string | No | Search scope: "user_content" \(default\) or "enterprise_content" | +| `fileExtensions` | string | No | Comma-separated file extensions to filter by \(e.g., "pdf,docx"\) | +| `ancestorFolderIds` | string | No | Comma-separated folder IDs to search within | +| `contentTypes` | string | No | Comma-separated content types: "name", "description", "file_content", "comments", "tag" | +| `limit` | number | No | Maximum number of results \(default: 30, max: 200\) | +| `offset` | number | No | Offset for pagination \(default: 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Array of search results | + +### `box_download_file` + +Get the download URL for a file in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to download | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | File content \(for text files\) | +| `downloadUrl` | string | URL to download the file | +| `fileName` | string | Name of the file | +| `mimeType` | string | MIME type of the file | + +### `box_get_current_user` + +Get information about the authenticated Box user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The authenticated user | + +### `box_create_shared_link` + +Create a shared link for a file or folder in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the file or folder | +| `itemType` | string | Yes | The type of item: "file" or "folder" | +| `access` | string | No | Access level: "open" \(anyone\), "company", or "collaborators" | +| `password` | string | No | Password to protect the shared link | +| `unsharedAt` | string | No | Expiration date in ISO 8601 format | +| `canDownload` | boolean | No | Whether the link allows downloads | +| `canPreview` | boolean | No | Whether the link allows previews | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sharedLink` | object | The shared link details | + +### `box_create_collaboration` + +Share a file or folder with a user or group in Box + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `itemId` | string | Yes | The ID of the file or folder to share | +| `itemType` | string | Yes | The type of item: "file" or "folder" | +| `accessibleByLogin` | string | No | Email address of the user to share with | +| `accessibleById` | string | No | ID of the user or group to share with | +| `accessibleByType` | string | Yes | Type of collaborator: "user" or "group" | +| `role` | string | Yes | Access role: "editor", "viewer", "previewer", "uploader", or "co-owner" | +| `canViewPath` | boolean | No | Whether the collaborator can see the full path to the folder \(folders only\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `collaboration` | object | The created collaboration | + + + +## Notes + +- Category: `tools` +- Type: `box` diff --git a/apps/docs/content/docs/en/tools/datadog.mdx b/apps/docs/content/docs/en/tools/datadog.mdx new file mode 100644 index 00000000000..ff79e95bf29 --- /dev/null +++ b/apps/docs/content/docs/en/tools/datadog.mdx @@ -0,0 +1,310 @@ +--- +title: Datadog +description: Monitor infrastructure, applications, and logs with Datadog +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Datadog](https://datadoghq.com/) is a comprehensive monitoring and analytics platform for infrastructure, applications, logs, and more. It enables organizations to gain real-time visibility into the health and performance of systems, detect anomalies, and automate incident response. + +With Datadog, you can: + +- **Monitor metrics**: Collect, visualize, and analyze metrics from servers, cloud services, and custom applications. +- **Query time series data**: Run advanced queries on performance metrics for trend analysis and reporting. +- **Manage monitors and events**: Set up monitors to detect issues, trigger alerts, and create events for observability. +- **Handle downtimes**: Schedule and programmatically manage planned downtimes to suppress alerts during maintenance. +- **Analyze logs and traces** *(with additional setup in Datadog)*: Centralize and inspect logs or distributed traces for deeper troubleshooting. + +Sim’s Datadog integration lets your agents automate these operations and interact with your Datadog account programmatically. Use it to submit custom metrics, query timeseries data, manage monitors, create events, and streamline your monitoring workflows directly within Sim automations. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Datadog monitoring into workflows. Submit metrics, manage monitors, query logs, create events, handle downtimes, and more. + + + +## Tools + +### `datadog_submit_metrics` + +Submit custom metrics to Datadog. Use for tracking application performance, business metrics, or custom monitoring data. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `series` | string | Yes | JSON array of metric series to submit. Each series should include metric name, type \(gauge/rate/count\), points \(timestamp/value pairs\), and optional tags. | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the metrics were submitted successfully | +| `errors` | array | Any errors that occurred during submission | + +### `datadog_query_timeseries` + +Query metric timeseries data from Datadog. Use for analyzing trends, creating reports, or retrieving metric values. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Datadog metrics query \(e.g., "avg:system.cpu.user\{*\}"\) | +| `from` | number | Yes | Start time as Unix timestamp in seconds | +| `to` | number | Yes | End time as Unix timestamp in seconds | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `series` | array | Array of timeseries data with metric name, tags, and data points | +| `status` | string | Query status | + +### `datadog_create_event` + +Post an event to the Datadog event stream. Use for deployment notifications, alerts, or any significant occurrences. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | string | Yes | Event title | +| `text` | string | Yes | Event body/description. Supports markdown. | +| `alertType` | string | No | Alert type: error, warning, info, success, user_update, recommendation, or snapshot | +| `priority` | string | No | Event priority: normal or low | +| `host` | string | No | Host name to associate with this event | +| `tags` | string | No | Comma-separated list of tags \(e.g., "env:production,service:api"\) | +| `aggregationKey` | string | No | Key to aggregate events together | +| `sourceTypeName` | string | No | Source type name for the event | +| `dateHappened` | number | No | Unix timestamp when the event occurred \(defaults to now\) | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event` | object | The created event details | + +### `datadog_create_monitor` + +Create a new monitor/alert in Datadog. Monitors can track metrics, service checks, events, and more. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Monitor name | +| `type` | string | Yes | Monitor type: metric alert, service check, event alert, process alert, log alert, query alert, composite, synthetics alert, slo alert | +| `query` | string | Yes | Monitor query \(e.g., "avg\(last_5m\):avg:system.cpu.idle\{*\} < 20"\) | +| `message` | string | No | Message to include with notifications. Can include @-mentions and markdown. | +| `tags` | string | No | Comma-separated list of tags | +| `priority` | number | No | Monitor priority \(1-5, where 1 is highest\) | +| `options` | string | No | JSON string of monitor options \(thresholds, notify_no_data, renotify_interval, etc.\) | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `monitor` | object | The created monitor details | + +### `datadog_get_monitor` + +Retrieve details of a specific monitor by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `monitorId` | string | Yes | The ID of the monitor to retrieve | +| `groupStates` | string | No | Comma-separated group states to include: alert, warn, no data, ok | +| `withDowntimes` | boolean | No | Include downtime data with the monitor | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `monitor` | object | The monitor details | + +### `datadog_list_monitors` + +List all monitors in Datadog with optional filtering by name, tags, or state. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupStates` | string | No | Comma-separated group states to filter by: alert, warn, no data, ok | +| `name` | string | No | Filter monitors by name \(partial match\) | +| `tags` | string | No | Comma-separated list of tags to filter by | +| `monitorTags` | string | No | Comma-separated list of monitor tags to filter by | +| `withDowntimes` | boolean | No | Include downtime data with monitors | +| `page` | number | No | Page number for pagination \(0-indexed\) | +| `pageSize` | number | No | Number of monitors per page \(max 1000\) | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `monitors` | array | List of monitors | + +### `datadog_mute_monitor` + +Mute a monitor to temporarily suppress notifications. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `monitorId` | string | Yes | The ID of the monitor to mute | +| `scope` | string | No | Scope to mute \(e.g., "host:myhost"\). If not specified, mutes all scopes. | +| `end` | number | No | Unix timestamp when the mute should end. If not specified, mutes indefinitely. | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the monitor was successfully muted | + +### `datadog_query_logs` + +Search and retrieve logs from Datadog. Use for troubleshooting, analysis, or monitoring. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Log search query \(e.g., "service:web-app status:error"\) | +| `from` | string | Yes | Start time in ISO-8601 format or relative \(e.g., "now-1h"\) | +| `to` | string | Yes | End time in ISO-8601 format or relative \(e.g., "now"\) | +| `limit` | number | No | Maximum number of logs to return \(default: 50, max: 1000\) | +| `sort` | string | No | Sort order: timestamp \(oldest first\) or -timestamp \(newest first\) | +| `indexes` | string | No | Comma-separated list of log indexes to search | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `logs` | array | List of log entries | + +### `datadog_send_logs` + +Send log entries to Datadog for centralized logging and analysis. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `logs` | string | Yes | JSON array of log entries. Each entry should have message and optionally ddsource, ddtags, hostname, service. | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the logs were sent successfully | + +### `datadog_create_downtime` + +Schedule a downtime to suppress monitor notifications during maintenance windows. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `scope` | string | Yes | Scope to apply downtime to \(e.g., "host:myhost", "env:production", or "*" for all\) | +| `message` | string | No | Message to display during downtime | +| `start` | number | No | Unix timestamp for downtime start \(defaults to now\) | +| `end` | number | No | Unix timestamp for downtime end | +| `timezone` | string | No | Timezone for the downtime \(e.g., "America/New_York"\) | +| `monitorId` | string | No | Specific monitor ID to mute | +| `monitorTags` | string | No | Comma-separated monitor tags to match \(e.g., "team:backend,priority:high"\) | +| `muteFirstRecoveryNotification` | boolean | No | Mute the first recovery notification | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `downtime` | object | The created downtime details | + +### `datadog_list_downtimes` + +List all scheduled downtimes in Datadog. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `currentOnly` | boolean | No | Only return currently active downtimes | +| `monitorId` | string | No | Filter by monitor ID | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `downtimes` | array | List of downtimes | + +### `datadog_cancel_downtime` + +Cancel a scheduled downtime. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `downtimeId` | string | Yes | The ID of the downtime to cancel | +| `apiKey` | string | Yes | Datadog API key | +| `applicationKey` | string | Yes | Datadog Application key | +| `site` | string | No | Datadog site/region \(default: datadoghq.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the downtime was successfully canceled | + + + +## Notes + +- Category: `tools` +- Type: `datadog` diff --git a/apps/docs/content/docs/en/tools/dropbox.mdx b/apps/docs/content/docs/en/tools/dropbox.mdx new file mode 100644 index 00000000000..a92555a697f --- /dev/null +++ b/apps/docs/content/docs/en/tools/dropbox.mdx @@ -0,0 +1,220 @@ +--- +title: Dropbox +description: Upload, download, share, and manage files in Dropbox +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Dropbox](https://dropbox.com/) is a popular cloud storage and collaboration platform that enables individuals and teams to securely store, access, and share files from anywhere. Dropbox is designed for easy file management, syncing, and powerful collaboration, whether you're working solo or with a group. + +With Dropbox in Sim, you can: + +- **Upload and download files**: Seamlessly upload any file to your Dropbox or retrieve content on demand +- **List folder contents**: Browse the files and folders within any Dropbox directory +- **Create new folders**: Organize your files by programmatically creating new folders in your Dropbox +- **Search files and folders**: Locate documents, images, or other items by name or content +- **Generate shared links**: Quickly create shareable public or private links for files and folders +- **Manage files**: Move, delete, or rename files and folders as part of automated workflows + +These capabilities allow your Sim agents to automate Dropbox operations directly within your workflows — from backing up important files to distributing content and maintaining organized folders. Use Dropbox as both a source and destination for files, enabling seamless cloud storage management as part of your business processes. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Dropbox into your workflow for file management, sharing, and collaboration. Upload files, download content, create folders, manage shared links, and more. + + + +## Tools + +### `dropbox_upload` + +Upload a file to Dropbox + +#### Input + +| 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 | +| `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 | +| `mute` | boolean | No | If true, don't notify the user about this upload | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | The uploaded file metadata | + +### `dropbox_download` + +Download a file from Dropbox and get a temporary link + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the file to download \(e.g., /folder/document.pdf\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | The file metadata | + +### `dropbox_list_folder` + +List the contents of a folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the folder to list \(use "" for root\) | +| `recursive` | boolean | No | If true, list contents recursively | +| `includeDeleted` | boolean | No | If true, include deleted files/folders | +| `includeMediaInfo` | boolean | No | If true, include media info for photos/videos | +| `limit` | number | No | Maximum number of results to return \(default: 500\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `entries` | array | List of files and folders in the directory | + +### `dropbox_create_folder` + +Create a new folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path where the folder should be created \(e.g., /new-folder\) | +| `autorename` | boolean | No | If true, rename the folder if there is a conflict | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `folder` | object | The created folder metadata | + +### `dropbox_delete` + +Delete a file or folder in Dropbox (moves to trash) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the file or folder to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Metadata of the deleted item | + +### `dropbox_copy` + +Copy a file or folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fromPath` | string | Yes | The source path of the file or folder to copy | +| `toPath` | string | Yes | The destination path for the copied file or folder | +| `autorename` | boolean | No | If true, rename the file if there is a conflict at destination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Metadata of the copied item | + +### `dropbox_move` + +Move or rename a file or folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fromPath` | string | Yes | The source path of the file or folder to move | +| `toPath` | string | Yes | The destination path for the moved file or folder | +| `autorename` | boolean | No | If true, rename the file if there is a conflict at destination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Metadata of the moved item | + +### `dropbox_get_metadata` + +Get metadata for a file or folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the file or folder to get metadata for | +| `includeMediaInfo` | boolean | No | If true, include media info for photos/videos | +| `includeDeleted` | boolean | No | If true, include deleted files in results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Metadata for the file or folder | + +### `dropbox_create_shared_link` + +Create a shareable link for a file or folder in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sharedLink` | object | The created shared link | + +### `dropbox_search` + +Search for files and folders in Dropbox + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | The search query | +| `path` | string | No | Limit search to a specific folder path | +| `fileExtensions` | string | No | Comma-separated list of file extensions to filter by \(e.g., pdf,xlsx\) | +| `maxResults` | number | No | Maximum number of results to return \(default: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matches` | array | Search results | + + + +## Notes + +- Category: `tools` +- Type: `dropbox` diff --git a/apps/docs/content/docs/en/tools/elasticsearch.mdx b/apps/docs/content/docs/en/tools/elasticsearch.mdx new file mode 100644 index 00000000000..6c2fd618f6f --- /dev/null +++ b/apps/docs/content/docs/en/tools/elasticsearch.mdx @@ -0,0 +1,370 @@ +--- +title: Elasticsearch +description: Search, index, and manage data in Elasticsearch +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Elasticsearch](https://www.elastic.co/elasticsearch/) is a powerful distributed search and analytics engine that enables you to index, search, and analyze large volumes of data in real time. It’s widely used for powering search features, log and event data analytics, observability, and more. + +With Elasticsearch in Sim, you gain programmatic access to core Elasticsearch capabilities, including: + +- **Search documents**: Perform advanced searches on structured or unstructured text using Query DSL, with support for sorting, pagination, and field selection. +- **Index documents**: Add new documents or update existing ones in any Elasticsearch index for immediate retrieval and analysis. +- **Get, update, or delete documents**: Retrieve, modify, or remove specific documents by ID. +- **Bulk operations**: Execute multiple indexing or update actions in a single request for high-throughput data processing. +- **Manage indexes**: Create, delete, or get details about indexes as part of your workflow automation. +- **Cluster monitoring**: Check the health and stats of your Elasticsearch deployment. + +Sim's Elasticsearch tools work with both self-hosted and Elastic Cloud environments. Integrate Elasticsearch into your agent workflows to automate data ingestion, search across vast datasets, run reporting, or build custom search-powered applications – all without manual intervention. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Elasticsearch into workflows for powerful search, indexing, and data management. Supports document CRUD operations, advanced search queries, bulk operations, index management, and cluster monitoring. Works with both self-hosted and Elastic Cloud deployments. + + + +## Tools + +### `elasticsearch_search` + +Search documents in Elasticsearch using Query DSL. Returns matching documents with scores and metadata. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name to search | +| `query` | string | No | Query DSL as JSON string | +| `from` | number | No | Starting offset for pagination \(default: 0\) | +| `size` | number | No | Number of results to return \(default: 10\) | +| `sort` | string | No | Sort specification as JSON string | +| `sourceIncludes` | string | No | Comma-separated list of fields to include in _source | +| `sourceExcludes` | string | No | Comma-separated list of fields to exclude from _source | +| `trackTotalHits` | boolean | No | Track accurate total hit count \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `took` | number | Time in milliseconds the search took | +| `timed_out` | boolean | Whether the search timed out | +| `hits` | object | Search results with total count and matching documents | +| `aggregations` | json | Aggregation results if any | + +### `elasticsearch_index_document` + +Index (create or update) a document in Elasticsearch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Target index name | +| `documentId` | string | No | Document ID \(auto-generated if not provided\) | +| `document` | string | Yes | Document body as JSON string | +| `refresh` | string | No | Refresh policy: true, false, or wait_for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `_index` | string | Index where the document was stored | +| `_id` | string | Document ID | +| `_version` | number | Document version | +| `result` | string | Operation result \(created or updated\) | + +### `elasticsearch_get_document` + +Retrieve a document by ID from Elasticsearch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name | +| `documentId` | string | Yes | Document ID to retrieve | +| `sourceIncludes` | string | No | Comma-separated list of fields to include | +| `sourceExcludes` | string | No | Comma-separated list of fields to exclude | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `_index` | string | Index name | +| `_id` | string | Document ID | +| `_version` | number | Document version | +| `found` | boolean | Whether the document was found | +| `_source` | json | Document content | + +### `elasticsearch_update_document` + +Partially update a document in Elasticsearch using doc merge. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name | +| `documentId` | string | Yes | Document ID to update | +| `document` | string | Yes | Partial document to merge as JSON string | +| `retryOnConflict` | number | No | Number of retries on version conflict | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `_index` | string | Index name | +| `_id` | string | Document ID | +| `_version` | number | New document version | +| `result` | string | Operation result \(updated or noop\) | + +### `elasticsearch_delete_document` + +Delete a document from Elasticsearch by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name | +| `documentId` | string | Yes | Document ID to delete | +| `refresh` | string | No | Refresh policy: true, false, or wait_for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `_index` | string | Index name | +| `_id` | string | Document ID | +| `_version` | number | Document version | +| `result` | string | Operation result \(deleted or not_found\) | + +### `elasticsearch_bulk` + +Perform multiple index, create, delete, or update operations in a single request for high performance. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | No | Default index for operations that do not specify one | +| `operations` | string | Yes | Bulk operations as NDJSON string \(newline-delimited JSON\) | +| `refresh` | string | No | Refresh policy: true, false, or wait_for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `took` | number | Time in milliseconds the bulk operation took | +| `errors` | boolean | Whether any operation had an error | +| `items` | array | Results for each operation | + +### `elasticsearch_count` + +Count documents matching a query in Elasticsearch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name to count documents in | +| `query` | string | No | Optional query to filter documents \(JSON string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `count` | number | Number of documents matching the query | +| `_shards` | object | Shard statistics | + +### `elasticsearch_create_index` + +Create a new index with optional settings and mappings. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name to create | +| `settings` | string | No | Index settings as JSON string | +| `mappings` | string | No | Index mappings as JSON string | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `acknowledged` | boolean | Whether the request was acknowledged | +| `shards_acknowledged` | boolean | Whether the shards were acknowledged | +| `index` | string | Created index name | + +### `elasticsearch_delete_index` + +Delete an index and all its documents. This operation is irreversible. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `acknowledged` | boolean | Whether the deletion was acknowledged | + +### `elasticsearch_get_index` + +Retrieve index information including settings, mappings, and aliases. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `index` | string | Yes | Index name to retrieve info for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `index` | json | Index information including aliases, mappings, and settings | + +### `elasticsearch_cluster_health` + +Get the health status of the Elasticsearch cluster. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | +| `waitForStatus` | string | No | Wait until cluster reaches this status: green, yellow, or red | +| `timeout` | string | No | Timeout for the wait operation \(e.g., 30s, 1m\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cluster_name` | string | Name of the cluster | +| `status` | string | Cluster health status: green, yellow, or red | +| `number_of_nodes` | number | Total number of nodes in the cluster | +| `number_of_data_nodes` | number | Number of data nodes | +| `active_shards` | number | Number of active shards | +| `unassigned_shards` | number | Number of unassigned shards | + +### `elasticsearch_cluster_stats` + +Get comprehensive statistics about the Elasticsearch cluster. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cluster_name` | string | Name of the cluster | +| `status` | string | Cluster health status | +| `nodes` | object | Node statistics including count and versions | +| `indices` | object | Index statistics including document count and store size | + + + +## Notes + +- Category: `tools` +- Type: `elasticsearch` diff --git a/apps/docs/content/docs/en/tools/gitlab.mdx b/apps/docs/content/docs/en/tools/gitlab.mdx new file mode 100644 index 00000000000..1f20c89c4b1 --- /dev/null +++ b/apps/docs/content/docs/en/tools/gitlab.mdx @@ -0,0 +1,434 @@ +--- +title: GitLab +description: Interact with GitLab projects, issues, merge requests, and pipelines +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[GitLab](https://gitlab.com/) is a comprehensive DevOps platform that allows teams to manage, collaborate on, and automate their software development lifecycle. With GitLab, you can effortlessly handle source code management, CI/CD, reviews, and collaboration in a single application. + +With GitLab in Sim, you can: + +- **Manage projects and repositories**: List and retrieve your GitLab projects, access details, and organize your repositories +- **Work with issues**: List, create, and comment on issues to track work and collaborate effectively +- **Handle merge requests**: Review, create, and manage merge requests for code changes and peer reviews +- **Automate CI/CD pipelines**: Trigger, monitor, and interact with GitLab pipelines as part of your automation flows +- **Collaborate with comments**: Add comments to issues or merge requests for efficient communication within your team + +Using Sim’s GitLab integration, your agents can programmatically interact with your GitLab projects. Automate project management, issue tracking, code reviews, and pipeline operations seamlessly in your workflows, optimizing your software development process and enhancing collaboration across your team. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate GitLab into the workflow. Can manage projects, issues, merge requests, pipelines, and add comments. Supports all core GitLab DevOps operations. + + + +## Tools + +### `gitlab_list_projects` + +List GitLab projects accessible to the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owned` | boolean | No | Limit to projects owned by the current user | +| `membership` | boolean | No | Limit to projects the current user is a member of | +| `search` | string | No | Search projects by name | +| `visibility` | string | No | Filter by visibility \(public, internal, private\) | +| `orderBy` | string | No | Order by field \(id, name, path, created_at, updated_at, last_activity_at\) | +| `sort` | string | No | Sort direction \(asc, desc\) | +| `perPage` | number | No | Number of results per page \(default 20, max 100\) | +| `page` | number | No | Page number for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `projects` | array | List of GitLab projects | +| `total` | number | Total number of projects | + +### `gitlab_get_project` + +Get details of a specific GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path \(e.g., "namespace/project"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `project` | object | The GitLab project details | + +### `gitlab_list_issues` + +List issues in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `state` | string | No | Filter by state \(opened, closed, all\) | +| `labels` | string | No | Comma-separated list of label names | +| `assigneeId` | number | No | Filter by assignee user ID | +| `milestoneTitle` | string | No | Filter by milestone title | +| `search` | string | No | Search issues by title and description | +| `orderBy` | string | No | Order by field \(created_at, updated_at\) | +| `sort` | string | No | Sort direction \(asc, desc\) | +| `perPage` | number | No | Number of results per page \(default 20, max 100\) | +| `page` | number | No | Page number for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issues` | array | List of GitLab issues | +| `total` | number | Total number of issues | + +### `gitlab_get_issue` + +Get details of a specific GitLab issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `issueIid` | number | Yes | Issue internal ID \(IID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issue` | object | The GitLab issue details | + +### `gitlab_create_issue` + +Create a new issue in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `title` | string | Yes | Issue title | +| `description` | string | No | Issue description \(Markdown supported\) | +| `labels` | string | No | Comma-separated list of label names | +| `assigneeIds` | array | No | Array of user IDs to assign | +| `milestoneId` | number | No | Milestone ID to assign | +| `dueDate` | string | No | Due date in YYYY-MM-DD format | +| `confidential` | boolean | No | Whether the issue is confidential | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issue` | object | The created GitLab issue | + +### `gitlab_update_issue` + +Update an existing issue in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `issueIid` | number | Yes | Issue internal ID \(IID\) | +| `title` | string | No | New issue title | +| `description` | string | No | New issue description \(Markdown supported\) | +| `stateEvent` | string | No | State event \(close or reopen\) | +| `labels` | string | No | Comma-separated list of label names | +| `assigneeIds` | array | No | Array of user IDs to assign | +| `milestoneId` | number | No | Milestone ID to assign | +| `dueDate` | string | No | Due date in YYYY-MM-DD format | +| `confidential` | boolean | No | Whether the issue is confidential | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issue` | object | The updated GitLab issue | + +### `gitlab_delete_issue` + +Delete an issue from a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `issueIid` | number | Yes | Issue internal ID \(IID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the issue was deleted successfully | + +### `gitlab_create_issue_note` + +Add a comment to a GitLab issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `issueIid` | number | Yes | Issue internal ID \(IID\) | +| `body` | string | Yes | Comment body \(Markdown supported\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `note` | object | The created comment | + +### `gitlab_list_merge_requests` + +List merge requests in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `state` | string | No | Filter by state \(opened, closed, merged, all\) | +| `labels` | string | No | Comma-separated list of label names | +| `sourceBranch` | string | No | Filter by source branch | +| `targetBranch` | string | No | Filter by target branch | +| `orderBy` | string | No | Order by field \(created_at, updated_at\) | +| `sort` | string | No | Sort direction \(asc, desc\) | +| `perPage` | number | No | Number of results per page \(default 20, max 100\) | +| `page` | number | No | Page number for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `mergeRequests` | array | List of GitLab merge requests | +| `total` | number | Total number of merge requests | + +### `gitlab_get_merge_request` + +Get details of a specific GitLab merge request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `mergeRequest` | object | The GitLab merge request details | + +### `gitlab_create_merge_request` + +Create a new merge request in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `sourceBranch` | string | Yes | Source branch name | +| `targetBranch` | string | Yes | Target branch name | +| `title` | string | Yes | Merge request title | +| `description` | string | No | Merge request description \(Markdown supported\) | +| `labels` | string | No | Comma-separated list of label names | +| `assigneeIds` | array | No | Array of user IDs to assign | +| `milestoneId` | number | No | Milestone ID to assign | +| `removeSourceBranch` | boolean | No | Delete source branch after merge | +| `squash` | boolean | No | Squash commits on merge | +| `draft` | boolean | No | Mark as draft \(work in progress\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `mergeRequest` | object | The created GitLab merge request | + +### `gitlab_update_merge_request` + +Update an existing merge request in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | +| `title` | string | No | New merge request title | +| `description` | string | No | New merge request description | +| `stateEvent` | string | No | State event \(close or reopen\) | +| `labels` | string | No | Comma-separated list of label names | +| `assigneeIds` | array | No | Array of user IDs to assign | +| `milestoneId` | number | No | Milestone ID to assign | +| `targetBranch` | string | No | New target branch | +| `removeSourceBranch` | boolean | No | Delete source branch after merge | +| `squash` | boolean | No | Squash commits on merge | +| `draft` | boolean | No | Mark as draft \(work in progress\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `mergeRequest` | object | The updated GitLab merge request | + +### `gitlab_merge_merge_request` + +Merge a merge request in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | +| `mergeCommitMessage` | string | No | Custom merge commit message | +| `squashCommitMessage` | string | No | Custom squash commit message | +| `squash` | boolean | No | Squash commits before merging | +| `shouldRemoveSourceBranch` | boolean | No | Delete source branch after merge | +| `mergeWhenPipelineSucceeds` | boolean | No | Merge when pipeline succeeds | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `mergeRequest` | object | The merged GitLab merge request | + +### `gitlab_create_merge_request_note` + +Add a comment to a GitLab merge request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `mergeRequestIid` | number | Yes | Merge request internal ID \(IID\) | +| `body` | string | Yes | Comment body \(Markdown supported\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `note` | object | The created comment | + +### `gitlab_list_pipelines` + +List pipelines in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `ref` | string | No | Filter by ref \(branch or tag\) | +| `status` | string | No | Filter by status \(created, waiting_for_resource, preparing, pending, running, success, failed, canceled, skipped, manual, scheduled\) | +| `orderBy` | string | No | Order by field \(id, status, ref, updated_at, user_id\) | +| `sort` | string | No | Sort direction \(asc, desc\) | +| `perPage` | number | No | Number of results per page \(default 20, max 100\) | +| `page` | number | No | Page number for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipelines` | array | List of GitLab pipelines | +| `total` | number | Total number of pipelines | + +### `gitlab_get_pipeline` + +Get details of a specific GitLab pipeline + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `pipelineId` | number | Yes | Pipeline ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | The GitLab pipeline details | + +### `gitlab_create_pipeline` + +Trigger a new pipeline in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `ref` | string | Yes | Branch or tag to run the pipeline on | +| `variables` | array | No | Array of variables for the pipeline \(each with key, value, and optional variable_type\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | The created GitLab pipeline | + +### `gitlab_retry_pipeline` + +Retry a failed GitLab pipeline + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `pipelineId` | number | Yes | Pipeline ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | The retried GitLab pipeline | + +### `gitlab_cancel_pipeline` + +Cancel a running GitLab pipeline + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `pipelineId` | number | Yes | Pipeline ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | The cancelled GitLab pipeline | + + + +## Notes + +- Category: `tools` +- Type: `gitlab` diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx new file mode 100644 index 00000000000..2707db758fb --- /dev/null +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -0,0 +1,613 @@ +--- +title: Grafana +description: Interact with Grafana dashboards, alerts, and annotations +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Grafana](https://grafana.com/) is a leading open-source platform for monitoring, observability, and visualization. It allows users to query, visualize, alert on, and analyze data from a variety of sources, making it an essential tool for infrastructure and application monitoring. + +With Grafana, you can: + +- **Visualize data**: Build and customize dashboards to display metrics, logs, and traces in real time +- **Monitor health and status**: Check the health of your Grafana instance and connected data sources +- **Manage alerts and annotations**: Set up alert rules, manage notifications, and annotate dashboards with important events +- **Organize content**: Organize dashboards and data sources into folders for better access management + +In Sim, the Grafana integration empowers your agents to interact directly with your Grafana instance via API, enabling actions such as: + +- Checking the Grafana server, database, and data source health status +- Retrieving, listing, and managing dashboards, alert rules, annotations, data sources, and folders +- Automating the monitoring of your infrastructure by integrating Grafana data and alerts into your workflow automations + +These capabilities enable Sim agents to monitor systems, proactively respond to alerts, and help ensure the reliability and visibility of your services — all as part of your automated workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data sources, folders, and monitor health status. + + + +## Tools + +### `grafana_health_check` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_data_source_health` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_get_dashboard` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_dashboards` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_create_dashboard` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_update_dashboard` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_delete_dashboard` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_alert_rules` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_get_alert_rule` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_create_alert_rule` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_update_alert_rule` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_delete_alert_rule` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_contact_points` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_create_annotation` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_annotations` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_update_annotation` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_delete_annotation` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_data_sources` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_get_data_source` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_list_folders` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + +### `grafana_create_folder` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | + + + +## Notes + +- Category: `tools` +- Type: `grafana` diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 8f853df9469..60f9b076f4c 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -1,22 +1,28 @@ { "pages": [ "index", + "ahrefs", "airtable", "apify", "apollo", "arxiv", "asana", + "box", "browser_use", "calendly", "clay", "confluence", + "datadog", "discord", + "dropbox", "dynamodb", + "elasticsearch", "elevenlabs", "exa", "file", "firecrawl", "github", + "gitlab", "gmail", "google_calendar", "google_docs", @@ -25,6 +31,7 @@ "google_search", "google_sheets", "google_vault", + "grafana", "hubspot", "huggingface", "hunter", @@ -70,8 +77,10 @@ "sentry", "serper", "sharepoint", + "shopify", "slack", "smtp", + "ssh", "stagehand", "stagehand_agent", "stripe", @@ -92,9 +101,11 @@ "webflow", "whatsapp", "wikipedia", + "wordpress", "x", "youtube", "zendesk", - "zep" + "zep", + "zoom" ] } diff --git a/apps/docs/content/docs/en/tools/shopify.mdx b/apps/docs/content/docs/en/tools/shopify.mdx new file mode 100644 index 00000000000..6ff9a1206cf --- /dev/null +++ b/apps/docs/content/docs/en/tools/shopify.mdx @@ -0,0 +1,352 @@ +--- +title: Shopify +description: Manage products, orders, customers, and inventory in your Shopify store +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Shopify](https://www.shopify.com/) is a leading e-commerce platform designed to help merchants build, run, and grow their online stores. Shopify makes it easy to manage every aspect of your store, from products and inventory to orders and customers. + +With Shopify in Sim, your agents can: + +- **Create and manage products**: Add new products, update product details, and remove products from your store. +- **List and retrieve orders**: Get information about customer orders, including filtering and order management. +- **Manage customers**: Access and update customer details, or add new customers to your store. +- **Adjust inventory levels**: Programmatically change product stock levels to keep your inventory accurate. + +Use Sim's Shopify integration to automate common store management workflows—such as syncing inventory, fulfilling orders, or managing listings—directly from your automations. Empower your agents to access, update, and organize all your store data using simple, programmatic tools. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Shopify into your workflow. Manage products, orders, customers, and inventory. Create, read, update, and delete products. List and manage orders. Handle customer data and adjust inventory levels. + + + +## Tools + +### `shopify_create_product` + +Create a new product in your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `title` | string | Yes | Product title | +| `descriptionHtml` | string | No | Product description \(HTML\) | +| `vendor` | string | No | Product vendor/brand | +| `productType` | string | No | Product type/category | +| `tags` | array | No | Product tags | +| `status` | string | No | Product status \(ACTIVE, DRAFT, ARCHIVED\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | object | The created product | + +### `shopify_get_product` + +Get a single product by ID from your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `productId` | string | Yes | Product ID \(gid://shopify/Product/123456789\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | object | The product details | + +### `shopify_list_products` + +List products from your Shopify store with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of products to return \(default: 50, max: 250\) | +| `query` | string | No | Search query to filter products \(e.g., "title:shirt" or "vendor:Nike" or "status:active"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `products` | array | List of products | +| `pageInfo` | object | Pagination information | + +### `shopify_update_product` + +Update an existing product in your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `productId` | string | Yes | Product ID to update \(gid://shopify/Product/123456789\) | +| `title` | string | No | New product title | +| `descriptionHtml` | string | No | New product description \(HTML\) | +| `vendor` | string | No | New product vendor/brand | +| `productType` | string | No | New product type/category | +| `tags` | array | No | New product tags | +| `status` | string | No | New product status \(ACTIVE, DRAFT, ARCHIVED\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | object | The updated product | + +### `shopify_delete_product` + +Delete a product from your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `productId` | string | Yes | Product ID to delete \(gid://shopify/Product/123456789\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deletedId` | string | The ID of the deleted product | + +### `shopify_get_order` + +Get a single order by ID from your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `orderId` | string | Yes | Order ID \(gid://shopify/Order/123456789\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `order` | object | The order details | + +### `shopify_list_orders` + +List orders from your Shopify store with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of orders to return \(default: 50, max: 250\) | +| `query` | string | No | Search query to filter orders \(e.g., "financial_status:paid" or "fulfillment_status:unfulfilled" or "email:customer@example.com"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `orders` | array | List of orders | +| `pageInfo` | object | Pagination information | + +### `shopify_update_order` + +Update an existing order in your Shopify store (note, tags, email) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `orderId` | string | Yes | Order ID to update \(gid://shopify/Order/123456789\) | +| `note` | string | No | New order note | +| `tags` | array | No | New order tags | +| `email` | string | No | New customer email for the order | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `order` | object | The updated order | + +### `shopify_cancel_order` + +Cancel an order in your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `orderId` | string | Yes | Order ID to cancel \(gid://shopify/Order/123456789\) | +| `reason` | string | Yes | Cancellation reason \(CUSTOMER, FRAUD, INVENTORY, DECLINED, OTHER\) | +| `notifyCustomer` | boolean | No | Whether to notify the customer about the cancellation | +| `refund` | boolean | No | Whether to refund the order | +| `restock` | boolean | No | Whether to restock the inventory | +| `staffNote` | string | No | A note about the cancellation for staff reference | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `order` | object | The cancellation result | + +### `shopify_create_customer` + +Create a new customer in your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `email` | string | No | Customer email address | +| `firstName` | string | No | Customer first name | +| `lastName` | string | No | Customer last name | +| `phone` | string | No | Customer phone number | +| `note` | string | No | Note about the customer | +| `tags` | array | No | Customer tags | +| `addresses` | array | No | Customer addresses | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | The created customer | + +### `shopify_get_customer` + +Get a single customer by ID from your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `customerId` | string | Yes | Customer ID \(gid://shopify/Customer/123456789\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | The customer details | + +### `shopify_list_customers` + +List customers from your Shopify store with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of customers to return \(default: 50, max: 250\) | +| `query` | string | No | Search query to filter customers \(e.g., "email:*@example.com" or "tag:vip" or "orders_count:>5"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customers` | array | List of customers | +| `pageInfo` | object | Pagination information | + +### `shopify_update_customer` + +Update an existing customer in your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `customerId` | string | Yes | Customer ID to update \(gid://shopify/Customer/123456789\) | +| `email` | string | No | New customer email address | +| `firstName` | string | No | New customer first name | +| `lastName` | string | No | New customer last name | +| `phone` | string | No | New customer phone number | +| `note` | string | No | New note about the customer | +| `tags` | array | No | New customer tags | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | object | The updated customer | + +### `shopify_delete_customer` + +Delete a customer from your Shopify store + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `customerId` | string | Yes | Customer ID to delete \(gid://shopify/Customer/123456789\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deletedId` | string | The ID of the deleted customer | + +### `shopify_get_inventory_level` + +Get inventory level for a product variant at a specific location + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `inventoryItemId` | string | Yes | Inventory item ID \(gid://shopify/InventoryItem/123456789\) | +| `locationId` | string | No | Location ID to filter by \(optional\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inventoryLevel` | object | The inventory level details | + +### `shopify_adjust_inventory` + +Adjust inventory quantity for a product variant at a specific location + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `inventoryItemId` | string | Yes | Inventory item ID \(gid://shopify/InventoryItem/123456789\) | +| `locationId` | string | Yes | Location ID \(gid://shopify/Location/123456789\) | +| `delta` | number | Yes | Amount to adjust \(positive to increase, negative to decrease\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inventoryLevel` | object | The inventory adjustment result | + + + +## Notes + +- Category: `tools` +- Type: `shopify` diff --git a/apps/docs/content/docs/en/tools/ssh.mdx b/apps/docs/content/docs/en/tools/ssh.mdx new file mode 100644 index 00000000000..3fb3466e12d --- /dev/null +++ b/apps/docs/content/docs/en/tools/ssh.mdx @@ -0,0 +1,399 @@ +--- +title: SSH +description: Connect to remote servers via SSH +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[SSH (Secure Shell)](https://en.wikipedia.org/wiki/Secure_Shell) is a widely-used protocol for securely connecting to remote servers, allowing you to execute commands, transfer files, and manage systems over encrypted channels. + +With SSH support in Sim, your agents can: + +- **Execute remote commands**: Run shell commands on any SSH-accessible server +- **Upload and run scripts**: Easily transfer and execute multi-line scripts for advanced automation +- **Transfer files securely**: Upload and download files as part of your workflows (coming soon or via command) +- **Automate server management**: Perform updates, maintenance, monitoring, deployments, and configuration tasks programmatically +- **Use flexible authentication**: Connect with password or private key authentication, including support for encrypted keys + +The following Sim SSH tools enable your agents to interact with servers as part of larger automations: + +- `ssh_execute_command`: Run any single shell command remotely and capture output, status, and errors. +- `ssh_execute_script`: Upload and execute a full multi-line script on the remote system. +- (Additional tools coming soon, such as file transfer.) + +By integrating SSH into your agent workflows, you can automate secure access, remote operations, and server orchestration—streamlining DevOps, IT automation, and custom remote management, all from within Sim. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access. + + + +## Tools + +### `ssh_execute_command` + +Execute a shell command on a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `command` | string | Yes | Shell command to execute on the remote server | +| `workingDirectory` | string | No | Working directory for command execution | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stdout` | string | Standard output from command | +| `stderr` | string | Standard error output | +| `exitCode` | number | Command exit code | +| `success` | boolean | Whether command succeeded \(exit code 0\) | +| `message` | string | Operation status message | + +### `ssh_execute_script` + +Upload and execute a multi-line script on a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `script` | string | Yes | Script content to execute \(bash, python, etc.\) | +| `interpreter` | string | No | Script interpreter \(default: /bin/bash\) | +| `workingDirectory` | string | No | Working directory for script execution | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stdout` | string | Standard output from script | +| `stderr` | string | Standard error output | +| `exitCode` | number | Script exit code | +| `success` | boolean | Whether script succeeded \(exit code 0\) | +| `scriptPath` | string | Temporary path where script was uploaded | +| `message` | string | Operation status message | + +### `ssh_check_command_exists` + +Check if a command/program exists on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `commandName` | string | Yes | Command name to check \(e.g., docker, git, python3\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `commandExists` | boolean | Whether the command exists | +| `commandPath` | string | Full path to the command \(if found\) | +| `version` | string | Command version output \(if applicable\) | +| `message` | string | Operation status message | + +### `ssh_upload_file` + +Upload a file to a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `fileContent` | string | Yes | File content to upload \(base64 encoded for binary files\) | +| `fileName` | string | Yes | Name of the file being uploaded | +| `remotePath` | string | Yes | Destination path on the remote server | +| `permissions` | string | No | File permissions \(e.g., 0644\) | +| `overwrite` | boolean | No | Whether to overwrite existing files \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `uploaded` | boolean | Whether the file was uploaded successfully | +| `remotePath` | string | Final path on the remote server | +| `size` | number | File size in bytes | +| `message` | string | Operation status message | + +### `ssh_download_file` + +Download a file from a remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `remotePath` | string | Yes | Path of the file on the remote server | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `downloaded` | boolean | Whether the file was downloaded successfully | +| `fileContent` | string | File content \(base64 encoded for binary files\) | +| `fileName` | string | Name of the downloaded file | +| `remotePath` | string | Source path on the remote server | +| `size` | number | File size in bytes | +| `message` | string | Operation status message | + +### `ssh_list_directory` + +List files and directories in a remote directory + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote directory path to list | +| `detailed` | boolean | No | Include file details \(size, permissions, modified date\) | +| `recursive` | boolean | No | List subdirectories recursively \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `entries` | array | Array of file and directory entries | + +### `ssh_check_file_exists` + +Check if a file or directory exists on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file or directory path to check | +| `type` | string | No | Expected type: file, directory, or any \(default: any\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `exists` | boolean | Whether the path exists | +| `type` | string | Type of path \(file, directory, symlink, not_found\) | +| `size` | number | File size if it is a file | +| `permissions` | string | File permissions \(e.g., 0755\) | +| `modified` | string | Last modified timestamp | +| `message` | string | Operation status message | + +### `ssh_create_directory` + +Create a directory on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Directory path to create | +| `recursive` | boolean | No | Create parent directories if they do not exist \(default: true\) | +| `permissions` | string | No | Directory permissions \(default: 0755\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `created` | boolean | Whether the directory was created successfully | +| `remotePath` | string | Created directory path | +| `alreadyExists` | boolean | Whether the directory already existed | +| `message` | string | Operation status message | + +### `ssh_delete_file` + +Delete a file or directory from the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Path to delete | +| `recursive` | boolean | No | Recursively delete directories \(default: false\) | +| `force` | boolean | No | Force deletion without confirmation \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the path was deleted successfully | +| `remotePath` | string | Deleted path | +| `message` | string | Operation status message | + +### `ssh_move_rename` + +Move or rename a file or directory on the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `sourcePath` | string | Yes | Current path of the file or directory | +| `destinationPath` | string | Yes | New path for the file or directory | +| `overwrite` | boolean | No | Overwrite destination if it exists \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `moved` | boolean | Whether the operation was successful | +| `sourcePath` | string | Original path | +| `destinationPath` | string | New path | +| `message` | string | Operation status message | + +### `ssh_get_system_info` + +Retrieve system information from the remote SSH server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `hostname` | string | Server hostname | +| `os` | string | Operating system \(e.g., Linux, Darwin\) | +| `architecture` | string | CPU architecture \(e.g., x64, arm64\) | +| `uptime` | number | System uptime in seconds | +| `memory` | json | Memory information \(total, free, used\) | +| `diskSpace` | json | Disk space information \(total, free, used\) | +| `message` | string | Operation status message | + +### `ssh_read_file_content` + +Read the contents of a remote file + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file path to read | +| `encoding` | string | No | File encoding \(default: utf-8\) | +| `maxSize` | number | No | Maximum file size to read in MB \(default: 10\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | File content as string | +| `size` | number | File size in bytes | +| `lines` | number | Number of lines in file | +| `remotePath` | string | Remote file path | +| `message` | string | Operation status message | + +### `ssh_write_file_content` + +Write or append content to a remote file + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | SSH server hostname or IP address | +| `port` | number | Yes | SSH server port \(default: 22\) | +| `username` | string | Yes | SSH username | +| `password` | string | No | Password for authentication \(if not using private key\) | +| `privateKey` | string | No | Private key for authentication \(OpenSSH format\) | +| `passphrase` | string | No | Passphrase for encrypted private key | +| `path` | string | Yes | Remote file path to write to | +| `content` | string | Yes | Content to write to the file | +| `mode` | string | No | Write mode: overwrite, append, or create \(default: overwrite\) | +| `permissions` | string | No | File permissions \(e.g., 0644\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `written` | boolean | Whether the file was written successfully | +| `remotePath` | string | File path | +| `size` | number | Final file size in bytes | +| `message` | string | Operation status message | + + + +## Notes + +- Category: `tools` +- Type: `ssh` diff --git a/apps/docs/content/docs/en/tools/wordpress.mdx b/apps/docs/content/docs/en/tools/wordpress.mdx new file mode 100644 index 00000000000..288f279aeac --- /dev/null +++ b/apps/docs/content/docs/en/tools/wordpress.mdx @@ -0,0 +1,629 @@ +--- +title: WordPress +description: Manage WordPress content +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[WordPress](https://wordpress.org/) is the world’s leading open-source content management system, making it easy to publish and manage websites, blogs, and all types of online content. With WordPress, you can create and update posts or pages, organize your content with categories and tags, manage media files, moderate comments, and handle user accounts—allowing you to run everything from personal blogs to complex business sites. + +Sim’s integration with WordPress lets your agents automate essential website tasks. You can programmatically create new blog posts with specific titles, content, categories, tags, and featured images. Updating existing posts—such as changing their content, title, or publishing status—is straightforward. You can also publish or save content as drafts, manage static pages, work with media uploads, oversee comments, and assign content to relevant organizational taxonomies. + +By connecting WordPress to your automations, Sim empowers your agents to streamline content publishing, editorial workflows, and everyday site management—helping you keep your website fresh, organized, and secure without manual effort. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports self-hosted WordPress sites using Application Passwords authentication. + + + +## Tools + +### `wordpress_create_post` + +Create a new blog post in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `title` | string | Yes | Post title | +| `content` | string | No | Post content \(HTML or plain text\) | +| `status` | string | No | Post status: publish, draft, pending, private, or future | +| `excerpt` | string | No | Post excerpt | +| `categories` | string | No | Comma-separated category IDs | +| `tags` | string | No | Comma-separated tag IDs | +| `featuredMedia` | number | No | Featured image media ID | +| `slug` | string | No | URL slug for the post | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `post` | object | The created post | + +### `wordpress_update_post` + +Update an existing blog post in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `postId` | number | Yes | The ID of the post to update | +| `title` | string | No | Post title | +| `content` | string | No | Post content \(HTML or plain text\) | +| `status` | string | No | Post status: publish, draft, pending, private, or future | +| `excerpt` | string | No | Post excerpt | +| `categories` | string | No | Comma-separated category IDs | +| `tags` | string | No | Comma-separated tag IDs | +| `featuredMedia` | number | No | Featured image media ID | +| `slug` | string | No | URL slug for the post | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `post` | object | The updated post | + +### `wordpress_delete_post` + +Delete a blog post from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `postId` | number | Yes | The ID of the post to delete | +| `force` | boolean | No | Bypass trash and force delete permanently | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the post was deleted | +| `post` | object | The deleted post | + +### `wordpress_get_post` + +Get a single blog post from WordPress by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `postId` | number | Yes | The ID of the post to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `post` | object | The retrieved post | + +### `wordpress_list_posts` + +List blog posts from WordPress with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of posts per page \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `status` | string | No | Post status filter: publish, draft, pending, private | +| `author` | number | No | Filter by author ID | +| `categories` | string | No | Comma-separated category IDs to filter by | +| `tags` | string | No | Comma-separated tag IDs to filter by | +| `search` | string | No | Search term to filter posts | +| `orderBy` | string | No | Order by field: date, id, title, slug, modified | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `posts` | array | List of posts | + +### `wordpress_create_page` + +Create a new page in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `title` | string | Yes | Page title | +| `content` | string | No | Page content \(HTML or plain text\) | +| `status` | string | No | Page status: publish, draft, pending, private | +| `excerpt` | string | No | Page excerpt | +| `parent` | number | No | Parent page ID for hierarchical pages | +| `menuOrder` | number | No | Order in page menu | +| `featuredMedia` | number | No | Featured image media ID | +| `slug` | string | No | URL slug for the page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | The created page | + +### `wordpress_update_page` + +Update an existing page in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `pageId` | number | Yes | The ID of the page to update | +| `title` | string | No | Page title | +| `content` | string | No | Page content \(HTML or plain text\) | +| `status` | string | No | Page status: publish, draft, pending, private | +| `excerpt` | string | No | Page excerpt | +| `parent` | number | No | Parent page ID for hierarchical pages | +| `menuOrder` | number | No | Order in page menu | +| `featuredMedia` | number | No | Featured image media ID | +| `slug` | string | No | URL slug for the page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | The updated page | + +### `wordpress_delete_page` + +Delete a page from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `pageId` | number | Yes | The ID of the page to delete | +| `force` | boolean | No | Bypass trash and force delete permanently | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the page was deleted | +| `page` | object | The deleted page | + +### `wordpress_get_page` + +Get a single page from WordPress by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `pageId` | number | Yes | The ID of the page to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | The retrieved page | + +### `wordpress_list_pages` + +List pages from WordPress with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of pages per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `status` | string | No | Page status filter: publish, draft, pending, private | +| `parent` | number | No | Filter by parent page ID | +| `search` | string | No | Search term to filter pages | +| `orderBy` | string | No | Order by field: date, id, title, slug, modified, menu_order | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pages` | array | List of pages | + +### `wordpress_upload_media` + +Upload a media file (image, video, document) to WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `file` | string | Yes | Base64 encoded file data or URL to fetch file from | +| `filename` | string | Yes | Filename with extension \(e.g., image.jpg\) | +| `title` | string | No | Media title | +| `caption` | string | No | Media caption | +| `altText` | string | No | Alternative text for accessibility | +| `description` | string | No | Media description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `media` | object | The uploaded media item | + +### `wordpress_get_media` + +Get a single media item from WordPress by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `mediaId` | number | Yes | The ID of the media item to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `media` | object | The retrieved media item | + +### `wordpress_list_media` + +List media items from the WordPress media library + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of media items per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `search` | string | No | Search term to filter media | +| `mediaType` | string | No | Filter by media type: image, video, audio, application | +| `mimeType` | string | No | Filter by specific MIME type \(e.g., image/jpeg\) | +| `orderBy` | string | No | Order by field: date, id, title, slug | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `media` | array | List of media items | + +### `wordpress_delete_media` + +Delete a media item from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `mediaId` | number | Yes | The ID of the media item to delete | +| `force` | boolean | No | Force delete \(media has no trash, so deletion is permanent\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the media was deleted | +| `media` | object | The deleted media item | + +### `wordpress_create_comment` + +Create a new comment on a WordPress post + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `postId` | number | Yes | The ID of the post to comment on | +| `content` | string | Yes | Comment content | +| `parent` | number | No | Parent comment ID for replies | +| `authorName` | string | No | Comment author display name | +| `authorEmail` | string | No | Comment author email | +| `authorUrl` | string | No | Comment author URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | The created comment | + +### `wordpress_list_comments` + +List comments from WordPress with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of comments per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `postId` | number | No | Filter by post ID | +| `status` | string | No | Filter by comment status: approved, hold, spam, trash | +| `search` | string | No | Search term to filter comments | +| `orderBy` | string | No | Order by field: date, id, parent | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comments` | array | List of comments | + +### `wordpress_update_comment` + +Update a comment in WordPress (content or status) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `commentId` | number | Yes | The ID of the comment to update | +| `content` | string | No | Updated comment content | +| `status` | string | No | Comment status: approved, hold, spam, trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | The updated comment | + +### `wordpress_delete_comment` + +Delete a comment from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `commentId` | number | Yes | The ID of the comment to delete | +| `force` | boolean | No | Bypass trash and force delete permanently | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the comment was deleted | +| `comment` | object | The deleted comment | + +### `wordpress_create_category` + +Create a new category in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `name` | string | Yes | Category name | +| `description` | string | No | Category description | +| `parent` | number | No | Parent category ID for hierarchical categories | +| `slug` | string | No | URL slug for the category | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `category` | object | The created category | + +### `wordpress_list_categories` + +List categories from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of categories per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `search` | string | No | Search term to filter categories | +| `parent` | number | No | Filter by parent category ID | +| `hideEmpty` | boolean | No | Hide categories with no posts | +| `orderBy` | string | No | Order by field: id, name, slug, count | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `categories` | array | List of categories | + +### `wordpress_create_tag` + +Create a new tag in WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `name` | string | Yes | Tag name | +| `description` | string | No | Tag description | +| `slug` | string | No | URL slug for the tag | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tag` | object | The created tag | + +### `wordpress_list_tags` + +List tags from WordPress + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of tags per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `search` | string | No | Search term to filter tags | +| `hideEmpty` | boolean | No | Hide tags with no posts | +| `orderBy` | string | No | Order by field: id, name, slug, count | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tags` | array | List of tags | + +### `wordpress_get_current_user` + +Get information about the currently authenticated WordPress user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The current user | + +### `wordpress_list_users` + +List users from WordPress (requires admin privileges) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `perPage` | number | No | Number of users per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `search` | string | No | Search term to filter users | +| `roles` | string | No | Comma-separated role names to filter by | +| `orderBy` | string | No | Order by field: id, name, slug, email, registered_date | +| `order` | string | No | Order direction: asc or desc | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | List of users | + +### `wordpress_get_user` + +Get a specific user from WordPress by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `userId` | number | Yes | The ID of the user to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The retrieved user | + +### `wordpress_search_content` + +Search across all content types in WordPress (posts, pages, media) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | +| `username` | string | Yes | WordPress username | +| `applicationPassword` | string | Yes | WordPress Application Password | +| `query` | string | Yes | Search query | +| `perPage` | number | No | Number of results per request \(default: 10, max: 100\) | +| `page` | number | No | Page number for pagination | +| `type` | string | No | Filter by content type: post, page, attachment | +| `subtype` | string | No | Filter by post type slug \(e.g., post, page\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Search results | + + + +## Notes + +- Category: `tools` +- Type: `wordpress` diff --git a/apps/docs/content/docs/en/tools/zoom.mdx b/apps/docs/content/docs/en/tools/zoom.mdx new file mode 100644 index 00000000000..6503342b62b --- /dev/null +++ b/apps/docs/content/docs/en/tools/zoom.mdx @@ -0,0 +1,178 @@ +--- +title: Zoom +description: Create and manage Zoom meetings +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Zoom](https://zoom.us/) is a leading cloud-based communications platform for video meetings, webinars, and online collaboration. It allows users and organizations to easily schedule, host, and manage meetings, providing tools for screen sharing, chat, recordings, and more. + +With Zoom, you can: + +- **Schedule and manage meetings**: Create instant or scheduled meetings, including recurring events +- **Configure meeting options**: Set meeting passwords, enable waiting rooms, and control participant video/audio +- **Send invitations and share details**: Retrieve meeting invitations and information for easy sharing +- **Get and update meeting data**: Access meeting details, modify existing meetings, and manage settings programmatically + +In Sim, the Zoom integration empowers your agents to automate scheduling and meeting management. Use tool actions to: + +- Programmatically create new meetings with custom settings +- List all meetings for a specific user (or yourself) +- Retrieve details or invitations for any meeting +- Update or delete existing meetings directly from your automations + +These capabilities let you streamline remote collaboration, automate recurring video sessions, and manage your organization's Zoom environment all as part of your workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, and manage meeting settings programmatically. + + + +## Tools + +### `zoom_create_meeting` + +Create a new Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID or email address. Use "me" for the authenticated user. | +| `topic` | string | Yes | Meeting topic | +| `type` | number | No | Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time | +| `startTime` | string | No | Meeting start time in ISO 8601 format \(e.g., 2025-06-03T10:00:00Z\) | +| `duration` | number | No | Meeting duration in minutes | +| `timezone` | string | No | Timezone for the meeting \(e.g., America/Los_Angeles\) | +| `password` | string | No | Meeting password | +| `agenda` | string | No | Meeting agenda | +| `hostVideo` | boolean | No | Start with host video on | +| `participantVideo` | boolean | No | Start with participant video on | +| `joinBeforeHost` | boolean | No | Allow participants to join before host | +| `muteUponEntry` | boolean | No | Mute participants upon entry | +| `waitingRoom` | boolean | No | Enable waiting room | +| `autoRecording` | string | No | Auto recording setting: local, cloud, or none | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `meeting` | object | The created meeting with all its properties | + +### `zoom_list_meetings` + +List all meetings for a Zoom user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID or email address. Use "me" for the authenticated user. | +| `type` | string | No | Meeting type filter: scheduled, live, upcoming, upcoming_meetings, or previous_meetings | +| `pageSize` | number | No | Number of records per page \(max 300\) | +| `nextPageToken` | string | No | Token for pagination to get next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `meetings` | array | List of meetings | +| `pageInfo` | object | Pagination information | + +### `zoom_get_meeting` + +Get details of a specific Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID | +| `occurrenceId` | string | No | Occurrence ID for recurring meetings | +| `showPreviousOccurrences` | boolean | No | Show previous occurrences for recurring meetings | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `meeting` | object | The meeting details | + +### `zoom_update_meeting` + +Update an existing Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID to update | +| `topic` | string | No | Meeting topic | +| `type` | number | No | Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time | +| `startTime` | string | No | Meeting start time in ISO 8601 format \(e.g., 2025-06-03T10:00:00Z\) | +| `duration` | number | No | Meeting duration in minutes | +| `timezone` | string | No | Timezone for the meeting \(e.g., America/Los_Angeles\) | +| `password` | string | No | Meeting password | +| `agenda` | string | No | Meeting agenda | +| `hostVideo` | boolean | No | Start with host video on | +| `participantVideo` | boolean | No | Start with participant video on | +| `joinBeforeHost` | boolean | No | Allow participants to join before host | +| `muteUponEntry` | boolean | No | Mute participants upon entry | +| `waitingRoom` | boolean | No | Enable waiting room | +| `autoRecording` | string | No | Auto recording setting: local, cloud, or none | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the meeting was updated successfully | + +### `zoom_delete_meeting` + +Delete or cancel a Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID to delete | +| `occurrenceId` | string | No | Occurrence ID for deleting a specific occurrence of a recurring meeting | +| `scheduleForReminder` | boolean | No | Send cancellation reminder email to registrants | +| `cancelMeetingReminder` | boolean | No | Send cancellation email to registrants and alternative hosts | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the meeting was deleted successfully | + +### `zoom_get_meeting_invitation` + +Get the meeting invitation text for a Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invitation` | string | The meeting invitation text | + + + +## Notes + +- Category: `tools` +- Type: `zoom` diff --git a/apps/sim/app/api/tools/ssh/check-command-exists/route.ts b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts new file mode 100644 index 00000000000..35d5579cc09 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts @@ -0,0 +1,105 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHCheckCommandExistsAPI') + +const CheckCommandExistsSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + commandName: z.string().min(1, 'Command name is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = CheckCommandExistsSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Checking if command '${params.commandName}' exists on ${params.host}:${params.port}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + // Use which/command to check if the command exists + const result = await executeSSHCommand( + client, + `command -v '${params.commandName}' 2>/dev/null || which '${params.commandName}' 2>/dev/null` + ) + + const exists = result.exitCode === 0 && result.stdout.trim().length > 0 + const path = exists ? result.stdout.trim() : undefined + + // Try to get version if command exists + let version: string | undefined + if (exists) { + try { + const versionResult = await executeSSHCommand( + client, + `'${params.commandName}' --version 2>&1 | head -1 || '${params.commandName}' -v 2>&1 | head -1` + ) + if (versionResult.exitCode === 0 && versionResult.stdout.trim()) { + version = versionResult.stdout.trim() + } + } catch { + // Version check failed, that's okay + } + } + + logger.info( + `[${requestId}] Command '${params.commandName}' ${exists ? 'exists' : 'does not exist'}` + ) + + return NextResponse.json({ + exists, + path, + version, + message: exists + ? `Command '${params.commandName}' found at ${path}` + : `Command '${params.commandName}' not found`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH check command exists failed:`, error) + + return NextResponse.json( + { error: `SSH check command exists failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/check-file-exists/route.ts b/apps/sim/app/api/tools/ssh/check-file-exists/route.ts new file mode 100644 index 00000000000..c02e008d039 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/check-file-exists/route.ts @@ -0,0 +1,134 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, SFTPWrapper, Stats } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + createSSHConnection, + getFileType, + parsePermissions, + sanitizePath, +} from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHCheckFileExistsAPI') + +const CheckFileExistsSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + type: z.enum(['file', 'directory', 'any']).default('any'), +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = CheckFileExistsSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Checking if path exists: ${params.path} on ${params.host}:${params.port}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const filePath = sanitizePath(params.path) + + const stats = await new Promise((resolve) => { + sftp.stat(filePath, (err, stats) => { + if (err) { + resolve(null) + } else { + resolve(stats) + } + }) + }) + + if (!stats) { + logger.info(`[${requestId}] Path does not exist: ${filePath}`) + return NextResponse.json({ + exists: false, + type: 'not_found', + message: `Path does not exist: ${filePath}`, + }) + } + + const fileType = getFileType(stats) + + // Check if the type matches the expected type + if (params.type !== 'any' && fileType !== params.type) { + logger.info(`[${requestId}] Path exists but is not a ${params.type}: ${filePath}`) + return NextResponse.json({ + exists: false, + type: fileType, + size: stats.size, + permissions: parsePermissions(stats.mode), + modified: new Date((stats.mtime || 0) * 1000).toISOString(), + message: `Path exists but is a ${fileType}, not a ${params.type}`, + }) + } + + logger.info(`[${requestId}] Path exists: ${filePath} (${fileType})`) + + return NextResponse.json({ + exists: true, + type: fileType, + size: stats.size, + permissions: parsePermissions(stats.mode), + modified: new Date((stats.mtime || 0) * 1000).toISOString(), + message: `Path exists: ${filePath} (${fileType})`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH check file exists failed:`, error) + + return NextResponse.json( + { error: `SSH check file exists failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/create-directory/route.ts b/apps/sim/app/api/tools/ssh/create-directory/route.ts new file mode 100644 index 00000000000..cf2733a3af5 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/create-directory/route.ts @@ -0,0 +1,101 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHCreateDirectoryAPI') + +const CreateDirectorySchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + recursive: z.boolean().default(true), + permissions: z.string().default('0755'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = CreateDirectorySchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Creating directory ${params.path} on ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const dirPath = sanitizePath(params.path) + + // Check if directory already exists + const checkResult = await executeSSHCommand(client, `test -d '${dirPath}' && echo "exists"`) + const alreadyExists = checkResult.stdout.trim() === 'exists' + + if (alreadyExists) { + logger.info(`[${requestId}] Directory already exists: ${dirPath}`) + return NextResponse.json({ + created: false, + path: dirPath, + alreadyExists: true, + message: `Directory already exists: ${dirPath}`, + }) + } + + // Create directory + const mkdirFlag = params.recursive ? '-p' : '' + const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${dirPath}'` + const result = await executeSSHCommand(client, command) + + if (result.exitCode !== 0) { + throw new Error(result.stderr || 'Failed to create directory') + } + + logger.info(`[${requestId}] Directory created successfully: ${dirPath}`) + + return NextResponse.json({ + created: true, + path: dirPath, + alreadyExists: false, + message: `Directory created successfully: ${dirPath}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH create directory failed:`, error) + + return NextResponse.json( + { error: `SSH create directory failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/delete-file/route.ts b/apps/sim/app/api/tools/ssh/delete-file/route.ts new file mode 100644 index 00000000000..9dd6b489005 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/delete-file/route.ts @@ -0,0 +1,94 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHDeleteFileAPI') + +const DeleteFileSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + recursive: z.boolean().default(false), + force: z.boolean().default(false), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = DeleteFileSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Deleting ${params.path} on ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const filePath = sanitizePath(params.path) + + // Check if path exists + const checkResult = await executeSSHCommand(client, `test -e '${filePath}' && echo "exists"`) + if (checkResult.stdout.trim() !== 'exists') { + return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 }) + } + + // Build delete command + let command: string + if (params.recursive) { + command = params.force ? `rm -rf '${filePath}'` : `rm -r '${filePath}'` + } else { + command = params.force ? `rm -f '${filePath}'` : `rm '${filePath}'` + } + + const result = await executeSSHCommand(client, command) + + if (result.exitCode !== 0) { + throw new Error(result.stderr || 'Failed to delete path') + } + + logger.info(`[${requestId}] Path deleted successfully: ${filePath}`) + + return NextResponse.json({ + deleted: true, + path: filePath, + message: `Successfully deleted: ${filePath}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH delete file failed:`, error) + + return NextResponse.json({ error: `SSH delete file failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/ssh/download-file/route.ts b/apps/sim/app/api/tools/ssh/download-file/route.ts new file mode 100644 index 00000000000..3689ab0324b --- /dev/null +++ b/apps/sim/app/api/tools/ssh/download-file/route.ts @@ -0,0 +1,127 @@ +import { randomUUID } from 'crypto' +import path from 'path' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, SFTPWrapper } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHDownloadFileAPI') + +const DownloadFileSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + remotePath: z.string().min(1, 'Remote path is required'), +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = DownloadFileSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Downloading file from ${params.host}:${params.port}${params.remotePath}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const remotePath = sanitizePath(params.remotePath) + + // Check if file exists + const stats = await new Promise<{ size: number }>((resolve, reject) => { + sftp.stat(remotePath, (err, stats) => { + if (err) { + reject(new Error(`File not found: ${remotePath}`)) + } else { + resolve(stats) + } + }) + }) + + // Read file content + const content = await new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + const readStream = sftp.createReadStream(remotePath) + + readStream.on('data', (chunk: Buffer) => { + chunks.push(chunk) + }) + + readStream.on('end', () => { + resolve(Buffer.concat(chunks)) + }) + + readStream.on('error', reject) + }) + + const fileName = path.basename(remotePath) + + // Encode content as base64 for binary safety + const base64Content = content.toString('base64') + + logger.info(`[${requestId}] File downloaded successfully from ${remotePath}`) + + return NextResponse.json({ + downloaded: true, + content: base64Content, + fileName: fileName, + remotePath: remotePath, + size: stats.size, + message: `File downloaded successfully from ${remotePath}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH file download failed:`, error) + + return NextResponse.json( + { error: `SSH file download failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/execute-command/route.ts b/apps/sim/app/api/tools/ssh/execute-command/route.ts new file mode 100644 index 00000000000..187c7be7563 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/execute-command/route.ts @@ -0,0 +1,84 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand, sanitizeCommand } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHExecuteCommandAPI') + +const ExecuteCommandSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + command: z.string().min(1, 'Command is required'), + workingDirectory: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ExecuteCommandSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Executing SSH command on ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + // Build command with optional working directory + let command = sanitizeCommand(params.command) + if (params.workingDirectory) { + command = `cd "${params.workingDirectory}" && ${command}` + } + + const result = await executeSSHCommand(client, command) + + logger.info(`[${requestId}] Command executed successfully with exit code ${result.exitCode}`) + + return NextResponse.json({ + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + success: result.exitCode === 0, + message: `Command executed with exit code ${result.exitCode}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH command execution failed:`, error) + + return NextResponse.json( + { error: `SSH command execution failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/execute-script/route.ts b/apps/sim/app/api/tools/ssh/execute-script/route.ts new file mode 100644 index 00000000000..5a24325cf49 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/execute-script/route.ts @@ -0,0 +1,103 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHExecuteScriptAPI') + +const ExecuteScriptSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + script: z.string().min(1, 'Script content is required'), + interpreter: z.string().default('/bin/bash'), + workingDirectory: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ExecuteScriptSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Executing SSH script on ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + // Create a temporary script file, execute it, and clean up + const scriptPath = `/tmp/sim_script_${requestId}.sh` + + // Escape the script content for use in a heredoc + const escapedScript = params.script.replace(/'/g, "'\\''") + + // Build the command to create, execute, and clean up the script + let command = `cat > '${scriptPath}' << 'SIMEOF' +${params.script} +SIMEOF +chmod +x '${scriptPath}'` + + if (params.workingDirectory) { + command += ` +cd "${params.workingDirectory}"` + } + + command += ` +'${params.interpreter}' '${scriptPath}' +exit_code=$? +rm -f '${scriptPath}' +exit $exit_code` + + const result = await executeSSHCommand(client, command) + + logger.info(`[${requestId}] Script executed successfully with exit code ${result.exitCode}`) + + return NextResponse.json({ + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + success: result.exitCode === 0, + scriptPath: scriptPath, + message: `Script executed with exit code ${result.exitCode}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH script execution failed:`, error) + + return NextResponse.json( + { error: `SSH script execution failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/get-system-info/route.ts b/apps/sim/app/api/tools/ssh/get-system-info/route.ts new file mode 100644 index 00000000000..4bee0e25b1a --- /dev/null +++ b/apps/sim/app/api/tools/ssh/get-system-info/route.ts @@ -0,0 +1,125 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHGetSystemInfoAPI') + +const GetSystemInfoSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = GetSystemInfoSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Getting system info from ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + // Get hostname + const hostnameResult = await executeSSHCommand(client, 'hostname') + const hostname = hostnameResult.stdout.trim() + + // Get OS info + const osResult = await executeSSHCommand(client, 'uname -s') + const os = osResult.stdout.trim() + + // Get architecture + const archResult = await executeSSHCommand(client, 'uname -m') + const architecture = archResult.stdout.trim() + + // Get uptime in seconds + const uptimeResult = await executeSSHCommand( + client, + "cat /proc/uptime 2>/dev/null | awk '{print int($1)}' || sysctl -n kern.boottime 2>/dev/null | awk '{print int(($(date +%s)) - $4)}'" + ) + const uptime = Number.parseInt(uptimeResult.stdout.trim()) || 0 + + // Get memory info + const memoryResult = await executeSSHCommand( + client, + "free -b 2>/dev/null | awk '/Mem:/ {print $2, $7, $3}' || vm_stat 2>/dev/null | awk '/Pages free|Pages active|Pages speculative|Pages wired|page size/ {gsub(/[^0-9]/, \"\"); print}'" + ) + const memParts = memoryResult.stdout.trim().split(/\s+/) + let memory = { total: 0, free: 0, used: 0 } + if (memParts.length >= 3) { + memory = { + total: Number.parseInt(memParts[0]) || 0, + free: Number.parseInt(memParts[1]) || 0, + used: Number.parseInt(memParts[2]) || 0, + } + } + + // Get disk space + const diskResult = await executeSSHCommand( + client, + "df -B1 / 2>/dev/null | awk 'NR==2 {print $2, $4, $3}' || df -k / 2>/dev/null | awk 'NR==2 {print $2*1024, $4*1024, $3*1024}'" + ) + const diskParts = diskResult.stdout.trim().split(/\s+/) + let diskSpace = { total: 0, free: 0, used: 0 } + if (diskParts.length >= 3) { + diskSpace = { + total: Number.parseInt(diskParts[0]) || 0, + free: Number.parseInt(diskParts[1]) || 0, + used: Number.parseInt(diskParts[2]) || 0, + } + } + + logger.info(`[${requestId}] System info retrieved successfully`) + + return NextResponse.json({ + hostname, + os, + architecture, + uptime, + memory, + diskSpace, + message: `System info retrieved for ${hostname}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH get system info failed:`, error) + + return NextResponse.json( + { error: `SSH get system info failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/list-directory/route.ts b/apps/sim/app/api/tools/ssh/list-directory/route.ts new file mode 100644 index 00000000000..987ab96a43e --- /dev/null +++ b/apps/sim/app/api/tools/ssh/list-directory/route.ts @@ -0,0 +1,132 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, FileEntry, SFTPWrapper } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + createSSHConnection, + getFileType, + parsePermissions, + sanitizePath, +} from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHListDirectoryAPI') + +const ListDirectorySchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + detailed: z.boolean().default(true), + recursive: z.boolean().default(false), +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +interface FileInfo { + name: string + type: 'file' | 'directory' | 'symlink' | 'other' + size: number + permissions: string + modified: string +} + +async function listDir(sftp: SFTPWrapper, dirPath: string): Promise { + return new Promise((resolve, reject) => { + sftp.readdir(dirPath, (err, list) => { + if (err) { + reject(err) + } else { + resolve(list) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ListDirectorySchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Listing directory ${params.path} on ${params.host}:${params.port}`) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const dirPath = sanitizePath(params.path) + + const list = await listDir(sftp, dirPath) + + const entries: FileInfo[] = list.map((entry) => ({ + name: entry.filename, + type: getFileType(entry.attrs), + size: entry.attrs.size, + permissions: parsePermissions(entry.attrs.mode), + modified: new Date((entry.attrs.mtime || 0) * 1000).toISOString(), + })) + + const totalFiles = entries.filter((e) => e.type === 'file').length + const totalDirectories = entries.filter((e) => e.type === 'directory').length + + logger.info( + `[${requestId}] Directory listed successfully: ${totalFiles} files, ${totalDirectories} directories` + ) + + return NextResponse.json({ + entries, + totalFiles, + totalDirectories, + message: `Found ${totalFiles} files and ${totalDirectories} directories`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH list directory failed:`, error) + + return NextResponse.json( + { error: `SSH list directory failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/move-rename/route.ts b/apps/sim/app/api/tools/ssh/move-rename/route.ts new file mode 100644 index 00000000000..21778533353 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/move-rename/route.ts @@ -0,0 +1,111 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHMoveRenameAPI') + +const MoveRenameSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + sourcePath: z.string().min(1, 'Source path is required'), + destinationPath: z.string().min(1, 'Destination path is required'), + overwrite: z.boolean().default(false), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = MoveRenameSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Moving ${params.sourcePath} to ${params.destinationPath} on ${params.host}:${params.port}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sourcePath = sanitizePath(params.sourcePath) + const destPath = sanitizePath(params.destinationPath) + + // Check if source exists + const sourceCheck = await executeSSHCommand( + client, + `test -e '${sourcePath}' && echo "exists"` + ) + if (sourceCheck.stdout.trim() !== 'exists') { + return NextResponse.json( + { error: `Source path does not exist: ${sourcePath}` }, + { status: 404 } + ) + } + + // Check if destination exists (when overwrite is false) + if (!params.overwrite) { + const destCheck = await executeSSHCommand(client, `test -e '${destPath}' && echo "exists"`) + if (destCheck.stdout.trim() === 'exists') { + return NextResponse.json( + { error: `Destination already exists and overwrite is disabled: ${destPath}` }, + { status: 409 } + ) + } + } + + // Move/rename + const command = params.overwrite + ? `mv -f '${sourcePath}' '${destPath}'` + : `mv '${sourcePath}' '${destPath}'` + const result = await executeSSHCommand(client, command) + + if (result.exitCode !== 0) { + throw new Error(result.stderr || 'Failed to move/rename') + } + + logger.info(`[${requestId}] Successfully moved ${sourcePath} to ${destPath}`) + + return NextResponse.json({ + success: true, + sourcePath, + destinationPath: destPath, + message: `Successfully moved ${sourcePath} to ${destPath}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH move/rename failed:`, error) + + return NextResponse.json({ error: `SSH move/rename failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/ssh/read-file-content/route.ts b/apps/sim/app/api/tools/ssh/read-file-content/route.ts new file mode 100644 index 00000000000..f4c6cb522fd --- /dev/null +++ b/apps/sim/app/api/tools/ssh/read-file-content/route.ts @@ -0,0 +1,135 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, SFTPWrapper } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHReadFileContentAPI') + +const ReadFileContentSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + encoding: z.string().default('utf-8'), + maxSize: z.coerce.number().default(10), // MB +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ReadFileContentSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Reading file content from ${params.path} on ${params.host}:${params.port}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const filePath = sanitizePath(params.path) + const maxBytes = params.maxSize * 1024 * 1024 // Convert MB to bytes + + // Check file stats + const stats = await new Promise<{ size: number }>((resolve, reject) => { + sftp.stat(filePath, (err, stats) => { + if (err) { + reject(new Error(`File not found: ${filePath}`)) + } else { + resolve(stats) + } + }) + }) + + if (stats.size > maxBytes) { + return NextResponse.json( + { error: `File size (${stats.size} bytes) exceeds maximum allowed (${maxBytes} bytes)` }, + { status: 400 } + ) + } + + // Read file content + const content = await new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + const readStream = sftp.createReadStream(filePath) + + readStream.on('data', (chunk: Buffer) => { + chunks.push(chunk) + }) + + readStream.on('end', () => { + const buffer = Buffer.concat(chunks) + resolve(buffer.toString(params.encoding as BufferEncoding)) + }) + + readStream.on('error', reject) + }) + + const lines = content.split('\n').length + + logger.info( + `[${requestId}] File content read successfully: ${stats.size} bytes, ${lines} lines` + ) + + return NextResponse.json({ + content, + size: stats.size, + lines, + path: filePath, + message: `File read successfully: ${stats.size} bytes, ${lines} lines`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH read file content failed:`, error) + + return NextResponse.json( + { error: `SSH read file content failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/ssh/upload-file/route.ts b/apps/sim/app/api/tools/ssh/upload-file/route.ts new file mode 100644 index 00000000000..bbe74a14f44 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/upload-file/route.ts @@ -0,0 +1,137 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, SFTPWrapper } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHUploadFileAPI') + +const UploadFileSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + fileContent: z.string().min(1, 'File content is required'), + fileName: z.string().min(1, 'File name is required'), + remotePath: z.string().min(1, 'Remote path is required'), + permissions: z.string().optional(), + overwrite: z.boolean().default(true), +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = UploadFileSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Uploading file to ${params.host}:${params.port}${params.remotePath}` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const remotePath = sanitizePath(params.remotePath) + + // Check if file exists and overwrite is false + if (!params.overwrite) { + const exists = await new Promise((resolve) => { + sftp.stat(remotePath, (err) => { + resolve(!err) + }) + }) + + if (exists) { + return NextResponse.json( + { error: 'File already exists and overwrite is disabled' }, + { status: 409 } + ) + } + } + + // Decode content (handle base64 for binary files) + let content: Buffer + try { + // Try to decode as base64 first + content = Buffer.from(params.fileContent, 'base64') + // If the decoded content is not valid UTF-8 or looks like base64, use it as-is + const reEncoded = content.toString('base64') + if (reEncoded !== params.fileContent) { + // Not valid base64, treat as plain text + content = Buffer.from(params.fileContent, 'utf-8') + } + } catch { + // Plain text content + content = Buffer.from(params.fileContent, 'utf-8') + } + + // Write file + await new Promise((resolve, reject) => { + const writeStream = sftp.createWriteStream(remotePath, { + mode: params.permissions ? Number.parseInt(params.permissions, 8) : 0o644, + }) + + writeStream.on('error', reject) + writeStream.on('close', () => resolve()) + + writeStream.end(content) + }) + + logger.info(`[${requestId}] File uploaded successfully to ${remotePath}`) + + return NextResponse.json({ + uploaded: true, + remotePath: remotePath, + size: content.length, + message: `File uploaded successfully to ${remotePath}`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH file upload failed:`, error) + + return NextResponse.json({ error: `SSH file upload failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/ssh/utils.ts b/apps/sim/app/api/tools/ssh/utils.ts new file mode 100644 index 00000000000..4dfa1759ef7 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/utils.ts @@ -0,0 +1,155 @@ +import { type Attributes, Client, type ConnectConfig } from 'ssh2' + +// File type constants from POSIX +const S_IFMT = 0o170000 // bit mask for the file type bit field +const S_IFDIR = 0o040000 // directory +const S_IFREG = 0o100000 // regular file +const S_IFLNK = 0o120000 // symbolic link + +export interface SSHConnectionConfig { + host: string + port: number + username: string + password?: string + privateKey?: string + passphrase?: string + timeout?: number + keepaliveInterval?: number + readyTimeout?: number +} + +export interface SSHCommandResult { + stdout: string + stderr: string + exitCode: number +} + +/** + * Create an SSH connection using the provided configuration + */ +export function createSSHConnection(config: SSHConnectionConfig): Promise { + return new Promise((resolve, reject) => { + const client = new Client() + + const connectConfig: ConnectConfig = { + host: config.host, + port: config.port || 22, + username: config.username, + readyTimeout: config.readyTimeout || 20000, + keepaliveInterval: config.keepaliveInterval || 10000, + } + + // Authentication: prioritize private key over password + if (config.privateKey) { + connectConfig.privateKey = config.privateKey + if (config.passphrase) { + connectConfig.passphrase = config.passphrase + } + } else if (config.password) { + connectConfig.password = config.password + } else { + reject(new Error('Either password or privateKey must be provided')) + return + } + + client.on('ready', () => { + resolve(client) + }) + + client.on('error', (err) => { + reject(err) + }) + + client.connect(connectConfig) + }) +} + +/** + * Execute a command on the SSH connection + */ +export function executeSSHCommand(client: Client, command: string): Promise { + return new Promise((resolve, reject) => { + client.exec(command, (err, stream) => { + if (err) { + reject(err) + return + } + + let stdout = '' + let stderr = '' + + stream.on('close', (code: number) => { + resolve({ + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode: code ?? 0, + }) + }) + + stream.on('data', (data: Buffer) => { + stdout += data.toString() + }) + + stream.stderr.on('data', (data: Buffer) => { + stderr += data.toString() + }) + }) + }) +} + +/** + * Sanitize command input to prevent command injection + */ +export function sanitizeCommand(command: string): string { + // Basic sanitization - trim whitespace + return command.trim() +} + +/** + * Sanitize file path to prevent directory traversal + */ +export function sanitizePath(path: string): string { + // Remove any null bytes + let sanitized = path.replace(/\0/g, '') + + // Normalize the path + sanitized = sanitized.trim() + + return sanitized +} + +/** + * Validate that authentication credentials are provided + */ +export function validateAuth(params: { password?: string; privateKey?: string }): { + isValid: boolean + error?: string +} { + if (!params.password && !params.privateKey) { + return { + isValid: false, + error: 'Either password or privateKey must be provided for authentication', + } + } + return { isValid: true } +} + +/** + * Parse file permissions from octal string + */ +export function parsePermissions(mode: number): string { + return `0${(mode & 0o777).toString(8)}` +} + +/** + * Get file type from attributes mode bits + */ +export function getFileType(attrs: Attributes): 'file' | 'directory' | 'symlink' | 'other' { + const mode = attrs.mode + const fileType = mode & S_IFMT + + if (fileType === S_IFDIR) return 'directory' + if (fileType === S_IFREG) return 'file' + if (fileType === S_IFLNK) return 'symlink' + return 'other' +} diff --git a/apps/sim/app/api/tools/ssh/write-file-content/route.ts b/apps/sim/app/api/tools/ssh/write-file-content/route.ts new file mode 100644 index 00000000000..455ecdf2968 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/write-file-content/route.ts @@ -0,0 +1,152 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import type { Client, SFTPWrapper } from 'ssh2' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils' + +const logger = createLogger('SSHWriteFileContentAPI') + +const WriteFileContentSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive().default(22), + username: z.string().min(1, 'Username is required'), + password: z.string().optional(), + privateKey: z.string().optional(), + passphrase: z.string().optional(), + path: z.string().min(1, 'Path is required'), + content: z.string(), + mode: z.enum(['overwrite', 'append', 'create']).default('overwrite'), + permissions: z.string().optional(), +}) + +function getSFTP(client: Client): Promise { + return new Promise((resolve, reject) => { + client.sftp((err, sftp) => { + if (err) { + reject(err) + } else { + resolve(sftp) + } + }) + }) +} + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = WriteFileContentSchema.parse(body) + + // Validate authentication + if (!params.password && !params.privateKey) { + return NextResponse.json( + { error: 'Either password or privateKey must be provided' }, + { status: 400 } + ) + } + + logger.info( + `[${requestId}] Writing file content to ${params.path} on ${params.host}:${params.port} (mode: ${params.mode})` + ) + + const client = await createSSHConnection({ + host: params.host, + port: params.port, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }) + + try { + const sftp = await getSFTP(client) + const filePath = sanitizePath(params.path) + + // Check if file exists for 'create' mode + if (params.mode === 'create') { + const exists = await new Promise((resolve) => { + sftp.stat(filePath, (err) => { + resolve(!err) + }) + }) + + if (exists) { + return NextResponse.json( + { error: `File already exists and mode is 'create': ${filePath}` }, + { status: 409 } + ) + } + } + + // Handle append mode by reading existing content first + let finalContent = params.content + if (params.mode === 'append') { + const existingContent = await new Promise((resolve) => { + const chunks: Buffer[] = [] + const readStream = sftp.createReadStream(filePath) + + readStream.on('data', (chunk: Buffer) => { + chunks.push(chunk) + }) + + readStream.on('end', () => { + resolve(Buffer.concat(chunks).toString('utf-8')) + }) + + readStream.on('error', () => { + resolve('') + }) + }) + finalContent = existingContent + params.content + } + + // Write file + const fileMode = params.permissions ? Number.parseInt(params.permissions, 8) : 0o644 + await new Promise((resolve, reject) => { + const writeStream = sftp.createWriteStream(filePath, { mode: fileMode }) + + writeStream.on('error', reject) + writeStream.on('close', () => resolve()) + + writeStream.end(Buffer.from(finalContent, 'utf-8')) + }) + + // Get final file size + const stats = await new Promise<{ size: number }>((resolve, reject) => { + sftp.stat(filePath, (err, stats) => { + if (err) reject(err) + else resolve(stats) + }) + }) + + logger.info(`[${requestId}] File written successfully: ${stats.size} bytes`) + + return NextResponse.json({ + written: true, + path: filePath, + size: stats.size, + message: `File written successfully: ${stats.size} bytes`, + }) + } finally { + client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] SSH write file content failed:`, error) + + return NextResponse.json( + { error: `SSH write file content failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/ahrefs.ts b/apps/sim/blocks/blocks/ahrefs.ts new file mode 100644 index 00000000000..85c62c9689b --- /dev/null +++ b/apps/sim/blocks/blocks/ahrefs.ts @@ -0,0 +1,408 @@ +import { AhRefsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { AhrefsResponse } from '@/tools/ahrefs/types' + +export const AhrefsBlock: BlockConfig = { + type: 'ahrefs', + name: 'Ahrefs', + description: 'SEO analysis with Ahrefs', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Ahrefs SEO tools into your workflow. Analyze domain ratings, backlinks, organic keywords, top pages, and more. Requires an Ahrefs Enterprise plan with API access.', + docsLink: 'https://docs.ahrefs.com/docs/api/reference/introduction', + category: 'tools', + bgColor: '#E0E0E0', + icon: AhRefsIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Domain Rating', id: 'ahrefs_domain_rating' }, + { label: 'Backlinks', id: 'ahrefs_backlinks' }, + { label: 'Backlinks Stats', id: 'ahrefs_backlinks_stats' }, + { label: 'Referring Domains', id: 'ahrefs_referring_domains' }, + { label: 'Organic Keywords', id: 'ahrefs_organic_keywords' }, + { label: 'Top Pages', id: 'ahrefs_top_pages' }, + { label: 'Keyword Overview', id: 'ahrefs_keyword_overview' }, + { label: 'Broken Backlinks', id: 'ahrefs_broken_backlinks' }, + ], + value: () => 'ahrefs_domain_rating', + }, + // Domain Rating operation inputs + { + id: 'target', + title: 'Target Domain', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_domain_rating' }, + required: true, + }, + // Backlinks operation inputs + { + id: 'target', + title: 'Target Domain/URL', + type: 'short-input', + placeholder: 'example.com or https://example.com/page', + condition: { field: 'operation', value: 'ahrefs_backlinks' }, + required: true, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + { label: 'Exact (exact URL)', id: 'exact' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_backlinks' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'ahrefs_backlinks' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'ahrefs_backlinks' }, + }, + // Backlinks Stats operation inputs + { + id: 'target', + title: 'Target Domain/URL', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_backlinks_stats' }, + required: true, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + { label: 'Exact (exact URL)', id: 'exact' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_backlinks_stats' }, + }, + // Referring Domains operation inputs + { + id: 'target', + title: 'Target Domain/URL', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_referring_domains' }, + required: true, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + { label: 'Exact (exact URL)', id: 'exact' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_referring_domains' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'ahrefs_referring_domains' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'ahrefs_referring_domains' }, + }, + // Organic Keywords operation inputs + { + id: 'target', + title: 'Target Domain/URL', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + required: true, + }, + { + id: 'country', + title: 'Country', + type: 'dropdown', + options: [ + { label: 'United States', id: 'us' }, + { label: 'United Kingdom', id: 'gb' }, + { label: 'Germany', id: 'de' }, + { label: 'France', id: 'fr' }, + { label: 'Spain', id: 'es' }, + { label: 'Italy', id: 'it' }, + { label: 'Canada', id: 'ca' }, + { label: 'Australia', id: 'au' }, + { label: 'Japan', id: 'jp' }, + { label: 'Brazil', id: 'br' }, + { label: 'India', id: 'in' }, + { label: 'Netherlands', id: 'nl' }, + { label: 'Poland', id: 'pl' }, + { label: 'Russia', id: 'ru' }, + { label: 'Mexico', id: 'mx' }, + ], + value: () => 'us', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + { label: 'Exact (exact URL)', id: 'exact' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + }, + // Top Pages operation inputs + { + id: 'target', + title: 'Target Domain', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + required: true, + }, + { + id: 'country', + title: 'Country', + type: 'dropdown', + options: [ + { label: 'United States', id: 'us' }, + { label: 'United Kingdom', id: 'gb' }, + { label: 'Germany', id: 'de' }, + { label: 'France', id: 'fr' }, + { label: 'Spain', id: 'es' }, + { label: 'Italy', id: 'it' }, + { label: 'Canada', id: 'ca' }, + { label: 'Australia', id: 'au' }, + { label: 'Japan', id: 'jp' }, + { label: 'Brazil', id: 'br' }, + { label: 'India', id: 'in' }, + { label: 'Netherlands', id: 'nl' }, + { label: 'Poland', id: 'pl' }, + { label: 'Russia', id: 'ru' }, + { label: 'Mexico', id: 'mx' }, + ], + value: () => 'us', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + }, + // Keyword Overview operation inputs + { + id: 'keyword', + title: 'Keyword', + type: 'short-input', + placeholder: 'Enter keyword to analyze', + condition: { field: 'operation', value: 'ahrefs_keyword_overview' }, + required: true, + }, + { + id: 'country', + title: 'Country', + type: 'dropdown', + options: [ + { label: 'United States', id: 'us' }, + { label: 'United Kingdom', id: 'gb' }, + { label: 'Germany', id: 'de' }, + { label: 'France', id: 'fr' }, + { label: 'Spain', id: 'es' }, + { label: 'Italy', id: 'it' }, + { label: 'Canada', id: 'ca' }, + { label: 'Australia', id: 'au' }, + { label: 'Japan', id: 'jp' }, + { label: 'Brazil', id: 'br' }, + { label: 'India', id: 'in' }, + { label: 'Netherlands', id: 'nl' }, + { label: 'Poland', id: 'pl' }, + { label: 'Russia', id: 'ru' }, + { label: 'Mexico', id: 'mx' }, + ], + value: () => 'us', + condition: { field: 'operation', value: 'ahrefs_keyword_overview' }, + }, + // Broken Backlinks operation inputs + { + id: 'target', + title: 'Target Domain/URL', + type: 'short-input', + placeholder: 'example.com', + condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, + required: true, + }, + { + id: 'mode', + title: 'Analysis Mode', + type: 'dropdown', + options: [ + { label: 'Domain (entire domain)', id: 'domain' }, + { label: 'Prefix (URL prefix)', id: 'prefix' }, + { label: 'Subdomains (include all)', id: 'subdomains' }, + { label: 'Exact (exact URL)', id: 'exact' }, + ], + value: () => 'domain', + condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, + }, + // API Key (common to all operations) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Ahrefs API key', + password: true, + required: true, + }, + ], + tools: { + access: [ + 'ahrefs_domain_rating', + 'ahrefs_backlinks', + 'ahrefs_backlinks_stats', + 'ahrefs_referring_domains', + 'ahrefs_organic_keywords', + 'ahrefs_top_pages', + 'ahrefs_keyword_overview', + 'ahrefs_broken_backlinks', + ], + config: { + tool: (params) => { + // Convert numeric string inputs to numbers + if (params.limit) { + params.limit = Number(params.limit) + } + if (params.offset) { + params.offset = Number(params.offset) + } + + switch (params.operation) { + case 'ahrefs_domain_rating': + return 'ahrefs_domain_rating' + case 'ahrefs_backlinks': + return 'ahrefs_backlinks' + case 'ahrefs_backlinks_stats': + return 'ahrefs_backlinks_stats' + case 'ahrefs_referring_domains': + return 'ahrefs_referring_domains' + case 'ahrefs_organic_keywords': + return 'ahrefs_organic_keywords' + case 'ahrefs_top_pages': + return 'ahrefs_top_pages' + case 'ahrefs_keyword_overview': + return 'ahrefs_keyword_overview' + case 'ahrefs_broken_backlinks': + return 'ahrefs_broken_backlinks' + default: + return 'ahrefs_domain_rating' + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Ahrefs API key' }, + target: { type: 'string', description: 'Target domain or URL to analyze' }, + keyword: { type: 'string', description: 'Keyword to analyze' }, + mode: { type: 'string', description: 'Analysis mode (domain, prefix, subdomains, exact)' }, + country: { type: 'string', description: 'Country code for geo-specific data' }, + limit: { type: 'number', description: 'Maximum number of results to return' }, + offset: { type: 'number', description: 'Number of results to skip for pagination' }, + }, + outputs: { + // Domain Rating output + domainRating: { type: 'number', description: 'Domain Rating score (0-100)' }, + ahrefsRank: { type: 'number', description: 'Ahrefs Rank (global ranking)' }, + // Backlinks output + backlinks: { type: 'json', description: 'List of backlinks' }, + // Backlinks Stats output + stats: { type: 'json', description: 'Backlink statistics' }, + // Referring Domains output + referringDomains: { type: 'json', description: 'List of referring domains' }, + // Organic Keywords output + keywords: { type: 'json', description: 'List of organic keywords' }, + // Top Pages output + pages: { type: 'json', description: 'List of top pages' }, + // Keyword Overview output + overview: { type: 'json', description: 'Keyword metrics overview' }, + // Broken Backlinks output + brokenBacklinks: { type: 'json', description: 'List of broken backlinks' }, + }, +} diff --git a/apps/sim/blocks/blocks/box.ts b/apps/sim/blocks/blocks/box.ts new file mode 100644 index 00000000000..69ec29cd1d0 --- /dev/null +++ b/apps/sim/blocks/blocks/box.ts @@ -0,0 +1,513 @@ +import { BoxCompanyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { BoxResponse } from '@/tools/box/types' + +export const BoxBlock: BlockConfig = { + type: 'box', + name: 'Box', + description: 'Manage files and folders in Box cloud storage', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Box into the workflow. Can manage files and folders, search content, create shared links, and collaborate with users. Supports file operations like copy, move, delete, and download.', + docsLink: 'https://docs.sim.ai/tools/box', + category: 'tools', + icon: BoxCompanyIcon, + bgColor: '#0061D5', + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // File Operations + { label: 'Get File Info', id: 'box_get_file', group: 'Files' }, + { label: 'Download File', id: 'box_download_file', group: 'Files' }, + { label: 'Update File', id: 'box_update_file', group: 'Files' }, + { label: 'Copy File', id: 'box_copy_file', group: 'Files' }, + { label: 'Delete File', id: 'box_delete_file', group: 'Files' }, + // Folder Operations + { label: 'List Folder', id: 'box_list_folder', group: 'Folders' }, + { label: 'Create Folder', id: 'box_create_folder', group: 'Folders' }, + { label: 'Delete Folder', id: 'box_delete_folder', group: 'Folders' }, + // Search + { label: 'Search', id: 'box_search', group: 'Search' }, + // Sharing + { label: 'Create Shared Link', id: 'box_create_shared_link', group: 'Sharing' }, + { label: 'Create Collaboration', id: 'box_create_collaboration', group: 'Sharing' }, + // User + { label: 'Get Current User', id: 'box_get_current_user', group: 'User' }, + ], + grouped: true, + }, + // OAuth Credential + { + id: 'credential', + title: 'Box Account', + type: 'oauth-input', + serviceId: 'box', + requiredScopes: ['root_readwrite'], + placeholder: 'Select Box account', + required: true, + }, + // File ID input (for file operations) + { + id: 'fileId', + title: 'File ID', + type: 'short-input', + placeholder: 'Enter file ID', + condition: { + field: 'operation', + value: [ + 'box_get_file', + 'box_download_file', + 'box_update_file', + 'box_copy_file', + 'box_delete_file', + ], + }, + required: { + field: 'operation', + value: [ + 'box_get_file', + 'box_download_file', + 'box_update_file', + 'box_copy_file', + 'box_delete_file', + ], + }, + }, + // Folder ID input (for folder operations) + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'Enter folder ID (0 for root)', + condition: { + field: 'operation', + value: ['box_list_folder', 'box_delete_folder'], + }, + required: { + field: 'operation', + value: ['box_list_folder', 'box_delete_folder'], + }, + }, + // Parent Folder ID (for create folder and copy) + { + id: 'parentFolderId', + title: 'Parent Folder ID', + type: 'short-input', + placeholder: 'Enter parent folder ID (0 for root)', + condition: { + field: 'operation', + value: ['box_create_folder', 'box_copy_file'], + }, + required: { + field: 'operation', + value: ['box_create_folder', 'box_copy_file'], + }, + }, + // Folder Name (for create folder) + { + id: 'folderName', + title: 'Folder Name', + type: 'short-input', + placeholder: 'Enter folder name', + condition: { field: 'operation', value: 'box_create_folder' }, + required: { field: 'operation', value: 'box_create_folder' }, + }, + // New Name (for update file and copy) + { + id: 'name', + title: 'New Name', + type: 'short-input', + placeholder: 'Enter new name (optional)', + condition: { + field: 'operation', + value: ['box_update_file', 'box_copy_file'], + }, + }, + // Description (for update file) + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Enter description (optional)', + condition: { field: 'operation', value: 'box_update_file' }, + }, + // Parent ID for move (update file) + { + id: 'parentId', + title: 'Move to Folder ID', + type: 'short-input', + placeholder: 'Enter destination folder ID to move file', + condition: { field: 'operation', value: 'box_update_file' }, + }, + // Tags (for update file) + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'Comma-separated tags (optional)', + condition: { field: 'operation', value: 'box_update_file' }, + }, + // Search Query + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Enter search query', + condition: { field: 'operation', value: 'box_search' }, + required: { field: 'operation', value: 'box_search' }, + }, + // Search Scope + { + id: 'scope', + title: 'Search Scope', + type: 'dropdown', + options: [ + { label: 'User Content', id: 'user_content' }, + { label: 'Enterprise Content', id: 'enterprise_content' }, + ], + condition: { field: 'operation', value: 'box_search' }, + }, + // File Extensions filter + { + id: 'fileExtensions', + title: 'File Extensions', + type: 'short-input', + placeholder: 'e.g., pdf,docx,xlsx', + condition: { field: 'operation', value: 'box_search' }, + }, + // Limit + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max results (default: 30)', + condition: { + field: 'operation', + value: ['box_list_folder', 'box_search'], + }, + }, + // Offset + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: 'Offset for pagination', + condition: { + field: 'operation', + value: ['box_list_folder', 'box_search'], + }, + }, + // Recursive delete (for folder) + { + id: 'recursive', + title: 'Delete Contents', + type: 'switch', + defaultValue: true, + condition: { field: 'operation', value: 'box_delete_folder' }, + }, + // Permanent delete (for file) + { + id: 'permanent', + title: 'Permanent Delete', + type: 'switch', + defaultValue: false, + condition: { field: 'operation', value: 'box_delete_file' }, + }, + // Shared Link - Item ID + { + id: 'itemId', + title: 'Item ID', + type: 'short-input', + placeholder: 'Enter file or folder ID', + condition: { + field: 'operation', + value: ['box_create_shared_link', 'box_create_collaboration'], + }, + required: { + field: 'operation', + value: ['box_create_shared_link', 'box_create_collaboration'], + }, + }, + // Item Type (for shared link and collaboration) + { + id: 'itemType', + title: 'Item Type', + type: 'dropdown', + options: [ + { label: 'File', id: 'file' }, + { label: 'Folder', id: 'folder' }, + ], + condition: { + field: 'operation', + value: ['box_create_shared_link', 'box_create_collaboration'], + }, + required: { + field: 'operation', + value: ['box_create_shared_link', 'box_create_collaboration'], + }, + }, + // Shared Link Access Level + { + id: 'access', + title: 'Access Level', + type: 'dropdown', + options: [ + { label: 'Open (Anyone with link)', id: 'open' }, + { label: 'Company (Only company members)', id: 'company' }, + { label: 'Collaborators Only', id: 'collaborators' }, + ], + condition: { field: 'operation', value: 'box_create_shared_link' }, + }, + // Shared Link Password + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'Optional password protection', + password: true, + condition: { field: 'operation', value: 'box_create_shared_link' }, + }, + // Shared Link Expiration + { + id: 'unsharedAt', + title: 'Expiration Date', + type: 'short-input', + placeholder: 'ISO 8601 date (e.g., 2024-12-31)', + condition: { field: 'operation', value: 'box_create_shared_link' }, + }, + // Can Download + { + id: 'canDownload', + title: 'Allow Download', + type: 'switch', + defaultValue: true, + condition: { field: 'operation', value: 'box_create_shared_link' }, + }, + // Can Preview + { + id: 'canPreview', + title: 'Allow Preview', + type: 'switch', + defaultValue: true, + condition: { field: 'operation', value: 'box_create_shared_link' }, + }, + // Collaboration - Email + { + id: 'accessibleByLogin', + title: 'User Email', + type: 'short-input', + placeholder: 'Enter user email to share with', + condition: { field: 'operation', value: 'box_create_collaboration' }, + }, + // Collaboration - Type + { + id: 'accessibleByType', + title: 'Collaborator Type', + type: 'dropdown', + options: [ + { label: 'User', id: 'user' }, + { label: 'Group', id: 'group' }, + ], + condition: { field: 'operation', value: 'box_create_collaboration' }, + required: { field: 'operation', value: 'box_create_collaboration' }, + }, + // Collaboration - Role + { + id: 'role', + title: 'Role', + type: 'dropdown', + options: [ + { label: 'Editor', id: 'editor' }, + { label: 'Viewer', id: 'viewer' }, + { label: 'Previewer', id: 'previewer' }, + { label: 'Uploader', id: 'uploader' }, + { label: 'Co-Owner', id: 'co-owner' }, + ], + condition: { field: 'operation', value: 'box_create_collaboration' }, + required: { field: 'operation', value: 'box_create_collaboration' }, + }, + ], + tools: { + access: [ + 'box_get_file', + 'box_list_folder', + 'box_create_folder', + 'box_delete_file', + 'box_delete_folder', + 'box_copy_file', + 'box_update_file', + 'box_search', + 'box_download_file', + 'box_get_current_user', + 'box_create_shared_link', + 'box_create_collaboration', + ], + config: { + tool: (params) => params.operation || 'box_list_folder', + params: (params) => { + const baseParams: Record = {} + + switch (params.operation) { + case 'box_get_file': + case 'box_download_file': + return { fileId: params.fileId } + + case 'box_update_file': + return { + fileId: params.fileId, + name: params.name || undefined, + description: params.description || undefined, + parentId: params.parentId || undefined, + tags: params.tags ? params.tags.split(',').map((t: string) => t.trim()) : undefined, + } + + case 'box_copy_file': + return { + fileId: params.fileId, + parentFolderId: params.parentFolderId, + newName: params.name || undefined, + } + + case 'box_delete_file': + return { + fileId: params.fileId, + permanent: params.permanent || false, + } + + case 'box_list_folder': + return { + folderId: params.folderId || '0', + limit: params.limit ? Number(params.limit) : undefined, + offset: params.offset ? Number(params.offset) : undefined, + } + + case 'box_create_folder': + return { + parentFolderId: params.parentFolderId || '0', + folderName: params.folderName, + } + + case 'box_delete_folder': + return { + folderId: params.folderId, + recursive: params.recursive !== false, + } + + case 'box_search': + return { + query: params.query, + scope: params.scope || undefined, + fileExtensions: params.fileExtensions || undefined, + limit: params.limit ? Number(params.limit) : undefined, + offset: params.offset ? Number(params.offset) : undefined, + } + + case 'box_get_current_user': + return {} + + case 'box_create_shared_link': + return { + itemId: params.itemId, + itemType: params.itemType, + access: params.access || undefined, + password: params.password || undefined, + unsharedAt: params.unsharedAt || undefined, + canDownload: params.canDownload, + canPreview: params.canPreview, + } + + case 'box_create_collaboration': + return { + itemId: params.itemId, + itemType: params.itemType, + accessibleByLogin: params.accessibleByLogin || undefined, + accessibleByType: params.accessibleByType, + role: params.role, + } + + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'The Box operation to perform' }, + credential: { type: 'string', description: 'Box OAuth credential' }, + fileId: { type: 'string', description: 'File ID' }, + folderId: { type: 'string', description: 'Folder ID' }, + parentFolderId: { type: 'string', description: 'Parent folder ID' }, + folderName: { type: 'string', description: 'Folder name' }, + name: { type: 'string', description: 'New name for file/folder' }, + description: { type: 'string', description: 'Description' }, + parentId: { type: 'string', description: 'Destination folder ID for move' }, + query: { type: 'string', description: 'Search query' }, + scope: { type: 'string', description: 'Search scope' }, + fileExtensions: { type: 'string', description: 'File extensions filter' }, + limit: { type: 'number', description: 'Maximum results' }, + offset: { type: 'number', description: 'Pagination offset' }, + recursive: { type: 'boolean', description: 'Delete folder contents recursively' }, + permanent: { type: 'boolean', description: 'Permanently delete file' }, + itemId: { type: 'string', description: 'Item ID for sharing' }, + itemType: { type: 'string', description: 'Item type (file or folder)' }, + access: { type: 'string', description: 'Shared link access level' }, + password: { type: 'string', description: 'Shared link password' }, + unsharedAt: { type: 'string', description: 'Shared link expiration date' }, + canDownload: { type: 'boolean', description: 'Allow download via shared link' }, + canPreview: { type: 'boolean', description: 'Allow preview via shared link' }, + tags: { type: 'string', description: 'Comma-separated tags for file' }, + accessibleByLogin: { type: 'string', description: 'Email of user to share with' }, + accessibleByType: { type: 'string', description: 'Type of collaborator' }, + role: { type: 'string', description: 'Collaboration role' }, + }, + outputs: { + file: { + type: 'json', + description: 'File information', + }, + folder: { + type: 'json', + description: 'Folder information', + }, + items: { + type: 'json', + description: 'List of files and folders', + }, + results: { + type: 'json', + description: 'Search results', + }, + sharedLink: { + type: 'json', + description: 'Shared link information', + }, + collaboration: { + type: 'json', + description: 'Collaboration information', + }, + user: { + type: 'json', + description: 'User information', + }, + success: { + type: 'boolean', + description: 'Whether the operation was successful', + }, + totalCount: { + type: 'number', + description: 'Total count of items', + }, + downloadUrl: { + type: 'string', + description: 'Download URL for file', + }, + content: { + type: 'string', + description: 'File content (for text files)', + }, + }, +} diff --git a/apps/sim/blocks/blocks/datadog.ts b/apps/sim/blocks/blocks/datadog.ts new file mode 100644 index 00000000000..8a713dd398b --- /dev/null +++ b/apps/sim/blocks/blocks/datadog.ts @@ -0,0 +1,560 @@ +import { DatadogIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { DatadogResponse } from '@/tools/datadog/types' + +export const DatadogBlock: BlockConfig = { + type: 'datadog', + name: 'Datadog', + description: 'Monitor infrastructure, applications, and logs with Datadog', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Datadog monitoring into workflows. Submit metrics, manage monitors, query logs, create events, handle downtimes, and more.', + docsLink: 'https://docs.sim.ai/tools/datadog', + category: 'tools', + bgColor: '#632CA6', + icon: DatadogIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Submit Metrics', id: 'datadog_submit_metrics' }, + { label: 'Query Timeseries', id: 'datadog_query_timeseries' }, + { label: 'Create Event', id: 'datadog_create_event' }, + { label: 'Create Monitor', id: 'datadog_create_monitor' }, + { label: 'Get Monitor', id: 'datadog_get_monitor' }, + { label: 'List Monitors', id: 'datadog_list_monitors' }, + { label: 'Mute Monitor', id: 'datadog_mute_monitor' }, + { label: 'Query Logs', id: 'datadog_query_logs' }, + { label: 'Send Logs', id: 'datadog_send_logs' }, + { label: 'Create Downtime', id: 'datadog_create_downtime' }, + { label: 'List Downtimes', id: 'datadog_list_downtimes' }, + { label: 'Cancel Downtime', id: 'datadog_cancel_downtime' }, + ], + value: () => 'datadog_submit_metrics', + }, + + // ======================== + // Submit Metrics inputs + // ======================== + { + id: 'series', + title: 'Metrics Data (JSON)', + type: 'code', + placeholder: `[ + { + "metric": "custom.app.response_time", + "type": "gauge", + "points": [{"timestamp": ${Math.floor(Date.now() / 1000)}, "value": 0.85}], + "tags": ["env:production", "service:api"] + } +]`, + condition: { field: 'operation', value: 'datadog_submit_metrics' }, + required: true, + }, + + // ======================== + // Query Timeseries inputs + // ======================== + { + id: 'query', + title: 'Query', + type: 'long-input', + placeholder: 'avg:system.cpu.user{*}', + condition: { field: 'operation', value: 'datadog_query_timeseries' }, + required: true, + }, + { + id: 'from', + title: 'From (Unix Timestamp)', + type: 'short-input', + placeholder: 'e.g., 1701360000', + condition: { field: 'operation', value: 'datadog_query_timeseries' }, + required: true, + }, + { + id: 'to', + title: 'To (Unix Timestamp)', + type: 'short-input', + placeholder: 'e.g., 1701446400', + condition: { field: 'operation', value: 'datadog_query_timeseries' }, + required: true, + }, + + // ======================== + // Create Event inputs + // ======================== + { + id: 'title', + title: 'Event Title', + type: 'short-input', + placeholder: 'Deployment completed', + condition: { field: 'operation', value: 'datadog_create_event' }, + required: true, + }, + { + id: 'text', + title: 'Event Text', + type: 'long-input', + placeholder: 'Describe the event...', + condition: { field: 'operation', value: 'datadog_create_event' }, + required: true, + }, + { + id: 'alertType', + title: 'Alert Type', + type: 'dropdown', + options: [ + { label: 'Info', id: 'info' }, + { label: 'Success', id: 'success' }, + { label: 'Warning', id: 'warning' }, + { label: 'Error', id: 'error' }, + ], + value: () => 'info', + condition: { field: 'operation', value: 'datadog_create_event' }, + }, + { + id: 'priority', + title: 'Priority', + type: 'dropdown', + options: [ + { label: 'Normal', id: 'normal' }, + { label: 'Low', id: 'low' }, + ], + value: () => 'normal', + condition: { field: 'operation', value: 'datadog_create_event' }, + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'env:production, service:api', + condition: { field: 'operation', value: 'datadog_create_event' }, + }, + + // ======================== + // Create Monitor inputs + // ======================== + { + id: 'name', + title: 'Monitor Name', + type: 'short-input', + placeholder: 'High CPU Usage Alert', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + required: true, + }, + { + id: 'type', + title: 'Monitor Type', + type: 'dropdown', + options: [ + { label: 'Metric Alert', id: 'metric alert' }, + { label: 'Service Check', id: 'service check' }, + { label: 'Event Alert', id: 'event alert' }, + { label: 'Log Alert', id: 'log alert' }, + { label: 'Query Alert', id: 'query alert' }, + { label: 'Composite', id: 'composite' }, + { label: 'SLO Alert', id: 'slo alert' }, + ], + value: () => 'metric alert', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + required: true, + }, + { + id: 'monitorQuery', + title: 'Monitor Query', + type: 'long-input', + placeholder: 'avg(last_5m):avg:system.cpu.idle{*} < 20', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + required: true, + }, + { + id: 'message', + title: 'Notification Message', + type: 'long-input', + placeholder: 'Alert! CPU usage is high. @slack-alerts', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + }, + { + id: 'monitorTags', + title: 'Tags', + type: 'short-input', + placeholder: 'team:backend, priority:high', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + }, + { + id: 'monitorPriority', + title: 'Priority (1-5)', + type: 'short-input', + placeholder: '3', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + }, + { + id: 'options', + title: 'Options (JSON)', + type: 'code', + placeholder: '{"notify_no_data": true, "thresholds": {"critical": 90}}', + condition: { field: 'operation', value: 'datadog_create_monitor' }, + }, + + // ======================== + // Get Monitor inputs + // ======================== + { + id: 'monitorId', + title: 'Monitor ID', + type: 'short-input', + placeholder: '12345678', + condition: { field: 'operation', value: 'datadog_get_monitor' }, + required: true, + }, + + // ======================== + // List Monitors inputs + // ======================== + { + id: 'listMonitorName', + title: 'Filter by Name', + type: 'short-input', + placeholder: 'CPU', + condition: { field: 'operation', value: 'datadog_list_monitors' }, + }, + { + id: 'listMonitorTags', + title: 'Filter by Tags', + type: 'short-input', + placeholder: 'env:production', + condition: { field: 'operation', value: 'datadog_list_monitors' }, + }, + + // ======================== + // Mute Monitor inputs + // ======================== + { + id: 'muteMonitorId', + title: 'Monitor ID', + type: 'short-input', + placeholder: '12345678', + condition: { field: 'operation', value: 'datadog_mute_monitor' }, + required: true, + }, + { + id: 'scope', + title: 'Scope', + type: 'short-input', + placeholder: 'host:myhost (optional)', + condition: { field: 'operation', value: 'datadog_mute_monitor' }, + }, + { + id: 'end', + title: 'End Time (Unix Timestamp)', + type: 'short-input', + placeholder: 'Leave empty for indefinite', + condition: { field: 'operation', value: 'datadog_mute_monitor' }, + }, + + // ======================== + // Query Logs inputs + // ======================== + { + id: 'logQuery', + title: 'Search Query', + type: 'long-input', + placeholder: 'service:web-app status:error', + condition: { field: 'operation', value: 'datadog_query_logs' }, + required: true, + }, + { + id: 'logFrom', + title: 'From', + type: 'short-input', + placeholder: 'now-1h', + condition: { field: 'operation', value: 'datadog_query_logs' }, + required: true, + }, + { + id: 'logTo', + title: 'To', + type: 'short-input', + placeholder: 'now', + condition: { field: 'operation', value: 'datadog_query_logs' }, + required: true, + }, + { + id: 'logLimit', + title: 'Limit', + type: 'short-input', + placeholder: '50', + condition: { field: 'operation', value: 'datadog_query_logs' }, + }, + + // ======================== + // Send Logs inputs + // ======================== + { + id: 'logs', + title: 'Logs (JSON)', + type: 'code', + placeholder: `[ + { + "message": "Application started successfully", + "service": "my-app", + "ddsource": "custom", + "ddtags": "env:production" + } +]`, + condition: { field: 'operation', value: 'datadog_send_logs' }, + required: true, + }, + + // ======================== + // Create Downtime inputs + // ======================== + { + id: 'downtimeScope', + title: 'Scope', + type: 'short-input', + placeholder: 'host:myhost or env:production or *', + condition: { field: 'operation', value: 'datadog_create_downtime' }, + required: true, + }, + { + id: 'downtimeMessage', + title: 'Message', + type: 'long-input', + placeholder: 'Scheduled maintenance', + condition: { field: 'operation', value: 'datadog_create_downtime' }, + }, + { + id: 'downtimeStart', + title: 'Start Time (Unix Timestamp)', + type: 'short-input', + placeholder: 'Leave empty for now', + condition: { field: 'operation', value: 'datadog_create_downtime' }, + }, + { + id: 'downtimeEnd', + title: 'End Time (Unix Timestamp)', + type: 'short-input', + placeholder: 'e.g., 1701450000', + condition: { field: 'operation', value: 'datadog_create_downtime' }, + }, + { + id: 'downtimeMonitorId', + title: 'Monitor ID (optional)', + type: 'short-input', + placeholder: '12345678', + condition: { field: 'operation', value: 'datadog_create_downtime' }, + }, + + // ======================== + // List Downtimes inputs + // ======================== + { + id: 'currentOnly', + title: 'Current Only', + type: 'switch', + condition: { field: 'operation', value: 'datadog_list_downtimes' }, + }, + + // ======================== + // Cancel Downtime inputs + // ======================== + { + id: 'downtimeId', + title: 'Downtime ID', + type: 'short-input', + placeholder: 'abc123', + condition: { field: 'operation', value: 'datadog_cancel_downtime' }, + required: true, + }, + + // ======================== + // Authentication (common) + // ======================== + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Datadog API key', + password: true, + required: true, + }, + { + id: 'applicationKey', + title: 'Application Key', + type: 'short-input', + placeholder: 'Enter your Datadog application key', + password: true, + required: true, + }, + { + id: 'site', + title: 'Datadog Site', + type: 'dropdown', + options: [ + { label: 'US1 (datadoghq.com)', id: 'datadoghq.com' }, + { label: 'US3 (us3.datadoghq.com)', id: 'us3.datadoghq.com' }, + { label: 'US5 (us5.datadoghq.com)', id: 'us5.datadoghq.com' }, + { label: 'EU (datadoghq.eu)', id: 'datadoghq.eu' }, + { label: 'AP1 (ap1.datadoghq.com)', id: 'ap1.datadoghq.com' }, + { label: 'US1-FED (ddog-gov.com)', id: 'ddog-gov.com' }, + ], + value: () => 'datadoghq.com', + }, + ], + tools: { + access: [ + 'datadog_submit_metrics', + 'datadog_query_timeseries', + 'datadog_create_event', + 'datadog_create_monitor', + 'datadog_get_monitor', + 'datadog_list_monitors', + 'datadog_mute_monitor', + 'datadog_query_logs', + 'datadog_send_logs', + 'datadog_create_downtime', + 'datadog_list_downtimes', + 'datadog_cancel_downtime', + ], + config: { + tool: (params) => { + // Map the input field IDs to the expected tool parameter names + const mappedParams = { ...params } + + // Handle monitor operations + if (params.monitorQuery) { + mappedParams.query = params.monitorQuery + } + if (params.monitorTags) { + mappedParams.tags = params.monitorTags + } + if (params.monitorPriority) { + mappedParams.priority = Number(params.monitorPriority) + } + if (params.muteMonitorId) { + mappedParams.monitorId = params.muteMonitorId + } + + // Handle log operations + if (params.logQuery) { + mappedParams.query = params.logQuery + } + if (params.logFrom) { + mappedParams.from = params.logFrom + } + if (params.logTo) { + mappedParams.to = params.logTo + } + if (params.logLimit) { + mappedParams.limit = Number(params.logLimit) + } + + // Handle downtime operations + if (params.downtimeScope) { + mappedParams.scope = params.downtimeScope + } + if (params.downtimeMessage) { + mappedParams.message = params.downtimeMessage + } + if (params.downtimeStart) { + mappedParams.start = Number(params.downtimeStart) + } + if (params.downtimeEnd) { + mappedParams.end = Number(params.downtimeEnd) + } + if (params.downtimeMonitorId) { + mappedParams.monitorId = params.downtimeMonitorId + } + + // Handle list monitors + if (params.listMonitorName) { + mappedParams.name = params.listMonitorName + } + if (params.listMonitorTags) { + mappedParams.tags = params.listMonitorTags + } + + // Convert numeric fields + if (params.from) { + mappedParams.from = Number(params.from) + } + if (params.to) { + mappedParams.to = Number(params.to) + } + if (params.end) { + mappedParams.end = Number(params.end) + } + + return params.operation + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Datadog API key' }, + applicationKey: { type: 'string', description: 'Datadog Application key' }, + site: { type: 'string', description: 'Datadog site/region' }, + // Metrics + series: { type: 'json', description: 'Metrics data to submit' }, + query: { type: 'string', description: 'Query string' }, + from: { type: 'number', description: 'Start time (Unix timestamp)' }, + to: { type: 'number', description: 'End time (Unix timestamp)' }, + // Events + title: { type: 'string', description: 'Event title' }, + text: { type: 'string', description: 'Event text/body' }, + alertType: { type: 'string', description: 'Alert type' }, + priority: { type: 'string', description: 'Priority level' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + // Monitors + name: { type: 'string', description: 'Monitor name' }, + type: { type: 'string', description: 'Monitor type' }, + monitorQuery: { type: 'string', description: 'Monitor query' }, + message: { type: 'string', description: 'Notification message' }, + monitorTags: { type: 'string', description: 'Monitor tags' }, + monitorPriority: { type: 'number', description: 'Monitor priority (1-5)' }, + options: { type: 'json', description: 'Monitor options' }, + monitorId: { type: 'string', description: 'Monitor ID' }, + muteMonitorId: { type: 'string', description: 'Monitor ID to mute' }, + scope: { type: 'string', description: 'Scope for muting' }, + end: { type: 'number', description: 'End time for mute' }, + // Logs + logQuery: { type: 'string', description: 'Log search query' }, + logFrom: { type: 'string', description: 'Log start time' }, + logTo: { type: 'string', description: 'Log end time' }, + logLimit: { type: 'number', description: 'Max logs to return' }, + logs: { type: 'json', description: 'Logs to send' }, + // Downtimes + downtimeScope: { type: 'string', description: 'Downtime scope' }, + downtimeMessage: { type: 'string', description: 'Downtime message' }, + downtimeStart: { type: 'number', description: 'Downtime start time' }, + downtimeEnd: { type: 'number', description: 'Downtime end time' }, + downtimeMonitorId: { type: 'string', description: 'Monitor ID for downtime' }, + currentOnly: { type: 'boolean', description: 'Filter to current downtimes' }, + downtimeId: { type: 'string', description: 'Downtime ID to cancel' }, + listMonitorName: { type: 'string', description: 'Filter monitors by name' }, + listMonitorTags: { type: 'string', description: 'Filter monitors by tags' }, + }, + outputs: { + success: { type: 'boolean', description: 'Whether the operation succeeded' }, + // Metrics + series: { type: 'json', description: 'Timeseries data' }, + status: { type: 'string', description: 'Query status' }, + // Events + event: { type: 'json', description: 'Event data' }, + events: { type: 'json', description: 'List of events' }, + // Monitors + monitor: { type: 'json', description: 'Monitor data' }, + monitors: { type: 'json', description: 'List of monitors' }, + // Logs + logs: { type: 'json', description: 'Log entries' }, + nextLogId: { type: 'string', description: 'Pagination cursor for logs' }, + // Downtimes + downtime: { type: 'json', description: 'Downtime data' }, + downtimes: { type: 'json', description: 'List of downtimes' }, + }, +} diff --git a/apps/sim/blocks/blocks/dropbox.ts b/apps/sim/blocks/blocks/dropbox.ts new file mode 100644 index 00000000000..459f1b8f38c --- /dev/null +++ b/apps/sim/blocks/blocks/dropbox.ts @@ -0,0 +1,368 @@ +import { DropboxIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { DropboxResponse } from '@/tools/dropbox/types' + +export const DropboxBlock: BlockConfig = { + type: 'dropbox', + name: 'Dropbox', + description: 'Upload, download, share, and manage files in Dropbox', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Dropbox into your workflow for file management, sharing, and collaboration. Upload files, download content, create folders, manage shared links, and more.', + docsLink: 'https://docs.sim.ai/tools/dropbox', + category: 'tools', + icon: DropboxIcon, + bgColor: '#0061FF', + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Upload File', id: 'dropbox_upload' }, + { label: 'Download File', id: 'dropbox_download' }, + { label: 'List Folder', id: 'dropbox_list_folder' }, + { label: 'Create Folder', id: 'dropbox_create_folder' }, + { label: 'Delete File/Folder', id: 'dropbox_delete' }, + { label: 'Copy File/Folder', id: 'dropbox_copy' }, + { label: 'Move File/Folder', id: 'dropbox_move' }, + { label: 'Get Metadata', id: 'dropbox_get_metadata' }, + { label: 'Create Shared Link', id: 'dropbox_create_shared_link' }, + { label: 'Search Files', id: 'dropbox_search' }, + ], + value: () => 'dropbox_upload', + }, + { + id: 'credential', + title: 'Dropbox Account', + type: 'oauth-input', + serviceId: 'dropbox', + requiredScopes: [ + 'account_info.read', + 'files.metadata.read', + 'files.metadata.write', + 'files.content.read', + 'files.content.write', + 'sharing.read', + 'sharing.write', + ], + placeholder: 'Select Dropbox account', + required: true, + }, + // Upload operation inputs + { + id: 'path', + title: 'Destination Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_upload' }, + required: true, + }, + { + id: 'fileContent', + title: 'File Content', + type: 'long-input', + placeholder: 'Base64 encoded file content or file reference', + condition: { field: 'operation', value: 'dropbox_upload' }, + required: true, + }, + { + id: 'mode', + title: 'Write Mode', + type: 'dropdown', + options: [ + { label: 'Add (create new)', id: 'add' }, + { label: 'Overwrite (replace existing)', id: 'overwrite' }, + ], + value: () => 'add', + condition: { field: 'operation', value: 'dropbox_upload' }, + }, + { + id: 'autorename', + title: 'Auto-rename on Conflict', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_upload' }, + }, + // Download operation inputs + { + id: 'path', + title: 'File Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_download' }, + required: true, + }, + // List folder operation inputs + { + id: 'path', + title: 'Folder Path', + type: 'short-input', + placeholder: '/ (root) or /folder', + condition: { field: 'operation', value: 'dropbox_list_folder' }, + required: true, + }, + { + id: 'recursive', + title: 'List Recursively', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_list_folder' }, + }, + { + id: 'limit', + title: 'Maximum Results', + type: 'short-input', + placeholder: '500', + condition: { field: 'operation', value: 'dropbox_list_folder' }, + }, + // Create folder operation inputs + { + id: 'path', + title: 'Folder Path', + type: 'short-input', + placeholder: '/new-folder', + condition: { field: 'operation', value: 'dropbox_create_folder' }, + required: true, + }, + { + id: 'autorename', + title: 'Auto-rename on Conflict', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_create_folder' }, + }, + // Delete operation inputs + { + id: 'path', + title: 'Path to Delete', + type: 'short-input', + placeholder: '/folder/file.txt', + condition: { field: 'operation', value: 'dropbox_delete' }, + required: true, + }, + // Copy operation inputs + { + id: 'fromPath', + title: 'Source Path', + type: 'short-input', + placeholder: '/source/document.pdf', + condition: { field: 'operation', value: 'dropbox_copy' }, + required: true, + }, + { + id: 'toPath', + title: 'Destination Path', + type: 'short-input', + placeholder: '/destination/document.pdf', + condition: { field: 'operation', value: 'dropbox_copy' }, + required: true, + }, + { + id: 'autorename', + title: 'Auto-rename on Conflict', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_copy' }, + }, + // Move operation inputs + { + id: 'fromPath', + title: 'Source Path', + type: 'short-input', + placeholder: '/old-location/document.pdf', + condition: { field: 'operation', value: 'dropbox_move' }, + required: true, + }, + { + id: 'toPath', + title: 'Destination Path', + type: 'short-input', + placeholder: '/new-location/document.pdf', + condition: { field: 'operation', value: 'dropbox_move' }, + required: true, + }, + { + id: 'autorename', + title: 'Auto-rename on Conflict', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_move' }, + }, + // Get metadata operation inputs + { + id: 'path', + title: 'File/Folder Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_get_metadata' }, + required: true, + }, + { + id: 'includeMediaInfo', + title: 'Include Media Info', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_get_metadata' }, + }, + // Create shared link operation inputs + { + id: 'path', + title: 'File/Folder Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_create_shared_link' }, + required: true, + }, + { + id: 'requestedVisibility', + title: 'Visibility', + type: 'dropdown', + options: [ + { label: 'Public (anyone with link)', id: 'public' }, + { label: 'Team Only', id: 'team_only' }, + { label: 'Password Protected', id: 'password' }, + ], + value: () => 'public', + condition: { field: 'operation', value: 'dropbox_create_shared_link' }, + }, + { + id: 'linkPassword', + title: 'Link Password', + type: 'short-input', + placeholder: 'Enter password for the link', + password: true, + condition: { field: 'operation', value: 'dropbox_create_shared_link' }, + }, + { + id: 'expires', + title: 'Expiration Date', + type: 'short-input', + placeholder: '2025-12-31T23:59:59Z', + condition: { field: 'operation', value: 'dropbox_create_shared_link' }, + }, + // Search operation inputs + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Enter search term...', + condition: { field: 'operation', value: 'dropbox_search' }, + required: true, + }, + { + id: 'path', + title: 'Search in Folder', + type: 'short-input', + placeholder: '/ (search all) or /folder', + condition: { field: 'operation', value: 'dropbox_search' }, + }, + { + id: 'fileExtensions', + title: 'File Extensions', + type: 'short-input', + placeholder: 'pdf,xlsx,docx (comma-separated)', + condition: { field: 'operation', value: 'dropbox_search' }, + }, + { + id: 'maxResults', + title: 'Maximum Results', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'dropbox_search' }, + }, + ], + tools: { + access: [ + 'dropbox_upload', + 'dropbox_download', + 'dropbox_list_folder', + 'dropbox_create_folder', + 'dropbox_delete', + 'dropbox_copy', + 'dropbox_move', + 'dropbox_get_metadata', + 'dropbox_create_shared_link', + 'dropbox_search', + ], + config: { + tool: (params) => { + // Convert numeric params + if (params.limit) { + params.limit = Number(params.limit) + } + if (params.maxResults) { + params.maxResults = Number(params.maxResults) + } + + switch (params.operation) { + case 'dropbox_upload': + return 'dropbox_upload' + case 'dropbox_download': + return 'dropbox_download' + case 'dropbox_list_folder': + return 'dropbox_list_folder' + case 'dropbox_create_folder': + return 'dropbox_create_folder' + case 'dropbox_delete': + return 'dropbox_delete' + case 'dropbox_copy': + return 'dropbox_copy' + case 'dropbox_move': + return 'dropbox_move' + case 'dropbox_get_metadata': + return 'dropbox_get_metadata' + case 'dropbox_create_shared_link': + return 'dropbox_create_shared_link' + case 'dropbox_search': + return 'dropbox_search' + default: + return 'dropbox_upload' + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Dropbox OAuth credential' }, + // Common inputs + path: { type: 'string', description: 'Path in Dropbox' }, + autorename: { type: 'boolean', description: 'Auto-rename on conflict' }, + // Upload inputs + fileContent: { type: 'string', description: 'Base64 encoded file content' }, + fileName: { type: 'string', description: 'Optional filename' }, + mode: { type: 'string', description: 'Write mode: add or overwrite' }, + mute: { type: 'boolean', description: 'Mute notifications' }, + // List folder inputs + recursive: { type: 'boolean', description: 'List recursively' }, + includeDeleted: { type: 'boolean', description: 'Include deleted files' }, + includeMediaInfo: { type: 'boolean', description: 'Include media info' }, + limit: { type: 'number', description: 'Maximum results' }, + // Copy/Move inputs + fromPath: { type: 'string', description: 'Source path' }, + toPath: { type: 'string', description: 'Destination path' }, + // Shared link inputs + requestedVisibility: { type: 'string', description: 'Link visibility' }, + linkPassword: { type: 'string', description: 'Password for the link' }, + expires: { type: 'string', description: 'Expiration date (ISO 8601)' }, + // Search inputs + query: { type: 'string', description: 'Search query' }, + fileExtensions: { type: 'string', description: 'File extensions filter' }, + maxResults: { type: 'number', description: 'Maximum search results' }, + }, + outputs: { + // Upload/Download outputs + file: { type: 'json', description: 'File metadata' }, + content: { type: 'string', description: 'File content (base64)' }, + temporaryLink: { type: 'string', description: 'Temporary download link' }, + // List folder outputs + entries: { type: 'json', description: 'List of files and folders' }, + cursor: { type: 'string', description: 'Pagination cursor' }, + hasMore: { type: 'boolean', description: 'Whether more results exist' }, + // Create folder output + folder: { type: 'json', description: 'Created folder metadata' }, + // Delete output + deleted: { type: 'boolean', description: 'Whether deletion was successful' }, + // Copy/Move/Get metadata output + metadata: { type: 'json', description: 'Item metadata' }, + // Shared link output + sharedLink: { type: 'json', description: 'Shared link details' }, + // Search outputs + matches: { type: 'json', description: 'Search results' }, + }, +} diff --git a/apps/sim/blocks/blocks/elasticsearch.ts b/apps/sim/blocks/blocks/elasticsearch.ts new file mode 100644 index 00000000000..b1279adff2d --- /dev/null +++ b/apps/sim/blocks/blocks/elasticsearch.ts @@ -0,0 +1,431 @@ +import { ElasticsearchIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { ElasticsearchResponse } from '@/tools/elasticsearch/types' + +export const ElasticsearchBlock: BlockConfig = { + type: 'elasticsearch', + name: 'Elasticsearch', + description: 'Search, index, and manage data in Elasticsearch', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Elasticsearch into workflows for powerful search, indexing, and data management. Supports document CRUD operations, advanced search queries, bulk operations, index management, and cluster monitoring. Works with both self-hosted and Elastic Cloud deployments.', + docsLink: 'https://docs.sim.ai/tools/elasticsearch', + category: 'tools', + bgColor: '#E0E0E0', + icon: ElasticsearchIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Document Operations + { label: 'Search', id: 'elasticsearch_search' }, + { label: 'Index Document', id: 'elasticsearch_index_document' }, + { label: 'Get Document', id: 'elasticsearch_get_document' }, + { label: 'Update Document', id: 'elasticsearch_update_document' }, + { label: 'Delete Document', id: 'elasticsearch_delete_document' }, + { label: 'Bulk Operations', id: 'elasticsearch_bulk' }, + { label: 'Count Documents', id: 'elasticsearch_count' }, + // Index Management + { label: 'Create Index', id: 'elasticsearch_create_index' }, + { label: 'Delete Index', id: 'elasticsearch_delete_index' }, + { label: 'Get Index Info', id: 'elasticsearch_get_index' }, + // Cluster Operations + { label: 'Cluster Health', id: 'elasticsearch_cluster_health' }, + { label: 'Cluster Stats', id: 'elasticsearch_cluster_stats' }, + ], + value: () => 'elasticsearch_search', + }, + + // Deployment type + { + id: 'deploymentType', + title: 'Deployment Type', + type: 'dropdown', + options: [ + { label: 'Self-Hosted', id: 'self_hosted' }, + { label: 'Elastic Cloud', id: 'cloud' }, + ], + value: () => 'self_hosted', + }, + + // Self-hosted host + { + id: 'host', + title: 'Elasticsearch Host', + type: 'short-input', + placeholder: 'https://localhost:9200', + required: true, + condition: { field: 'deploymentType', value: 'self_hosted' }, + }, + + // Cloud ID + { + id: 'cloudId', + title: 'Cloud ID', + type: 'short-input', + placeholder: 'deployment-name:base64-encoded-data', + required: true, + condition: { field: 'deploymentType', value: 'cloud' }, + }, + + // Authentication method + { + id: 'authMethod', + title: 'Authentication Method', + type: 'dropdown', + options: [ + { label: 'API Key', id: 'api_key' }, + { label: 'Basic Auth', id: 'basic_auth' }, + ], + value: () => 'api_key', + }, + + // API Key + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter encoded API key', + password: true, + required: true, + condition: { field: 'authMethod', value: 'api_key' }, + }, + + // Username + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'Enter username', + required: true, + condition: { field: 'authMethod', value: 'basic_auth' }, + }, + + // Password + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'Enter password', + password: true, + required: true, + condition: { field: 'authMethod', value: 'basic_auth' }, + }, + + // Index name - for most operations + { + id: 'index', + title: 'Index Name', + type: 'short-input', + placeholder: 'my-index', + required: true, + condition: { + field: 'operation', + value: [ + 'elasticsearch_search', + 'elasticsearch_index_document', + 'elasticsearch_get_document', + 'elasticsearch_update_document', + 'elasticsearch_delete_document', + 'elasticsearch_bulk', + 'elasticsearch_count', + 'elasticsearch_create_index', + 'elasticsearch_delete_index', + 'elasticsearch_get_index', + ], + }, + }, + + // Document ID - for get/update/delete + { + id: 'documentId', + title: 'Document ID', + type: 'short-input', + placeholder: 'unique-document-id', + required: true, + condition: { + field: 'operation', + value: [ + 'elasticsearch_get_document', + 'elasticsearch_update_document', + 'elasticsearch_delete_document', + ], + }, + }, + + // Optional Document ID - for index document + { + id: 'documentId', + title: 'Document ID (optional)', + type: 'short-input', + placeholder: 'Leave empty for auto-generated ID', + condition: { field: 'operation', value: 'elasticsearch_index_document' }, + }, + + // Document body - for index + { + id: 'document', + title: 'Document', + type: 'long-input', + placeholder: '{ "field": "value", "another_field": 123 }', + required: true, + condition: { field: 'operation', value: 'elasticsearch_index_document' }, + }, + + // Document body - for update (partial) + { + id: 'document', + title: 'Partial Document', + type: 'long-input', + placeholder: '{ "field_to_update": "new_value" }', + required: true, + condition: { field: 'operation', value: 'elasticsearch_update_document' }, + }, + + // Search query + { + id: 'query', + title: 'Search Query', + type: 'long-input', + placeholder: '{ "match": { "field": "search term" } }', + condition: { field: 'operation', value: 'elasticsearch_search' }, + }, + + // Count query + { + id: 'query', + title: 'Query (optional)', + type: 'long-input', + placeholder: '{ "match": { "field": "value" } }', + condition: { field: 'operation', value: 'elasticsearch_count' }, + }, + + // Search size + { + id: 'size', + title: 'Number of Results', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'elasticsearch_search' }, + }, + + // Search from (offset) + { + id: 'from', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'elasticsearch_search' }, + }, + + // Sort + { + id: 'sort', + title: 'Sort', + type: 'long-input', + placeholder: '[{ "field": { "order": "asc" } }]', + condition: { field: 'operation', value: 'elasticsearch_search' }, + }, + + // Source includes + { + id: 'sourceIncludes', + title: 'Fields to Include', + type: 'short-input', + placeholder: 'field1, field2 (comma-separated)', + condition: { + field: 'operation', + value: ['elasticsearch_search', 'elasticsearch_get_document'], + }, + }, + + // Source excludes + { + id: 'sourceExcludes', + title: 'Fields to Exclude', + type: 'short-input', + placeholder: 'field1, field2 (comma-separated)', + condition: { + field: 'operation', + value: ['elasticsearch_search', 'elasticsearch_get_document'], + }, + }, + + // Bulk operations + { + id: 'operations', + title: 'Bulk Operations', + type: 'long-input', + placeholder: + '{ "index": { "_index": "my-index", "_id": "1" } }\n{ "field": "value" }\n{ "delete": { "_index": "my-index", "_id": "2" } }', + required: true, + condition: { field: 'operation', value: 'elasticsearch_bulk' }, + }, + + // Index settings + { + id: 'settings', + title: 'Index Settings', + type: 'long-input', + placeholder: '{ "number_of_shards": 1, "number_of_replicas": 1 }', + condition: { field: 'operation', value: 'elasticsearch_create_index' }, + }, + + // Index mappings + { + id: 'mappings', + title: 'Index Mappings', + type: 'long-input', + placeholder: '{ "properties": { "field": { "type": "text" } } }', + condition: { field: 'operation', value: 'elasticsearch_create_index' }, + }, + + // Refresh option + { + id: 'refresh', + title: 'Refresh', + type: 'dropdown', + options: [ + { label: 'Default', id: '' }, + { label: 'Wait For', id: 'wait_for' }, + { label: 'Immediate', id: 'true' }, + { label: 'None', id: 'false' }, + ], + value: () => '', + condition: { + field: 'operation', + value: [ + 'elasticsearch_index_document', + 'elasticsearch_delete_document', + 'elasticsearch_bulk', + ], + }, + }, + + // Cluster health wait for status + { + id: 'waitForStatus', + title: 'Wait for Status', + type: 'dropdown', + options: [ + { label: 'None', id: '' }, + { label: 'Green', id: 'green' }, + { label: 'Yellow', id: 'yellow' }, + { label: 'Red', id: 'red' }, + ], + value: () => '', + condition: { field: 'operation', value: 'elasticsearch_cluster_health' }, + }, + + // Cluster health timeout + { + id: 'timeout', + title: 'Timeout', + type: 'short-input', + placeholder: '30s', + condition: { field: 'operation', value: 'elasticsearch_cluster_health' }, + }, + + // Retry on conflict + { + id: 'retryOnConflict', + title: 'Retry on Conflict', + type: 'short-input', + placeholder: '3', + condition: { field: 'operation', value: 'elasticsearch_update_document' }, + }, + ], + + tools: { + access: [ + 'elasticsearch_search', + 'elasticsearch_index_document', + 'elasticsearch_get_document', + 'elasticsearch_update_document', + 'elasticsearch_delete_document', + 'elasticsearch_bulk', + 'elasticsearch_count', + 'elasticsearch_create_index', + 'elasticsearch_delete_index', + 'elasticsearch_get_index', + 'elasticsearch_cluster_health', + 'elasticsearch_cluster_stats', + ], + config: { + tool: (params) => { + // Convert numeric strings to numbers + if (params.size) { + params.size = Number(params.size) + } + if (params.from) { + params.from = Number(params.from) + } + if (params.retryOnConflict) { + params.retryOnConflict = Number(params.retryOnConflict) + } + + // Return the operation as the tool ID + return params.operation || 'elasticsearch_search' + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + deploymentType: { type: 'string', description: 'self_hosted or cloud' }, + host: { type: 'string', description: 'Elasticsearch host URL' }, + cloudId: { type: 'string', description: 'Elastic Cloud ID' }, + authMethod: { type: 'string', description: 'api_key or basic_auth' }, + apiKey: { type: 'string', description: 'API key for authentication' }, + username: { type: 'string', description: 'Username for basic auth' }, + password: { type: 'string', description: 'Password for basic auth' }, + index: { type: 'string', description: 'Index name' }, + documentId: { type: 'string', description: 'Document ID' }, + document: { type: 'string', description: 'Document body as JSON' }, + query: { type: 'string', description: 'Search query as JSON' }, + size: { type: 'number', description: 'Number of results' }, + from: { type: 'number', description: 'Starting offset' }, + sort: { type: 'string', description: 'Sort specification as JSON' }, + sourceIncludes: { type: 'string', description: 'Fields to include' }, + sourceExcludes: { type: 'string', description: 'Fields to exclude' }, + operations: { type: 'string', description: 'Bulk operations as NDJSON' }, + settings: { type: 'string', description: 'Index settings as JSON' }, + mappings: { type: 'string', description: 'Index mappings as JSON' }, + refresh: { type: 'string', description: 'Refresh policy' }, + waitForStatus: { type: 'string', description: 'Wait for cluster status' }, + timeout: { type: 'string', description: 'Timeout for wait operations' }, + retryOnConflict: { type: 'number', description: 'Retry attempts on conflict' }, + }, + + outputs: { + // Search outputs + hits: { type: 'json', description: 'Search results' }, + took: { type: 'number', description: 'Time taken in milliseconds' }, + timed_out: { type: 'boolean', description: 'Whether the operation timed out' }, + aggregations: { type: 'json', description: 'Aggregation results' }, + // Document outputs + _index: { type: 'string', description: 'Index name' }, + _id: { type: 'string', description: 'Document ID' }, + _version: { type: 'number', description: 'Document version' }, + _source: { type: 'json', description: 'Document content' }, + result: { type: 'string', description: 'Operation result' }, + found: { type: 'boolean', description: 'Whether document was found' }, + // Bulk outputs + errors: { type: 'boolean', description: 'Whether any errors occurred' }, + items: { type: 'json', description: 'Bulk operation results' }, + // Count outputs + count: { type: 'number', description: 'Document count' }, + // Index outputs + acknowledged: { type: 'boolean', description: 'Whether operation was acknowledged' }, + // Cluster outputs + cluster_name: { type: 'string', description: 'Cluster name' }, + status: { type: 'string', description: 'Cluster health status' }, + number_of_nodes: { type: 'number', description: 'Number of nodes' }, + indices: { type: 'json', description: 'Index statistics' }, + nodes: { type: 'json', description: 'Node statistics' }, + }, +} diff --git a/apps/sim/blocks/blocks/gitlab.ts b/apps/sim/blocks/blocks/gitlab.ts new file mode 100644 index 00000000000..350e2e64a86 --- /dev/null +++ b/apps/sim/blocks/blocks/gitlab.ts @@ -0,0 +1,698 @@ +import { GitLabIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GitLabResponse } from '@/tools/gitlab/types' + +export const GitLabBlock: BlockConfig = { + type: 'gitlab', + name: 'GitLab', + description: 'Interact with GitLab projects, issues, merge requests, and pipelines', + authMode: AuthMode.OAuth, + triggerAllowed: false, + longDescription: + 'Integrate GitLab into the workflow. Can manage projects, issues, merge requests, pipelines, and add comments. Supports all core GitLab DevOps operations.', + docsLink: 'https://docs.sim.ai/tools/gitlab', + category: 'tools', + icon: GitLabIcon, + bgColor: '#E0E0E0', + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Project Operations + { label: 'List Projects', id: 'gitlab_list_projects' }, + { label: 'Get Project', id: 'gitlab_get_project' }, + // Issue Operations + { label: 'List Issues', id: 'gitlab_list_issues' }, + { label: 'Get Issue', id: 'gitlab_get_issue' }, + { label: 'Create Issue', id: 'gitlab_create_issue' }, + { label: 'Update Issue', id: 'gitlab_update_issue' }, + { label: 'Delete Issue', id: 'gitlab_delete_issue' }, + { label: 'Add Issue Comment', id: 'gitlab_create_issue_note' }, + // Merge Request Operations + { label: 'List Merge Requests', id: 'gitlab_list_merge_requests' }, + { label: 'Get Merge Request', id: 'gitlab_get_merge_request' }, + { label: 'Create Merge Request', id: 'gitlab_create_merge_request' }, + { label: 'Update Merge Request', id: 'gitlab_update_merge_request' }, + { label: 'Merge Merge Request', id: 'gitlab_merge_merge_request' }, + { label: 'Add MR Comment', id: 'gitlab_create_merge_request_note' }, + // Pipeline Operations + { label: 'List Pipelines', id: 'gitlab_list_pipelines' }, + { label: 'Get Pipeline', id: 'gitlab_get_pipeline' }, + { label: 'Create Pipeline', id: 'gitlab_create_pipeline' }, + { label: 'Retry Pipeline', id: 'gitlab_retry_pipeline' }, + { label: 'Cancel Pipeline', id: 'gitlab_cancel_pipeline' }, + ], + value: () => 'gitlab_list_projects', + }, + { + id: 'credential', + title: 'GitLab Account', + type: 'oauth-input', + serviceId: 'gitlab', + requiredScopes: ['api', 'read_user', 'read_repository', 'write_repository'], + placeholder: 'Select GitLab account', + required: true, + }, + // Project ID (required for most operations) + { + id: 'projectId', + title: 'Project ID', + type: 'short-input', + placeholder: 'Enter project ID or path (e.g., username/project)', + required: true, + condition: { + field: 'operation', + value: [ + 'gitlab_get_project', + 'gitlab_list_issues', + 'gitlab_get_issue', + 'gitlab_create_issue', + 'gitlab_update_issue', + 'gitlab_delete_issue', + 'gitlab_create_issue_note', + 'gitlab_list_merge_requests', + 'gitlab_get_merge_request', + 'gitlab_create_merge_request', + 'gitlab_update_merge_request', + 'gitlab_merge_merge_request', + 'gitlab_create_merge_request_note', + 'gitlab_list_pipelines', + 'gitlab_get_pipeline', + 'gitlab_create_pipeline', + 'gitlab_retry_pipeline', + 'gitlab_cancel_pipeline', + ], + }, + }, + // Issue IID (for issue operations) + { + id: 'issueIid', + title: 'Issue IID', + type: 'short-input', + placeholder: 'Enter issue internal ID (IID)', + required: true, + condition: { + field: 'operation', + value: [ + 'gitlab_get_issue', + 'gitlab_update_issue', + 'gitlab_delete_issue', + 'gitlab_create_issue_note', + ], + }, + }, + // Merge Request IID + { + id: 'mergeRequestIid', + title: 'Merge Request IID', + type: 'short-input', + placeholder: 'Enter merge request internal ID (IID)', + required: true, + condition: { + field: 'operation', + value: [ + 'gitlab_get_merge_request', + 'gitlab_update_merge_request', + 'gitlab_merge_merge_request', + 'gitlab_create_merge_request_note', + ], + }, + }, + // Pipeline ID + { + id: 'pipelineId', + title: 'Pipeline ID', + type: 'short-input', + placeholder: 'Enter pipeline ID', + required: true, + condition: { + field: 'operation', + value: ['gitlab_get_pipeline', 'gitlab_retry_pipeline', 'gitlab_cancel_pipeline'], + }, + }, + // Title (for issue/MR creation) + { + id: 'title', + title: 'Title', + type: 'short-input', + placeholder: 'Enter title', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_issue', 'gitlab_create_merge_request'], + }, + }, + // Description + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Enter description (Markdown supported)', + condition: { + field: 'operation', + value: [ + 'gitlab_create_issue', + 'gitlab_update_issue', + 'gitlab_create_merge_request', + 'gitlab_update_merge_request', + ], + }, + }, + // Comment body + { + id: 'body', + title: 'Comment', + type: 'long-input', + placeholder: 'Enter comment text (Markdown supported)', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_issue_note', 'gitlab_create_merge_request_note'], + }, + }, + // Source branch (for MR creation) + { + id: 'sourceBranch', + title: 'Source Branch', + type: 'short-input', + placeholder: 'Enter source branch name', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_merge_request'], + }, + }, + // Target branch (for MR creation) + { + id: 'targetBranch', + title: 'Target Branch', + type: 'short-input', + placeholder: 'Enter target branch name (e.g., main)', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_merge_request'], + }, + }, + // Ref (for pipeline creation) + { + id: 'ref', + title: 'Branch/Tag', + type: 'short-input', + placeholder: 'Enter branch or tag name', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_pipeline'], + }, + }, + // Labels + { + id: 'labels', + title: 'Labels', + type: 'short-input', + placeholder: 'Enter labels (comma-separated)', + condition: { + field: 'operation', + value: [ + 'gitlab_create_issue', + 'gitlab_update_issue', + 'gitlab_list_issues', + 'gitlab_create_merge_request', + 'gitlab_update_merge_request', + 'gitlab_list_merge_requests', + ], + }, + }, + // Assignee IDs + { + id: 'assigneeIds', + title: 'Assignee IDs', + type: 'short-input', + placeholder: 'Enter assignee user IDs (comma-separated)', + condition: { + field: 'operation', + value: [ + 'gitlab_create_issue', + 'gitlab_update_issue', + 'gitlab_create_merge_request', + 'gitlab_update_merge_request', + ], + }, + }, + // Milestone ID + { + id: 'milestoneId', + title: 'Milestone ID', + type: 'short-input', + placeholder: 'Enter milestone ID', + condition: { + field: 'operation', + value: ['gitlab_create_issue', 'gitlab_update_issue'], + }, + }, + // State filter for issues + { + id: 'issueState', + title: 'State', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Open', id: 'opened' }, + { label: 'Closed', id: 'closed' }, + ], + value: () => 'all', + condition: { + field: 'operation', + value: ['gitlab_list_issues'], + }, + }, + // State filter for merge requests + { + id: 'mrState', + title: 'State', + type: 'dropdown', + options: [ + { label: 'All', id: 'all' }, + { label: 'Open', id: 'opened' }, + { label: 'Closed', id: 'closed' }, + { label: 'Merged', id: 'merged' }, + ], + value: () => 'all', + condition: { + field: 'operation', + value: ['gitlab_list_merge_requests'], + }, + }, + // State event (for updates) + { + id: 'stateEvent', + title: 'State Event', + type: 'dropdown', + options: [ + { label: 'No Change', id: '' }, + { label: 'Close', id: 'close' }, + { label: 'Reopen', id: 'reopen' }, + ], + value: () => '', + condition: { + field: 'operation', + value: ['gitlab_update_issue', 'gitlab_update_merge_request'], + }, + }, + // Pipeline status filter + { + id: 'pipelineStatus', + title: 'Pipeline Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Running', id: 'running' }, + { label: 'Pending', id: 'pending' }, + { label: 'Success', id: 'success' }, + { label: 'Failed', id: 'failed' }, + { label: 'Canceled', id: 'canceled' }, + { label: 'Skipped', id: 'skipped' }, + ], + value: () => '', + condition: { + field: 'operation', + value: ['gitlab_list_pipelines'], + }, + }, + // Remove source branch after merge + { + id: 'removeSourceBranch', + title: 'Remove Source Branch', + type: 'switch', + condition: { + field: 'operation', + value: ['gitlab_create_merge_request', 'gitlab_merge_merge_request'], + }, + }, + // Squash commits + { + id: 'squash', + title: 'Squash Commits', + type: 'switch', + condition: { + field: 'operation', + value: ['gitlab_merge_merge_request'], + }, + }, + // Merge commit message + { + id: 'mergeCommitMessage', + title: 'Merge Commit Message', + type: 'long-input', + placeholder: 'Enter custom merge commit message (optional)', + condition: { + field: 'operation', + value: ['gitlab_merge_merge_request'], + }, + }, + // Per page (pagination) + { + id: 'perPage', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'Number of results per page (default: 20, max: 100)', + condition: { + field: 'operation', + value: [ + 'gitlab_list_projects', + 'gitlab_list_issues', + 'gitlab_list_merge_requests', + 'gitlab_list_pipelines', + ], + }, + }, + // Page number + { + id: 'page', + title: 'Page Number', + type: 'short-input', + placeholder: 'Page number (default: 1)', + condition: { + field: 'operation', + value: [ + 'gitlab_list_projects', + 'gitlab_list_issues', + 'gitlab_list_merge_requests', + 'gitlab_list_pipelines', + ], + }, + }, + ], + tools: { + access: [ + 'gitlab_list_projects', + 'gitlab_get_project', + 'gitlab_list_issues', + 'gitlab_get_issue', + 'gitlab_create_issue', + 'gitlab_update_issue', + 'gitlab_delete_issue', + 'gitlab_create_issue_note', + 'gitlab_list_merge_requests', + 'gitlab_get_merge_request', + 'gitlab_create_merge_request', + 'gitlab_update_merge_request', + 'gitlab_merge_merge_request', + 'gitlab_create_merge_request_note', + 'gitlab_list_pipelines', + 'gitlab_get_pipeline', + 'gitlab_create_pipeline', + 'gitlab_retry_pipeline', + 'gitlab_cancel_pipeline', + ], + config: { + tool: (params) => { + return params.operation || 'gitlab_list_projects' + }, + params: (params) => { + const baseParams: Record = { + credential: params.credential, + } + + switch (params.operation) { + case 'gitlab_list_projects': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + } + + case 'gitlab_get_project': + if (!params.projectId?.trim()) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + } + + case 'gitlab_list_issues': + if (!params.projectId?.trim()) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + state: params.issueState !== 'all' ? params.issueState : undefined, + labels: params.labels?.trim() || undefined, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + } + + case 'gitlab_get_issue': + if (!params.projectId?.trim() || !params.issueIid) { + throw new Error('Project ID and Issue IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + issueIid: Number(params.issueIid), + } + + case 'gitlab_create_issue': + if (!params.projectId?.trim() || !params.title?.trim()) { + throw new Error('Project ID and title are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + title: params.title.trim(), + description: params.description?.trim() || undefined, + labels: params.labels?.trim() || undefined, + assigneeIds: params.assigneeIds + ? params.assigneeIds.split(',').map((id: string) => Number(id.trim())) + : undefined, + milestoneId: params.milestoneId ? Number(params.milestoneId) : undefined, + } + + case 'gitlab_update_issue': + if (!params.projectId?.trim() || !params.issueIid) { + throw new Error('Project ID and Issue IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + issueIid: Number(params.issueIid), + title: params.title?.trim() || undefined, + description: params.description?.trim() || undefined, + labels: params.labels?.trim() || undefined, + assigneeIds: params.assigneeIds + ? params.assigneeIds.split(',').map((id: string) => Number(id.trim())) + : undefined, + stateEvent: params.stateEvent || undefined, + } + + case 'gitlab_delete_issue': + if (!params.projectId?.trim() || !params.issueIid) { + throw new Error('Project ID and Issue IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + issueIid: Number(params.issueIid), + } + + case 'gitlab_create_issue_note': + if (!params.projectId?.trim() || !params.issueIid || !params.body?.trim()) { + throw new Error('Project ID, Issue IID, and comment body are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + issueIid: Number(params.issueIid), + body: params.body.trim(), + } + + case 'gitlab_list_merge_requests': + if (!params.projectId?.trim()) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + state: params.mrState !== 'all' ? params.mrState : undefined, + labels: params.labels?.trim() || undefined, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + } + + case 'gitlab_get_merge_request': + if (!params.projectId?.trim() || !params.mergeRequestIid) { + throw new Error('Project ID and Merge Request IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + mergeRequestIid: Number(params.mergeRequestIid), + } + + case 'gitlab_create_merge_request': + if ( + !params.projectId?.trim() || + !params.title?.trim() || + !params.sourceBranch?.trim() || + !params.targetBranch?.trim() + ) { + throw new Error('Project ID, title, source branch, and target branch are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + title: params.title.trim(), + sourceBranch: params.sourceBranch.trim(), + targetBranch: params.targetBranch.trim(), + description: params.description?.trim() || undefined, + labels: params.labels?.trim() || undefined, + assigneeIds: params.assigneeIds + ? params.assigneeIds.split(',').map((id: string) => Number(id.trim())) + : undefined, + removeSourceBranch: params.removeSourceBranch || undefined, + } + + case 'gitlab_update_merge_request': + if (!params.projectId?.trim() || !params.mergeRequestIid) { + throw new Error('Project ID and Merge Request IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + mergeRequestIid: Number(params.mergeRequestIid), + title: params.title?.trim() || undefined, + description: params.description?.trim() || undefined, + labels: params.labels?.trim() || undefined, + assigneeIds: params.assigneeIds + ? params.assigneeIds.split(',').map((id: string) => Number(id.trim())) + : undefined, + stateEvent: params.stateEvent || undefined, + } + + case 'gitlab_merge_merge_request': + if (!params.projectId?.trim() || !params.mergeRequestIid) { + throw new Error('Project ID and Merge Request IID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + mergeRequestIid: Number(params.mergeRequestIid), + mergeCommitMessage: params.mergeCommitMessage?.trim() || undefined, + squash: params.squash || undefined, + shouldRemoveSourceBranch: params.removeSourceBranch || undefined, + } + + case 'gitlab_create_merge_request_note': + if (!params.projectId?.trim() || !params.mergeRequestIid || !params.body?.trim()) { + throw new Error('Project ID, Merge Request IID, and comment body are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + mergeRequestIid: Number(params.mergeRequestIid), + body: params.body.trim(), + } + + case 'gitlab_list_pipelines': + if (!params.projectId?.trim()) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + status: params.pipelineStatus || undefined, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + } + + case 'gitlab_get_pipeline': + if (!params.projectId?.trim() || !params.pipelineId) { + throw new Error('Project ID and Pipeline ID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + pipelineId: Number(params.pipelineId), + } + + case 'gitlab_create_pipeline': + if (!params.projectId?.trim() || !params.ref?.trim()) { + throw new Error('Project ID and branch/tag ref are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + ref: params.ref.trim(), + } + + case 'gitlab_retry_pipeline': + case 'gitlab_cancel_pipeline': + if (!params.projectId?.trim() || !params.pipelineId) { + throw new Error('Project ID and Pipeline ID are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + pipelineId: Number(params.pipelineId), + } + + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'GitLab access token' }, + projectId: { type: 'string', description: 'Project ID or URL-encoded path' }, + issueIid: { type: 'number', description: 'Issue internal ID' }, + mergeRequestIid: { type: 'number', description: 'Merge request internal ID' }, + pipelineId: { type: 'number', description: 'Pipeline ID' }, + title: { type: 'string', description: 'Title for issue or merge request' }, + description: { type: 'string', description: 'Description (Markdown supported)' }, + body: { type: 'string', description: 'Comment body' }, + sourceBranch: { type: 'string', description: 'Source branch for merge request' }, + targetBranch: { type: 'string', description: 'Target branch for merge request' }, + ref: { type: 'string', description: 'Branch or tag reference for pipeline' }, + labels: { type: 'string', description: 'Labels (comma-separated)' }, + assigneeIds: { type: 'string', description: 'Assignee user IDs (comma-separated)' }, + milestoneId: { type: 'number', description: 'Milestone ID' }, + issueState: { type: 'string', description: 'Issue state filter (opened, closed, all)' }, + mrState: { + type: 'string', + description: 'Merge request state filter (opened, closed, merged, all)', + }, + stateEvent: { type: 'string', description: 'State event (close, reopen)' }, + pipelineStatus: { type: 'string', description: 'Pipeline status filter' }, + removeSourceBranch: { type: 'boolean', description: 'Remove source branch after merge' }, + squash: { type: 'boolean', description: 'Squash commits on merge' }, + mergeCommitMessage: { type: 'string', description: 'Custom merge commit message' }, + perPage: { type: 'number', description: 'Results per page' }, + page: { type: 'number', description: 'Page number' }, + }, + outputs: { + // Project outputs + projects: { type: 'json', description: 'List of projects' }, + project: { type: 'json', description: 'Project details' }, + // Issue outputs + issues: { type: 'json', description: 'List of issues' }, + issue: { type: 'json', description: 'Issue details' }, + // Merge request outputs + mergeRequests: { type: 'json', description: 'List of merge requests' }, + mergeRequest: { type: 'json', description: 'Merge request details' }, + // Pipeline outputs + pipelines: { type: 'json', description: 'List of pipelines' }, + pipeline: { type: 'json', description: 'Pipeline details' }, + // Note outputs + note: { type: 'json', description: 'Comment/note details' }, + // Success indicator + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/blocks/blocks/grafana.ts b/apps/sim/blocks/blocks/grafana.ts new file mode 100644 index 00000000000..e4cc43aec27 --- /dev/null +++ b/apps/sim/blocks/blocks/grafana.ts @@ -0,0 +1,508 @@ +import { GrafanaIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { GrafanaResponse } from '@/tools/grafana/types' + +export const GrafanaBlock: BlockConfig = { + type: 'grafana', + name: 'Grafana', + description: 'Interact with Grafana dashboards, alerts, and annotations', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data sources, folders, and monitor health status.', + docsLink: 'https://docs.sim.ai/tools/grafana', + category: 'tools', + bgColor: '#E0E0E0', + icon: GrafanaIcon, + subBlocks: [ + // Operation dropdown + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Health + { label: 'Health Check', id: 'grafana_health_check' }, + { label: 'Data Source Health', id: 'grafana_data_source_health' }, + // Dashboards + { label: 'List Dashboards', id: 'grafana_list_dashboards' }, + { label: 'Get Dashboard', id: 'grafana_get_dashboard' }, + { label: 'Create Dashboard', id: 'grafana_create_dashboard' }, + { label: 'Update Dashboard', id: 'grafana_update_dashboard' }, + { label: 'Delete Dashboard', id: 'grafana_delete_dashboard' }, + // Alerts + { label: 'List Alert Rules', id: 'grafana_list_alert_rules' }, + { label: 'Get Alert Rule', id: 'grafana_get_alert_rule' }, + { label: 'Create Alert Rule', id: 'grafana_create_alert_rule' }, + { label: 'Update Alert Rule', id: 'grafana_update_alert_rule' }, + { label: 'Delete Alert Rule', id: 'grafana_delete_alert_rule' }, + { label: 'List Contact Points', id: 'grafana_list_contact_points' }, + // Annotations + { label: 'Create Annotation', id: 'grafana_create_annotation' }, + { label: 'List Annotations', id: 'grafana_list_annotations' }, + { label: 'Update Annotation', id: 'grafana_update_annotation' }, + { label: 'Delete Annotation', id: 'grafana_delete_annotation' }, + // Data Sources + { label: 'List Data Sources', id: 'grafana_list_data_sources' }, + { label: 'Get Data Source', id: 'grafana_get_data_source' }, + // Folders + { label: 'List Folders', id: 'grafana_list_folders' }, + { label: 'Create Folder', id: 'grafana_create_folder' }, + ], + value: () => 'grafana_health_check', + }, + + // Base Configuration (common to all operations) + { + id: 'baseUrl', + title: 'Grafana URL', + type: 'short-input', + placeholder: 'https://your-grafana.com', + required: true, + }, + { + id: 'apiKey', + title: 'Service Account Token', + type: 'short-input', + placeholder: 'glsa_...', + password: true, + required: true, + }, + { + id: 'organizationId', + title: 'Organization ID', + type: 'short-input', + placeholder: 'Optional - for multi-org instances', + }, + + // Data Source Health operation + { + id: 'dataSourceId', + title: 'Data Source ID', + type: 'short-input', + placeholder: 'Enter data source ID or UID', + required: true, + condition: { + field: 'operation', + value: ['grafana_data_source_health', 'grafana_get_data_source'], + }, + }, + + // Dashboard operations + { + id: 'dashboardUid', + title: 'Dashboard UID', + type: 'short-input', + placeholder: 'Enter dashboard UID', + required: true, + condition: { + field: 'operation', + value: ['grafana_get_dashboard', 'grafana_update_dashboard', 'grafana_delete_dashboard'], + }, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Filter dashboards by title', + condition: { field: 'operation', value: 'grafana_list_dashboards' }, + }, + { + id: 'tag', + title: 'Filter by Tag', + type: 'short-input', + placeholder: 'tag1, tag2 (comma-separated)', + condition: { field: 'operation', value: 'grafana_list_dashboards' }, + }, + + // Create/Update Dashboard + { + id: 'title', + title: 'Dashboard Title', + type: 'short-input', + placeholder: 'Enter dashboard title', + required: true, + condition: { field: 'operation', value: 'grafana_create_dashboard' }, + }, + { + id: 'folderUid', + title: 'Folder UID', + type: 'short-input', + placeholder: 'Optional - folder to create dashboard in', + condition: { + field: 'operation', + value: [ + 'grafana_create_dashboard', + 'grafana_update_dashboard', + 'grafana_create_alert_rule', + ], + }, + }, + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'tag1, tag2 (comma-separated)', + condition: { + field: 'operation', + value: ['grafana_create_dashboard', 'grafana_update_dashboard'], + }, + }, + { + id: 'panels', + title: 'Panels (JSON)', + type: 'long-input', + placeholder: 'JSON array of panel configurations', + condition: { + field: 'operation', + value: ['grafana_create_dashboard', 'grafana_update_dashboard'], + }, + }, + { + id: 'message', + title: 'Commit Message', + type: 'short-input', + placeholder: 'Optional version message', + condition: { + field: 'operation', + value: ['grafana_create_dashboard', 'grafana_update_dashboard'], + }, + }, + + // Alert Rule operations + { + id: 'alertRuleUid', + title: 'Alert Rule UID', + type: 'short-input', + placeholder: 'Enter alert rule UID', + required: true, + condition: { + field: 'operation', + value: ['grafana_get_alert_rule', 'grafana_update_alert_rule', 'grafana_delete_alert_rule'], + }, + }, + { + id: 'alertTitle', + title: 'Alert Title', + type: 'short-input', + placeholder: 'Enter alert rule name', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'folderUid', + title: 'Folder UID', + type: 'short-input', + placeholder: 'Folder UID for the alert rule', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'ruleGroup', + title: 'Rule Group', + type: 'short-input', + placeholder: 'Enter rule group name', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'condition', + title: 'Condition', + type: 'short-input', + placeholder: 'Condition refId (e.g., A)', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'data', + title: 'Query Data (JSON)', + type: 'long-input', + placeholder: 'JSON array of query/expression data objects', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'forDuration', + title: 'For Duration', + type: 'short-input', + placeholder: '5m (e.g., 5m, 1h)', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'noDataState', + title: 'No Data State', + type: 'dropdown', + options: [ + { label: 'No Data', id: 'NoData' }, + { label: 'Alerting', id: 'Alerting' }, + { label: 'OK', id: 'OK' }, + ], + value: () => 'NoData', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'execErrState', + title: 'Error State', + type: 'dropdown', + options: [ + { label: 'Alerting', id: 'Alerting' }, + { label: 'OK', id: 'OK' }, + ], + value: () => 'Alerting', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + + // Annotation operations + { + id: 'text', + title: 'Annotation Text', + type: 'long-input', + placeholder: 'Enter annotation text...', + required: true, + condition: { + field: 'operation', + value: ['grafana_create_annotation', 'grafana_update_annotation'], + }, + }, + { + id: 'annotationTags', + title: 'Tags', + type: 'short-input', + placeholder: 'tag1, tag2 (comma-separated)', + condition: { + field: 'operation', + value: [ + 'grafana_create_annotation', + 'grafana_update_annotation', + 'grafana_list_annotations', + ], + }, + }, + { + id: 'annotationDashboardUid', + title: 'Dashboard UID', + type: 'short-input', + placeholder: 'Optional - attach to specific dashboard', + condition: { + field: 'operation', + value: ['grafana_create_annotation', 'grafana_list_annotations'], + }, + }, + { + id: 'panelId', + title: 'Panel ID', + type: 'short-input', + placeholder: 'Optional - attach to specific panel', + condition: { + field: 'operation', + value: ['grafana_create_annotation', 'grafana_list_annotations'], + }, + }, + { + id: 'time', + title: 'Time (epoch ms)', + type: 'short-input', + placeholder: 'Optional - defaults to now', + condition: { + field: 'operation', + value: ['grafana_create_annotation', 'grafana_update_annotation'], + }, + }, + { + id: 'timeEnd', + title: 'End Time (epoch ms)', + type: 'short-input', + placeholder: 'Optional - for range annotations', + condition: { + field: 'operation', + value: ['grafana_create_annotation', 'grafana_update_annotation'], + }, + }, + { + id: 'annotationId', + title: 'Annotation ID', + type: 'short-input', + placeholder: 'Enter annotation ID', + required: true, + condition: { + field: 'operation', + value: ['grafana_update_annotation', 'grafana_delete_annotation'], + }, + }, + { + id: 'from', + title: 'From Time (epoch ms)', + type: 'short-input', + placeholder: 'Filter from time', + condition: { field: 'operation', value: 'grafana_list_annotations' }, + }, + { + id: 'to', + title: 'To Time (epoch ms)', + type: 'short-input', + placeholder: 'Filter to time', + condition: { field: 'operation', value: 'grafana_list_annotations' }, + }, + + // Folder operations + { + id: 'folderTitle', + title: 'Folder Title', + type: 'short-input', + placeholder: 'Enter folder title', + required: true, + condition: { field: 'operation', value: 'grafana_create_folder' }, + }, + { + id: 'folderUidNew', + title: 'Folder UID', + type: 'short-input', + placeholder: 'Optional - auto-generated if not provided', + condition: { field: 'operation', value: 'grafana_create_folder' }, + }, + ], + tools: { + access: [ + 'grafana_health_check', + 'grafana_data_source_health', + 'grafana_get_dashboard', + 'grafana_list_dashboards', + 'grafana_create_dashboard', + 'grafana_update_dashboard', + 'grafana_delete_dashboard', + 'grafana_list_alert_rules', + 'grafana_get_alert_rule', + 'grafana_create_alert_rule', + 'grafana_update_alert_rule', + 'grafana_delete_alert_rule', + 'grafana_list_contact_points', + 'grafana_create_annotation', + 'grafana_list_annotations', + 'grafana_update_annotation', + 'grafana_delete_annotation', + 'grafana_list_data_sources', + 'grafana_get_data_source', + 'grafana_list_folders', + 'grafana_create_folder', + ], + config: { + tool: (params) => { + // Convert numeric string fields to numbers + if (params.panelId) { + params.panelId = Number(params.panelId) + } + if (params.annotationId) { + params.annotationId = Number(params.annotationId) + } + if (params.time) { + params.time = Number(params.time) + } + if (params.timeEnd) { + params.timeEnd = Number(params.timeEnd) + } + if (params.from) { + params.from = Number(params.from) + } + if (params.to) { + params.to = Number(params.to) + } + + // Map subblock fields to tool parameter names + if (params.alertTitle) { + params.title = params.alertTitle + } + if (params.folderTitle) { + params.title = params.folderTitle + } + if (params.folderUidNew) { + params.uid = params.folderUidNew + } + if (params.annotationTags) { + params.tags = params.annotationTags + } + if (params.annotationDashboardUid) { + params.dashboardUid = params.annotationDashboardUid + } + + return params.operation + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + baseUrl: { type: 'string', description: 'Grafana instance URL' }, + apiKey: { type: 'string', description: 'Service Account Token' }, + organizationId: { type: 'string', description: 'Organization ID (optional)' }, + // Dashboard inputs + dashboardUid: { type: 'string', description: 'Dashboard UID' }, + title: { type: 'string', description: 'Dashboard or folder title' }, + folderUid: { type: 'string', description: 'Folder UID' }, + tags: { type: 'string', description: 'Comma-separated tags' }, + panels: { type: 'string', description: 'JSON array of panels' }, + message: { type: 'string', description: 'Commit message' }, + query: { type: 'string', description: 'Search query' }, + tag: { type: 'string', description: 'Filter by tag' }, + // Alert inputs + alertRuleUid: { type: 'string', description: 'Alert rule UID' }, + alertTitle: { type: 'string', description: 'Alert rule title' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + condition: { type: 'string', description: 'Alert condition refId' }, + data: { type: 'string', description: 'Query data JSON' }, + forDuration: { type: 'string', description: 'Duration before firing' }, + noDataState: { type: 'string', description: 'State on no data' }, + execErrState: { type: 'string', description: 'State on error' }, + // Annotation inputs + text: { type: 'string', description: 'Annotation text' }, + annotationId: { type: 'number', description: 'Annotation ID' }, + panelId: { type: 'number', description: 'Panel ID' }, + time: { type: 'number', description: 'Start time (epoch ms)' }, + timeEnd: { type: 'number', description: 'End time (epoch ms)' }, + from: { type: 'number', description: 'Filter from time' }, + to: { type: 'number', description: 'Filter to time' }, + // Data source inputs + dataSourceId: { type: 'string', description: 'Data source ID or UID' }, + }, + outputs: { + // Health outputs + version: { type: 'string', description: 'Grafana version' }, + database: { type: 'string', description: 'Database health status' }, + status: { type: 'string', description: 'Health status' }, + // Dashboard outputs + dashboard: { type: 'json', description: 'Dashboard JSON' }, + meta: { type: 'json', description: 'Dashboard metadata' }, + dashboards: { type: 'json', description: 'List of dashboards' }, + uid: { type: 'string', description: 'Created/updated UID' }, + url: { type: 'string', description: 'Dashboard URL' }, + // Alert outputs + rules: { type: 'json', description: 'Alert rules list' }, + contactPoints: { type: 'json', description: 'Contact points list' }, + // Annotation outputs + annotations: { type: 'json', description: 'Annotations list' }, + id: { type: 'number', description: 'Annotation ID' }, + // Data source outputs + dataSources: { type: 'json', description: 'Data sources list' }, + // Folder outputs + folders: { type: 'json', description: 'Folders list' }, + // Common + message: { type: 'string', description: 'Status message' }, + }, +} diff --git a/apps/sim/blocks/blocks/shopify.ts b/apps/sim/blocks/blocks/shopify.ts new file mode 100644 index 00000000000..cee32bfec78 --- /dev/null +++ b/apps/sim/blocks/blocks/shopify.ts @@ -0,0 +1,674 @@ +import { ShopifyIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +interface ShopifyResponse { + success: boolean + error?: string + output: Record +} + +export const ShopifyBlock: BlockConfig = { + type: 'shopify', + name: 'Shopify', + description: 'Manage products, orders, customers, and inventory in your Shopify store', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Shopify into your workflow. Manage products, orders, customers, and inventory. Create, read, update, and delete products. List and manage orders. Handle customer data and adjust inventory levels.', + docsLink: 'https://docs.sim.ai/tools/shopify', + category: 'tools', + icon: ShopifyIcon, + bgColor: '#96BF48', + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Product Operations + { label: 'Create Product', id: 'shopify_create_product' }, + { label: 'Get Product', id: 'shopify_get_product' }, + { label: 'List Products', id: 'shopify_list_products' }, + { label: 'Update Product', id: 'shopify_update_product' }, + { label: 'Delete Product', id: 'shopify_delete_product' }, + // Order Operations + { label: 'Get Order', id: 'shopify_get_order' }, + { label: 'List Orders', id: 'shopify_list_orders' }, + { label: 'Update Order', id: 'shopify_update_order' }, + { label: 'Cancel Order', id: 'shopify_cancel_order' }, + // Customer Operations + { label: 'Create Customer', id: 'shopify_create_customer' }, + { label: 'Get Customer', id: 'shopify_get_customer' }, + { label: 'List Customers', id: 'shopify_list_customers' }, + { label: 'Update Customer', id: 'shopify_update_customer' }, + { label: 'Delete Customer', id: 'shopify_delete_customer' }, + // Inventory Operations + { label: 'Get Inventory Level', id: 'shopify_get_inventory_level' }, + { label: 'Adjust Inventory', id: 'shopify_adjust_inventory' }, + ], + value: () => 'shopify_list_products', + }, + { + id: 'credential', + title: 'Shopify Account', + type: 'oauth-input', + serviceId: 'shopify', + requiredScopes: [ + 'read_products', + 'write_products', + 'read_orders', + 'write_orders', + 'read_customers', + 'write_customers', + 'read_inventory', + 'write_inventory', + ], + placeholder: 'Select Shopify account', + required: true, + }, + { + id: 'shopDomain', + title: 'Shop Domain', + type: 'short-input', + placeholder: 'mystore.myshopify.com', + required: true, + }, + // Product ID (for get/update/delete operations) + { + id: 'productId', + title: 'Product ID', + type: 'short-input', + placeholder: 'gid://shopify/Product/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_get_product', 'shopify_update_product', 'shopify_delete_product'], + }, + }, + // Product Title (for create/update) + { + id: 'title', + title: 'Product Title', + type: 'short-input', + placeholder: 'Enter product title', + required: { + field: 'operation', + value: ['shopify_create_product'], + }, + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Product Description + { + id: 'descriptionHtml', + title: 'Description (HTML)', + type: 'long-input', + placeholder: 'Enter product description', + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Product Type + { + id: 'productType', + title: 'Product Type', + type: 'short-input', + placeholder: 'e.g., Shoes, Electronics', + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Vendor + { + id: 'vendor', + title: 'Vendor', + type: 'short-input', + placeholder: 'Enter vendor name', + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Tags + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'tag1, tag2, tag3 (comma-separated)', + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Status + { + id: 'status', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'Active', id: 'ACTIVE' }, + { label: 'Draft', id: 'DRAFT' }, + { label: 'Archived', id: 'ARCHIVED' }, + ], + value: () => 'ACTIVE', + condition: { + field: 'operation', + value: ['shopify_create_product', 'shopify_update_product'], + }, + }, + // Query for listing products + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Filter products (optional)', + condition: { + field: 'operation', + value: ['shopify_list_products', 'shopify_list_customers'], + }, + }, + // Order ID + { + id: 'orderId', + title: 'Order ID', + type: 'short-input', + placeholder: 'gid://shopify/Order/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_get_order', 'shopify_update_order', 'shopify_cancel_order'], + }, + }, + // Order Status (for listing) + { + id: 'orderStatus', + title: 'Order Status', + type: 'dropdown', + options: [ + { label: 'Any', id: 'any' }, + { label: 'Open', id: 'open' }, + { label: 'Closed', id: 'closed' }, + { label: 'Cancelled', id: 'cancelled' }, + ], + value: () => 'any', + condition: { + field: 'operation', + value: ['shopify_list_orders'], + }, + }, + // Order Note (for update) + { + id: 'orderNote', + title: 'Order Note', + type: 'long-input', + placeholder: 'Enter order note', + condition: { + field: 'operation', + value: ['shopify_update_order'], + }, + }, + // Order Email (for update) + { + id: 'orderEmail', + title: 'Customer Email', + type: 'short-input', + placeholder: 'customer@example.com', + condition: { + field: 'operation', + value: ['shopify_update_order'], + }, + }, + // Order Tags (for update) + { + id: 'orderTags', + title: 'Order Tags', + type: 'short-input', + placeholder: 'tag1, tag2, tag3 (comma-separated)', + condition: { + field: 'operation', + value: ['shopify_update_order'], + }, + }, + // Cancel Order Reason + { + id: 'cancelReason', + title: 'Cancel Reason', + type: 'dropdown', + options: [ + { label: 'Customer Request', id: 'CUSTOMER' }, + { label: 'Declined Payment', id: 'DECLINED' }, + { label: 'Fraud', id: 'FRAUD' }, + { label: 'Inventory Issue', id: 'INVENTORY' }, + { label: 'Other', id: 'OTHER' }, + ], + value: () => 'OTHER', + required: true, + condition: { + field: 'operation', + value: ['shopify_cancel_order'], + }, + }, + // Staff Note (for cancel order) + { + id: 'staffNote', + title: 'Staff Note', + type: 'long-input', + placeholder: 'Internal note about this cancellation', + condition: { + field: 'operation', + value: ['shopify_cancel_order'], + }, + }, + // Customer ID + { + id: 'customerId', + title: 'Customer ID', + type: 'short-input', + placeholder: 'gid://shopify/Customer/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_get_customer', 'shopify_update_customer', 'shopify_delete_customer'], + }, + }, + // Customer Email (at least one of email/phone/firstName/lastName required for create) + { + id: 'customerEmail', + title: 'Email', + type: 'short-input', + placeholder: 'customer@example.com', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Customer First Name + { + id: 'firstName', + title: 'First Name', + type: 'short-input', + placeholder: 'Enter first name', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Customer Last Name + { + id: 'lastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Enter last name', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Customer Phone + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Customer Note + { + id: 'customerNote', + title: 'Customer Note', + type: 'long-input', + placeholder: 'Enter note about customer', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Customer Tags + { + id: 'customerTags', + title: 'Customer Tags', + type: 'short-input', + placeholder: 'vip, wholesale (comma-separated)', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Accepts Marketing + { + id: 'acceptsMarketing', + title: 'Accepts Marketing', + type: 'switch', + condition: { + field: 'operation', + value: ['shopify_create_customer', 'shopify_update_customer'], + }, + }, + // Inventory Item ID + { + id: 'inventoryItemId', + title: 'Inventory Item ID', + type: 'short-input', + placeholder: 'gid://shopify/InventoryItem/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_get_inventory_level', 'shopify_adjust_inventory'], + }, + }, + // Location ID + { + id: 'locationId', + title: 'Location ID', + type: 'short-input', + placeholder: 'gid://shopify/Location/123456789', + required: { + field: 'operation', + value: ['shopify_adjust_inventory'], + }, + condition: { + field: 'operation', + value: ['shopify_get_inventory_level', 'shopify_adjust_inventory'], + }, + }, + // Delta (for inventory adjustment) + { + id: 'delta', + title: 'Quantity Change', + type: 'short-input', + placeholder: 'Positive to add, negative to subtract', + required: true, + condition: { + field: 'operation', + value: ['shopify_adjust_inventory'], + }, + }, + ], + tools: { + access: [ + 'shopify_create_product', + 'shopify_get_product', + 'shopify_list_products', + 'shopify_update_product', + 'shopify_delete_product', + 'shopify_get_order', + 'shopify_list_orders', + 'shopify_update_order', + 'shopify_cancel_order', + 'shopify_create_customer', + 'shopify_get_customer', + 'shopify_list_customers', + 'shopify_update_customer', + 'shopify_delete_customer', + 'shopify_get_inventory_level', + 'shopify_adjust_inventory', + ], + config: { + tool: (params) => { + return params.operation || 'shopify_list_products' + }, + params: (params) => { + const baseParams: Record = { + credential: params.credential, + shopDomain: params.shopDomain?.trim(), + } + + switch (params.operation) { + // Product Operations + case 'shopify_create_product': + if (!params.title?.trim()) { + throw new Error('Product title is required.') + } + return { + ...baseParams, + title: params.title.trim(), + descriptionHtml: params.descriptionHtml?.trim(), + productType: params.productType?.trim(), + vendor: params.vendor?.trim(), + tags: params.tags + ?.split(',') + .map((t: string) => t.trim()) + .filter(Boolean), + status: params.status, + } + + case 'shopify_get_product': + if (!params.productId?.trim()) { + throw new Error('Product ID is required.') + } + return { + ...baseParams, + productId: params.productId.trim(), + } + + case 'shopify_list_products': + return { + ...baseParams, + query: params.query?.trim(), + } + + case 'shopify_update_product': + if (!params.productId?.trim()) { + throw new Error('Product ID is required.') + } + return { + ...baseParams, + productId: params.productId.trim(), + title: params.title?.trim(), + descriptionHtml: params.descriptionHtml?.trim(), + productType: params.productType?.trim(), + vendor: params.vendor?.trim(), + tags: params.tags + ?.split(',') + .map((t: string) => t.trim()) + .filter(Boolean), + status: params.status, + } + + case 'shopify_delete_product': + if (!params.productId?.trim()) { + throw new Error('Product ID is required.') + } + return { + ...baseParams, + productId: params.productId.trim(), + } + + // Order Operations + case 'shopify_get_order': + if (!params.orderId?.trim()) { + throw new Error('Order ID is required.') + } + return { + ...baseParams, + orderId: params.orderId.trim(), + } + + case 'shopify_list_orders': + return { + ...baseParams, + status: params.orderStatus !== 'any' ? params.orderStatus : undefined, + } + + case 'shopify_update_order': + if (!params.orderId?.trim()) { + throw new Error('Order ID is required.') + } + return { + ...baseParams, + orderId: params.orderId.trim(), + note: params.orderNote?.trim(), + email: params.orderEmail?.trim(), + tags: params.orderTags + ?.split(',') + .map((t: string) => t.trim()) + .filter(Boolean), + } + + case 'shopify_cancel_order': + if (!params.orderId?.trim()) { + throw new Error('Order ID is required.') + } + if (!params.cancelReason) { + throw new Error('Cancel reason is required.') + } + return { + ...baseParams, + orderId: params.orderId.trim(), + reason: params.cancelReason, + staffNote: params.staffNote?.trim(), + } + + // Customer Operations + case 'shopify_create_customer': + // At least one of email/phone/firstName/lastName required (validated in tool) + return { + ...baseParams, + email: params.customerEmail?.trim(), + firstName: params.firstName?.trim(), + lastName: params.lastName?.trim(), + phone: params.phone?.trim(), + note: params.customerNote?.trim(), + tags: params.customerTags + ?.split(',') + .map((t: string) => t.trim()) + .filter(Boolean), + acceptsMarketing: params.acceptsMarketing, + } + + case 'shopify_get_customer': + if (!params.customerId?.trim()) { + throw new Error('Customer ID is required.') + } + return { + ...baseParams, + customerId: params.customerId.trim(), + } + + case 'shopify_list_customers': + return { + ...baseParams, + query: params.query?.trim(), + } + + case 'shopify_update_customer': + if (!params.customerId?.trim()) { + throw new Error('Customer ID is required.') + } + return { + ...baseParams, + customerId: params.customerId.trim(), + email: params.customerEmail?.trim(), + firstName: params.firstName?.trim(), + lastName: params.lastName?.trim(), + phone: params.phone?.trim(), + note: params.customerNote?.trim(), + tags: params.customerTags + ?.split(',') + .map((t: string) => t.trim()) + .filter(Boolean), + } + + case 'shopify_delete_customer': + if (!params.customerId?.trim()) { + throw new Error('Customer ID is required.') + } + return { + ...baseParams, + customerId: params.customerId.trim(), + } + + // Inventory Operations + case 'shopify_get_inventory_level': + if (!params.inventoryItemId?.trim()) { + throw new Error('Inventory Item ID is required.') + } + return { + ...baseParams, + inventoryItemId: params.inventoryItemId.trim(), + locationId: params.locationId?.trim(), + } + + case 'shopify_adjust_inventory': + if (!params.inventoryItemId?.trim()) { + throw new Error('Inventory Item ID is required.') + } + if (!params.locationId?.trim()) { + throw new Error('Location ID is required.') + } + if (params.delta === undefined || params.delta === '') { + throw new Error('Quantity change (delta) is required.') + } + return { + ...baseParams, + inventoryItemId: params.inventoryItemId.trim(), + locationId: params.locationId.trim(), + delta: Number(params.delta), + } + + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Shopify access token' }, + shopDomain: { type: 'string', description: 'Shopify store domain' }, + // Product inputs + productId: { type: 'string', description: 'Product ID' }, + title: { type: 'string', description: 'Product title' }, + descriptionHtml: { type: 'string', description: 'Product description (HTML)' }, + productType: { type: 'string', description: 'Product type' }, + vendor: { type: 'string', description: 'Product vendor' }, + tags: { type: 'string', description: 'Tags (comma-separated)' }, + status: { type: 'string', description: 'Product status' }, + query: { type: 'string', description: 'Search query' }, + // Order inputs + orderId: { type: 'string', description: 'Order ID' }, + orderStatus: { type: 'string', description: 'Order status filter' }, + orderNote: { type: 'string', description: 'Order note' }, + orderEmail: { type: 'string', description: 'Order customer email' }, + orderTags: { type: 'string', description: 'Order tags' }, + cancelReason: { type: 'string', description: 'Order cancellation reason' }, + staffNote: { type: 'string', description: 'Staff note for order cancellation' }, + // Customer inputs + customerId: { type: 'string', description: 'Customer ID' }, + customerEmail: { type: 'string', description: 'Customer email' }, + firstName: { type: 'string', description: 'Customer first name' }, + lastName: { type: 'string', description: 'Customer last name' }, + phone: { type: 'string', description: 'Customer phone' }, + customerNote: { type: 'string', description: 'Customer note' }, + customerTags: { type: 'string', description: 'Customer tags' }, + acceptsMarketing: { type: 'boolean', description: 'Accepts marketing' }, + // Inventory inputs + inventoryItemId: { type: 'string', description: 'Inventory item ID' }, + locationId: { type: 'string', description: 'Location ID' }, + delta: { type: 'number', description: 'Quantity change' }, + }, + outputs: { + // Product outputs + product: { type: 'json', description: 'Product data' }, + products: { type: 'json', description: 'Products list' }, + // Order outputs + order: { type: 'json', description: 'Order data' }, + orders: { type: 'json', description: 'Orders list' }, + // Customer outputs + customer: { type: 'json', description: 'Customer data' }, + customers: { type: 'json', description: 'Customers list' }, + // Inventory outputs + inventoryLevel: { type: 'json', description: 'Inventory level data' }, + // Delete outputs + deletedId: { type: 'string', description: 'ID of deleted resource' }, + // Success indicator + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/blocks/blocks/ssh.ts b/apps/sim/blocks/blocks/ssh.ts new file mode 100644 index 00000000000..ae68dcc6b74 --- /dev/null +++ b/apps/sim/blocks/blocks/ssh.ts @@ -0,0 +1,518 @@ +import { SshIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { SSHResponse } from '@/tools/ssh/types' + +export const SSHBlock: BlockConfig = { + type: 'ssh', + name: 'SSH', + description: 'Connect to remote servers via SSH', + authMode: AuthMode.ApiKey, + longDescription: + 'Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access.', + docsLink: 'https://docs.sim.ai/tools/ssh', + category: 'tools', + bgColor: '#2C3E50', + icon: SshIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Execute Command', id: 'ssh_execute_command' }, + { label: 'Execute Script', id: 'ssh_execute_script' }, + { label: 'Check Command Exists', id: 'ssh_check_command_exists' }, + { label: 'Upload File', id: 'ssh_upload_file' }, + { label: 'Download File', id: 'ssh_download_file' }, + { label: 'List Directory', id: 'ssh_list_directory' }, + { label: 'Check File/Directory Exists', id: 'ssh_check_file_exists' }, + { label: 'Create Directory', id: 'ssh_create_directory' }, + { label: 'Delete File/Directory', id: 'ssh_delete_file' }, + { label: 'Move/Rename', id: 'ssh_move_rename' }, + { label: 'Get System Info', id: 'ssh_get_system_info' }, + { label: 'Read File Content', id: 'ssh_read_file_content' }, + { label: 'Write File Content', id: 'ssh_write_file_content' }, + ], + value: () => 'ssh_execute_command', + }, + + // Connection parameters + { + id: 'host', + title: 'Host', + type: 'short-input', + placeholder: 'example.com or 192.168.1.100', + required: true, + }, + { + id: 'port', + title: 'Port', + type: 'short-input', + placeholder: '22', + value: () => '22', + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'ubuntu, root, or deploy', + required: true, + }, + + // Authentication method selector + { + id: 'authMethod', + title: 'Authentication Method', + type: 'dropdown', + options: [ + { label: 'Password', id: 'password' }, + { label: 'Private Key', id: 'privateKey' }, + ], + value: () => 'password', + }, + + // Password authentication + { + id: 'password', + title: 'Password', + type: 'short-input', + password: true, + placeholder: 'Your SSH password', + condition: { field: 'authMethod', value: 'password' }, + }, + + // Private key authentication + { + id: 'privateKey', + title: 'Private Key', + type: 'code', + placeholder: '-----BEGIN OPENSSH PRIVATE KEY-----\n...', + condition: { field: 'authMethod', value: 'privateKey' }, + }, + { + id: 'passphrase', + title: 'Passphrase', + type: 'short-input', + password: true, + placeholder: 'Passphrase for encrypted key (optional)', + condition: { field: 'authMethod', value: 'privateKey' }, + }, + + // EXECUTE COMMAND + { + id: 'command', + title: 'Command', + type: 'code', + placeholder: 'ls -la /var/www', + required: true, + condition: { field: 'operation', value: 'ssh_execute_command' }, + }, + { + id: 'workingDirectory', + title: 'Working Directory', + type: 'short-input', + placeholder: '/var/www/html (optional)', + condition: { field: 'operation', value: 'ssh_execute_command' }, + }, + + // EXECUTE SCRIPT + { + id: 'script', + title: 'Script Content', + type: 'code', + placeholder: '#!/bin/bash\necho "Hello World"', + required: true, + condition: { field: 'operation', value: 'ssh_execute_script' }, + }, + { + id: 'interpreter', + title: 'Interpreter', + type: 'short-input', + placeholder: '/bin/bash', + condition: { field: 'operation', value: 'ssh_execute_script' }, + }, + { + id: 'scriptWorkingDirectory', + title: 'Working Directory', + type: 'short-input', + placeholder: '/var/www/html (optional)', + condition: { field: 'operation', value: 'ssh_execute_script' }, + }, + + // CHECK COMMAND EXISTS + { + id: 'commandName', + title: 'Command Name', + type: 'short-input', + placeholder: 'docker, git, python3', + required: true, + condition: { field: 'operation', value: 'ssh_check_command_exists' }, + }, + + // UPLOAD FILE + { + id: 'fileContent', + title: 'File Content', + type: 'code', + placeholder: 'Content to upload...', + required: true, + condition: { field: 'operation', value: 'ssh_upload_file' }, + }, + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + placeholder: 'config.json', + required: true, + condition: { field: 'operation', value: 'ssh_upload_file' }, + }, + { + id: 'remotePath', + title: 'Remote Path', + type: 'short-input', + placeholder: '/var/www/html/config.json', + required: true, + condition: { field: 'operation', value: 'ssh_upload_file' }, + }, + { + id: 'permissions', + title: 'Permissions', + type: 'short-input', + placeholder: '0644', + condition: { field: 'operation', value: 'ssh_upload_file' }, + }, + + // DOWNLOAD FILE + { + id: 'downloadRemotePath', + title: 'Remote File Path', + type: 'short-input', + placeholder: '/var/log/app.log', + required: true, + condition: { field: 'operation', value: 'ssh_download_file' }, + }, + + // LIST DIRECTORY + { + id: 'listPath', + title: 'Directory Path', + type: 'short-input', + placeholder: '/var/www', + required: true, + condition: { field: 'operation', value: 'ssh_list_directory' }, + }, + { + id: 'detailed', + title: 'Show Details', + type: 'switch', + condition: { field: 'operation', value: 'ssh_list_directory' }, + }, + + // CHECK FILE EXISTS + { + id: 'checkPath', + title: 'Path to Check', + type: 'short-input', + placeholder: '/etc/nginx/nginx.conf', + required: true, + condition: { field: 'operation', value: 'ssh_check_file_exists' }, + }, + { + id: 'checkType', + title: 'Expected Type', + type: 'dropdown', + options: [ + { label: 'Any', id: 'any' }, + { label: 'File', id: 'file' }, + { label: 'Directory', id: 'directory' }, + ], + value: () => 'any', + condition: { field: 'operation', value: 'ssh_check_file_exists' }, + }, + + // CREATE DIRECTORY + { + id: 'createPath', + title: 'Directory Path', + type: 'short-input', + placeholder: '/var/www/new-site', + required: true, + condition: { field: 'operation', value: 'ssh_create_directory' }, + }, + { + id: 'recursive', + title: 'Create Parent Directories', + type: 'switch', + defaultValue: true, + condition: { field: 'operation', value: 'ssh_create_directory' }, + }, + + // DELETE FILE + { + id: 'deletePath', + title: 'Path to Delete', + type: 'short-input', + placeholder: '/tmp/old-file.txt', + required: true, + condition: { field: 'operation', value: 'ssh_delete_file' }, + }, + { + id: 'deleteRecursive', + title: 'Recursive Delete', + type: 'switch', + condition: { field: 'operation', value: 'ssh_delete_file' }, + }, + { + id: 'force', + title: 'Force Delete', + type: 'switch', + condition: { field: 'operation', value: 'ssh_delete_file' }, + }, + + // MOVE/RENAME + { + id: 'sourcePath', + title: 'Source Path', + type: 'short-input', + placeholder: '/var/www/old-name', + required: true, + condition: { field: 'operation', value: 'ssh_move_rename' }, + }, + { + id: 'destinationPath', + title: 'Destination Path', + type: 'short-input', + placeholder: '/var/www/new-name', + required: true, + condition: { field: 'operation', value: 'ssh_move_rename' }, + }, + { + id: 'overwrite', + title: 'Overwrite if Exists', + type: 'switch', + condition: { field: 'operation', value: 'ssh_move_rename' }, + }, + + // READ FILE CONTENT + { + id: 'readPath', + title: 'File Path', + type: 'short-input', + placeholder: '/var/log/app.log', + required: true, + condition: { field: 'operation', value: 'ssh_read_file_content' }, + }, + { + id: 'encoding', + title: 'Encoding', + type: 'short-input', + placeholder: 'utf-8', + condition: { field: 'operation', value: 'ssh_read_file_content' }, + }, + { + id: 'maxSize', + title: 'Max Size (MB)', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'ssh_read_file_content' }, + }, + + // WRITE FILE CONTENT + { + id: 'writePath', + title: 'File Path', + type: 'short-input', + placeholder: '/etc/config.json', + required: true, + condition: { field: 'operation', value: 'ssh_write_file_content' }, + }, + { + id: 'content', + title: 'File Content', + type: 'code', + placeholder: 'Content to write...', + required: true, + condition: { field: 'operation', value: 'ssh_write_file_content' }, + }, + { + id: 'writeMode', + title: 'Write Mode', + type: 'dropdown', + options: [ + { label: 'Overwrite', id: 'overwrite' }, + { label: 'Append', id: 'append' }, + { label: 'Create (fail if exists)', id: 'create' }, + ], + value: () => 'overwrite', + condition: { field: 'operation', value: 'ssh_write_file_content' }, + }, + { + id: 'writePermissions', + title: 'Permissions', + type: 'short-input', + placeholder: '0644', + condition: { field: 'operation', value: 'ssh_write_file_content' }, + }, + ], + tools: { + access: [ + 'ssh_execute_command', + 'ssh_execute_script', + 'ssh_check_command_exists', + 'ssh_upload_file', + 'ssh_download_file', + 'ssh_list_directory', + 'ssh_check_file_exists', + 'ssh_create_directory', + 'ssh_delete_file', + 'ssh_move_rename', + 'ssh_get_system_info', + 'ssh_read_file_content', + 'ssh_write_file_content', + ], + config: { + tool: (params) => { + return params.operation || 'ssh_execute_command' + }, + params: (params) => { + // Build connection config + const connectionConfig: Record = { + host: params.host, + port: + typeof params.port === 'string' ? Number.parseInt(params.port, 10) : params.port || 22, + username: params.username, + } + + // Add authentication based on method + if (params.authMethod === 'privateKey') { + connectionConfig.privateKey = params.privateKey + if (params.passphrase) { + connectionConfig.passphrase = params.passphrase + } + } else { + connectionConfig.password = params.password + } + + // Build operation-specific parameters based on the selected operation + const operation = params.operation || 'ssh_execute_command' + + switch (operation) { + case 'ssh_execute_command': + return { + ...connectionConfig, + command: params.command, + workingDirectory: params.workingDirectory, + } + case 'ssh_execute_script': + return { + ...connectionConfig, + script: params.script, + interpreter: params.interpreter || '/bin/bash', + workingDirectory: params.scriptWorkingDirectory, + } + case 'ssh_check_command_exists': + return { + ...connectionConfig, + commandName: params.commandName, + } + case 'ssh_upload_file': + return { + ...connectionConfig, + fileContent: params.fileContent, + fileName: params.fileName, + remotePath: params.remotePath, + permissions: params.permissions, + } + case 'ssh_download_file': + return { + ...connectionConfig, + remotePath: params.downloadRemotePath, + } + case 'ssh_list_directory': + return { + ...connectionConfig, + path: params.listPath, + detailed: params.detailed, + } + case 'ssh_check_file_exists': + return { + ...connectionConfig, + path: params.checkPath, + type: params.checkType || 'any', + } + case 'ssh_create_directory': + return { + ...connectionConfig, + path: params.createPath, + recursive: params.recursive !== false, + } + case 'ssh_delete_file': + return { + ...connectionConfig, + path: params.deletePath, + recursive: params.deleteRecursive, + force: params.force, + } + case 'ssh_move_rename': + return { + ...connectionConfig, + sourcePath: params.sourcePath, + destinationPath: params.destinationPath, + overwrite: params.overwrite, + } + case 'ssh_get_system_info': + return connectionConfig + case 'ssh_read_file_content': + return { + ...connectionConfig, + path: params.readPath, + encoding: params.encoding || 'utf-8', + maxSize: params.maxSize ? Number.parseInt(params.maxSize, 10) : 10, + } + case 'ssh_write_file_content': + return { + ...connectionConfig, + path: params.writePath, + content: params.content, + mode: params.writeMode || 'overwrite', + permissions: params.writePermissions, + } + default: + return connectionConfig + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'SSH operation to perform' }, + host: { type: 'string', description: 'SSH server hostname' }, + port: { type: 'number', description: 'SSH server port' }, + username: { type: 'string', description: 'SSH username' }, + authMethod: { type: 'string', description: 'Authentication method' }, + password: { type: 'string', description: 'Password for authentication' }, + privateKey: { type: 'string', description: 'Private key for authentication' }, + passphrase: { type: 'string', description: 'Passphrase for encrypted key' }, + command: { type: 'string', description: 'Command to execute' }, + script: { type: 'string', description: 'Script content to execute' }, + commandName: { type: 'string', description: 'Command name to check' }, + fileContent: { type: 'string', description: 'File content to upload' }, + fileName: { type: 'string', description: 'Name of the file' }, + remotePath: { type: 'string', description: 'Remote file/directory path' }, + content: { type: 'string', description: 'File content' }, + }, + outputs: { + stdout: { type: 'string', description: 'Command standard output' }, + stderr: { type: 'string', description: 'Command standard error' }, + exitCode: { type: 'number', description: 'Command exit code' }, + success: { type: 'boolean', description: 'Operation success status' }, + fileContent: { type: 'string', description: 'Downloaded/read file content' }, + entries: { type: 'json', description: 'Directory entries' }, + exists: { type: 'boolean', description: 'File/directory existence' }, + content: { type: 'string', description: 'File content' }, + hostname: { type: 'string', description: 'Server hostname' }, + os: { type: 'string', description: 'Operating system' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/blocks/blocks/wordpress.ts b/apps/sim/blocks/blocks/wordpress.ts new file mode 100644 index 00000000000..36e8d98e418 --- /dev/null +++ b/apps/sim/blocks/blocks/wordpress.ts @@ -0,0 +1,979 @@ +import { WordpressIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { WordPressResponse } from '@/tools/wordpress/types' + +export const WordPressBlock: BlockConfig = { + type: 'wordpress', + name: 'WordPress', + description: 'Manage WordPress content', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports self-hosted WordPress sites using Application Passwords authentication.', + docsLink: 'https://docs.sim.ai/tools/wordpress', + category: 'tools', + bgColor: '#21759B', + icon: WordpressIcon, + subBlocks: [ + // Operation Selection + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Posts + { label: 'Create Post', id: 'wordpress_create_post' }, + { label: 'Update Post', id: 'wordpress_update_post' }, + { label: 'Delete Post', id: 'wordpress_delete_post' }, + { label: 'Get Post', id: 'wordpress_get_post' }, + { label: 'List Posts', id: 'wordpress_list_posts' }, + // Pages + { label: 'Create Page', id: 'wordpress_create_page' }, + { label: 'Update Page', id: 'wordpress_update_page' }, + { label: 'Delete Page', id: 'wordpress_delete_page' }, + { label: 'Get Page', id: 'wordpress_get_page' }, + { label: 'List Pages', id: 'wordpress_list_pages' }, + // Media + { label: 'Upload Media', id: 'wordpress_upload_media' }, + { label: 'Get Media', id: 'wordpress_get_media' }, + { label: 'List Media', id: 'wordpress_list_media' }, + { label: 'Delete Media', id: 'wordpress_delete_media' }, + // Comments + { label: 'Create Comment', id: 'wordpress_create_comment' }, + { label: 'List Comments', id: 'wordpress_list_comments' }, + { label: 'Update Comment', id: 'wordpress_update_comment' }, + { label: 'Delete Comment', id: 'wordpress_delete_comment' }, + // Categories + { label: 'Create Category', id: 'wordpress_create_category' }, + { label: 'List Categories', id: 'wordpress_list_categories' }, + // Tags + { label: 'Create Tag', id: 'wordpress_create_tag' }, + { label: 'List Tags', id: 'wordpress_list_tags' }, + // Users + { label: 'Get Current User', id: 'wordpress_get_current_user' }, + { label: 'List Users', id: 'wordpress_list_users' }, + { label: 'Get User', id: 'wordpress_get_user' }, + // Search + { label: 'Search Content', id: 'wordpress_search_content' }, + ], + value: () => 'wordpress_create_post', + }, + + // Authentication Fields + { + id: 'siteUrl', + title: 'Site URL', + type: 'short-input', + placeholder: 'https://your-site.com', + required: true, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'WordPress username', + required: true, + }, + { + id: 'applicationPassword', + title: 'Application Password', + type: 'short-input', + placeholder: 'xxxx xxxx xxxx xxxx xxxx xxxx', + password: true, + required: true, + }, + + // Post Operations - Post ID + { + id: 'postId', + title: 'Post ID', + type: 'short-input', + placeholder: 'Enter post ID', + condition: { + field: 'operation', + value: ['wordpress_update_post', 'wordpress_delete_post', 'wordpress_get_post'], + }, + required: { + field: 'operation', + value: ['wordpress_update_post', 'wordpress_delete_post', 'wordpress_get_post'], + }, + }, + + // Post/Page Title + { + id: 'title', + title: 'Title', + type: 'short-input', + placeholder: 'Post or page title', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + required: { + field: 'operation', + value: ['wordpress_create_post', 'wordpress_create_page'], + }, + }, + + // Post/Page Content + { + id: 'content', + title: 'Content', + type: 'long-input', + placeholder: 'Post or page content (HTML or plain text)', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + }, + + // Post/Page Status + { + id: 'status', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'Publish', id: 'publish' }, + { label: 'Draft', id: 'draft' }, + { label: 'Pending', id: 'pending' }, + { label: 'Private', id: 'private' }, + ], + value: () => 'publish', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + }, + + // Excerpt (for posts and pages) + { + id: 'excerpt', + title: 'Excerpt', + type: 'long-input', + placeholder: 'Post or page excerpt', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + }, + + // Slug (for posts and pages) + { + id: 'slug', + title: 'Slug', + type: 'short-input', + placeholder: 'URL slug (optional)', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + }, + + // Categories (for posts only) + { + id: 'categories', + title: 'Categories', + type: 'short-input', + placeholder: 'Comma-separated category IDs', + condition: { + field: 'operation', + value: ['wordpress_create_post', 'wordpress_update_post'], + }, + }, + + // Tags (for posts only) + { + id: 'tags', + title: 'Tags', + type: 'short-input', + placeholder: 'Comma-separated tag IDs', + condition: { + field: 'operation', + value: ['wordpress_create_post', 'wordpress_update_post'], + }, + }, + + // Featured Media ID + { + id: 'featuredMedia', + title: 'Featured Image ID', + type: 'short-input', + placeholder: 'Media ID for featured image', + condition: { + field: 'operation', + value: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_create_page', + 'wordpress_update_page', + ], + }, + }, + + // Page-specific: Page ID + { + id: 'pageId', + title: 'Page ID', + type: 'short-input', + placeholder: 'Enter page ID', + condition: { + field: 'operation', + value: ['wordpress_update_page', 'wordpress_delete_page', 'wordpress_get_page'], + }, + required: { + field: 'operation', + value: ['wordpress_update_page', 'wordpress_delete_page', 'wordpress_get_page'], + }, + }, + + // Page-specific: Parent Page + { + id: 'parent', + title: 'Parent Page ID', + type: 'short-input', + placeholder: 'Parent page ID (for hierarchy)', + condition: { + field: 'operation', + value: ['wordpress_create_page', 'wordpress_update_page'], + }, + }, + + // Page-specific: Menu Order + { + id: 'menuOrder', + title: 'Menu Order', + type: 'short-input', + placeholder: 'Order in menu (number)', + condition: { + field: 'operation', + value: ['wordpress_create_page', 'wordpress_update_page'], + }, + }, + + // Media Operations + { + id: 'file', + title: 'File', + type: 'short-input', + placeholder: 'Base64 encoded file data or file URL', + condition: { field: 'operation', value: 'wordpress_upload_media' }, + required: { field: 'operation', value: 'wordpress_upload_media' }, + }, + { + id: 'filename', + title: 'Filename', + type: 'short-input', + placeholder: 'image.jpg', + condition: { field: 'operation', value: 'wordpress_upload_media' }, + required: { field: 'operation', value: 'wordpress_upload_media' }, + }, + { + id: 'mediaTitle', + title: 'Media Title', + type: 'short-input', + placeholder: 'Title for the media', + condition: { field: 'operation', value: 'wordpress_upload_media' }, + }, + { + id: 'caption', + title: 'Caption', + type: 'short-input', + placeholder: 'Media caption', + condition: { field: 'operation', value: 'wordpress_upload_media' }, + }, + { + id: 'altText', + title: 'Alt Text', + type: 'short-input', + placeholder: 'Alternative text for accessibility', + condition: { field: 'operation', value: 'wordpress_upload_media' }, + }, + { + id: 'mediaId', + title: 'Media ID', + type: 'short-input', + placeholder: 'Enter media ID', + condition: { + field: 'operation', + value: ['wordpress_get_media', 'wordpress_delete_media'], + }, + required: { + field: 'operation', + value: ['wordpress_get_media', 'wordpress_delete_media'], + }, + }, + { + id: 'mediaType', + title: 'Media Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Image', id: 'image' }, + { label: 'Video', id: 'video' }, + { label: 'Audio', id: 'audio' }, + { label: 'Application', id: 'application' }, + ], + value: () => '', + condition: { field: 'operation', value: 'wordpress_list_media' }, + }, + + // Comment Operations + { + id: 'commentPostId', + title: 'Post ID', + type: 'short-input', + placeholder: 'Post ID to comment on', + condition: { field: 'operation', value: 'wordpress_create_comment' }, + required: { field: 'operation', value: 'wordpress_create_comment' }, + }, + { + id: 'commentContent', + title: 'Comment Content', + type: 'long-input', + placeholder: 'Comment text', + condition: { + field: 'operation', + value: ['wordpress_create_comment', 'wordpress_update_comment'], + }, + required: { field: 'operation', value: 'wordpress_create_comment' }, + }, + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + placeholder: 'Enter comment ID', + condition: { + field: 'operation', + value: ['wordpress_update_comment', 'wordpress_delete_comment'], + }, + required: { + field: 'operation', + value: ['wordpress_update_comment', 'wordpress_delete_comment'], + }, + }, + { + id: 'commentStatus', + title: 'Comment Status', + type: 'dropdown', + options: [ + { label: 'Approved', id: 'approved' }, + { label: 'Hold', id: 'hold' }, + { label: 'Spam', id: 'spam' }, + { label: 'Trash', id: 'trash' }, + ], + value: () => 'approved', + condition: { field: 'operation', value: 'wordpress_update_comment' }, + }, + + // Category Operations + { + id: 'categoryName', + title: 'Category Name', + type: 'short-input', + placeholder: 'Category name', + condition: { field: 'operation', value: 'wordpress_create_category' }, + required: { field: 'operation', value: 'wordpress_create_category' }, + }, + { + id: 'categoryDescription', + title: 'Description', + type: 'long-input', + placeholder: 'Category description', + condition: { field: 'operation', value: 'wordpress_create_category' }, + }, + { + id: 'categoryParent', + title: 'Parent Category ID', + type: 'short-input', + placeholder: 'Parent category ID', + condition: { field: 'operation', value: 'wordpress_create_category' }, + }, + { + id: 'categorySlug', + title: 'Category Slug', + type: 'short-input', + placeholder: 'URL slug (optional)', + condition: { field: 'operation', value: 'wordpress_create_category' }, + }, + + // Tag Operations + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + placeholder: 'Tag name', + condition: { field: 'operation', value: 'wordpress_create_tag' }, + required: { field: 'operation', value: 'wordpress_create_tag' }, + }, + { + id: 'tagDescription', + title: 'Description', + type: 'long-input', + placeholder: 'Tag description', + condition: { field: 'operation', value: 'wordpress_create_tag' }, + }, + { + id: 'tagSlug', + title: 'Tag Slug', + type: 'short-input', + placeholder: 'URL slug (optional)', + condition: { field: 'operation', value: 'wordpress_create_tag' }, + }, + + // User Operations + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Enter user ID', + condition: { field: 'operation', value: 'wordpress_get_user' }, + required: { field: 'operation', value: 'wordpress_get_user' }, + }, + { + id: 'roles', + title: 'User Roles', + type: 'short-input', + placeholder: 'Comma-separated role names (e.g., administrator, editor)', + condition: { field: 'operation', value: 'wordpress_list_users' }, + }, + + // Search Operations + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Search keywords', + condition: { field: 'operation', value: 'wordpress_search_content' }, + required: { field: 'operation', value: 'wordpress_search_content' }, + }, + { + id: 'searchType', + title: 'Content Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Post', id: 'post' }, + { label: 'Page', id: 'page' }, + { label: 'Attachment', id: 'attachment' }, + ], + value: () => '', + condition: { field: 'operation', value: 'wordpress_search_content' }, + }, + + // List Operations - Common Parameters + { + id: 'perPage', + title: 'Results Per Page', + type: 'short-input', + placeholder: '10 (max 100)', + condition: { + field: 'operation', + value: [ + 'wordpress_list_posts', + 'wordpress_list_pages', + 'wordpress_list_media', + 'wordpress_list_comments', + 'wordpress_list_categories', + 'wordpress_list_tags', + 'wordpress_list_users', + 'wordpress_search_content', + ], + }, + }, + { + id: 'page', + title: 'Page Number', + type: 'short-input', + placeholder: '1', + condition: { + field: 'operation', + value: [ + 'wordpress_list_posts', + 'wordpress_list_pages', + 'wordpress_list_media', + 'wordpress_list_comments', + 'wordpress_list_categories', + 'wordpress_list_tags', + 'wordpress_list_users', + 'wordpress_search_content', + ], + }, + }, + { + id: 'search', + title: 'Search Filter', + type: 'short-input', + placeholder: 'Search term to filter results', + condition: { + field: 'operation', + value: [ + 'wordpress_list_posts', + 'wordpress_list_pages', + 'wordpress_list_media', + 'wordpress_list_comments', + 'wordpress_list_categories', + 'wordpress_list_tags', + 'wordpress_list_users', + ], + }, + }, + { + id: 'orderBy', + title: 'Order By', + type: 'dropdown', + options: [ + { label: 'Date', id: 'date' }, + { label: 'ID', id: 'id' }, + { label: 'Title', id: 'title' }, + { label: 'Slug', id: 'slug' }, + { label: 'Modified', id: 'modified' }, + ], + value: () => 'date', + condition: { + field: 'operation', + value: [ + 'wordpress_list_posts', + 'wordpress_list_pages', + 'wordpress_list_media', + 'wordpress_list_comments', + ], + }, + }, + { + id: 'order', + title: 'Order', + type: 'dropdown', + options: [ + { label: 'Descending', id: 'desc' }, + { label: 'Ascending', id: 'asc' }, + ], + value: () => 'desc', + condition: { + field: 'operation', + value: [ + 'wordpress_list_posts', + 'wordpress_list_pages', + 'wordpress_list_media', + 'wordpress_list_comments', + 'wordpress_list_categories', + 'wordpress_list_tags', + 'wordpress_list_users', + ], + }, + }, + + // List Posts - Status filter + { + id: 'listStatus', + title: 'Status Filter', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Published', id: 'publish' }, + { label: 'Draft', id: 'draft' }, + { label: 'Pending', id: 'pending' }, + { label: 'Private', id: 'private' }, + ], + value: () => '', + condition: { + field: 'operation', + value: ['wordpress_list_posts', 'wordpress_list_pages'], + }, + }, + + // Delete Operations - Force delete + { + id: 'force', + title: 'Force Delete', + type: 'switch', + condition: { + field: 'operation', + value: [ + 'wordpress_delete_post', + 'wordpress_delete_page', + 'wordpress_delete_media', + 'wordpress_delete_comment', + ], + }, + }, + + // Hide empty for taxonomies + { + id: 'hideEmpty', + title: 'Hide Empty', + type: 'switch', + condition: { + field: 'operation', + value: ['wordpress_list_categories', 'wordpress_list_tags'], + }, + }, + ], + tools: { + access: [ + 'wordpress_create_post', + 'wordpress_update_post', + 'wordpress_delete_post', + 'wordpress_get_post', + 'wordpress_list_posts', + 'wordpress_create_page', + 'wordpress_update_page', + 'wordpress_delete_page', + 'wordpress_get_page', + 'wordpress_list_pages', + 'wordpress_upload_media', + 'wordpress_get_media', + 'wordpress_list_media', + 'wordpress_delete_media', + 'wordpress_create_comment', + 'wordpress_list_comments', + 'wordpress_update_comment', + 'wordpress_delete_comment', + 'wordpress_create_category', + 'wordpress_list_categories', + 'wordpress_create_tag', + 'wordpress_list_tags', + 'wordpress_get_current_user', + 'wordpress_list_users', + 'wordpress_get_user', + 'wordpress_search_content', + ], + config: { + tool: (params) => params.operation || 'wordpress_create_post', + params: (params) => { + const baseParams = { + siteUrl: params.siteUrl, + username: params.username, + applicationPassword: params.applicationPassword, + } + + switch (params.operation) { + case 'wordpress_create_post': + return { + ...baseParams, + title: params.title, + content: params.content, + status: params.status, + excerpt: params.excerpt, + slug: params.slug, + categories: params.categories, + tags: params.tags, + featuredMedia: params.featuredMedia ? Number(params.featuredMedia) : undefined, + } + case 'wordpress_update_post': + return { + ...baseParams, + postId: Number(params.postId), + title: params.title, + content: params.content, + status: params.status, + excerpt: params.excerpt, + slug: params.slug, + categories: params.categories, + tags: params.tags, + featuredMedia: params.featuredMedia ? Number(params.featuredMedia) : undefined, + } + case 'wordpress_delete_post': + return { + ...baseParams, + postId: Number(params.postId), + force: params.force, + } + case 'wordpress_get_post': + return { + ...baseParams, + postId: Number(params.postId), + } + case 'wordpress_list_posts': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + status: params.listStatus || undefined, + search: params.search, + orderBy: params.orderBy, + order: params.order, + categories: params.categories, + tags: params.tags, + } + case 'wordpress_create_page': + return { + ...baseParams, + title: params.title, + content: params.content, + status: params.status, + excerpt: params.excerpt, + slug: params.slug, + parent: params.parent ? Number(params.parent) : undefined, + menuOrder: params.menuOrder ? Number(params.menuOrder) : undefined, + featuredMedia: params.featuredMedia ? Number(params.featuredMedia) : undefined, + } + case 'wordpress_update_page': + return { + ...baseParams, + pageId: Number(params.pageId), + title: params.title, + content: params.content, + status: params.status, + excerpt: params.excerpt, + slug: params.slug, + parent: params.parent ? Number(params.parent) : undefined, + menuOrder: params.menuOrder ? Number(params.menuOrder) : undefined, + featuredMedia: params.featuredMedia ? Number(params.featuredMedia) : undefined, + } + case 'wordpress_delete_page': + return { + ...baseParams, + pageId: Number(params.pageId), + force: params.force, + } + case 'wordpress_get_page': + return { + ...baseParams, + pageId: Number(params.pageId), + } + case 'wordpress_list_pages': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + status: params.listStatus || undefined, + search: params.search, + orderBy: params.orderBy, + order: params.order, + parent: params.parent ? Number(params.parent) : undefined, + } + case 'wordpress_upload_media': + return { + ...baseParams, + file: params.file, + filename: params.filename, + title: params.mediaTitle, + caption: params.caption, + altText: params.altText, + } + case 'wordpress_get_media': + return { + ...baseParams, + mediaId: Number(params.mediaId), + } + case 'wordpress_list_media': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + search: params.search, + mediaType: params.mediaType || undefined, + orderBy: params.orderBy, + order: params.order, + } + case 'wordpress_delete_media': + return { + ...baseParams, + mediaId: Number(params.mediaId), + force: params.force, + } + case 'wordpress_create_comment': + return { + ...baseParams, + postId: Number(params.commentPostId), + content: params.commentContent, + } + case 'wordpress_list_comments': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + postId: params.commentPostId ? Number(params.commentPostId) : undefined, + search: params.search, + orderBy: params.orderBy, + order: params.order, + } + case 'wordpress_update_comment': + return { + ...baseParams, + commentId: Number(params.commentId), + content: params.commentContent, + status: params.commentStatus, + } + case 'wordpress_delete_comment': + return { + ...baseParams, + commentId: Number(params.commentId), + force: params.force, + } + case 'wordpress_create_category': + return { + ...baseParams, + name: params.categoryName, + description: params.categoryDescription, + parent: params.categoryParent ? Number(params.categoryParent) : undefined, + slug: params.categorySlug, + } + case 'wordpress_list_categories': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + search: params.search, + hideEmpty: params.hideEmpty, + orderBy: params.orderBy, + order: params.order, + } + case 'wordpress_create_tag': + return { + ...baseParams, + name: params.tagName, + description: params.tagDescription, + slug: params.tagSlug, + } + case 'wordpress_list_tags': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + search: params.search, + hideEmpty: params.hideEmpty, + orderBy: params.orderBy, + order: params.order, + } + case 'wordpress_get_current_user': + return baseParams + case 'wordpress_list_users': + return { + ...baseParams, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + search: params.search, + roles: params.roles, + orderBy: params.orderBy, + order: params.order, + } + case 'wordpress_get_user': + return { + ...baseParams, + userId: Number(params.userId), + } + case 'wordpress_search_content': + return { + ...baseParams, + query: params.query, + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + type: params.searchType || undefined, + } + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + siteUrl: { type: 'string', description: 'WordPress site URL' }, + username: { type: 'string', description: 'WordPress username' }, + applicationPassword: { type: 'string', description: 'WordPress Application Password' }, + // Post inputs + postId: { type: 'number', description: 'Post ID' }, + title: { type: 'string', description: 'Post or page title' }, + content: { type: 'string', description: 'Post or page content' }, + status: { type: 'string', description: 'Post or page status' }, + excerpt: { type: 'string', description: 'Post or page excerpt' }, + slug: { type: 'string', description: 'URL slug' }, + categories: { type: 'string', description: 'Category IDs (comma-separated)' }, + tags: { type: 'string', description: 'Tag IDs (comma-separated)' }, + featuredMedia: { type: 'number', description: 'Featured media ID' }, + // Page inputs + pageId: { type: 'number', description: 'Page ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menuOrder: { type: 'number', description: 'Menu order' }, + // Media inputs + file: { type: 'string', description: 'File data (base64) or URL' }, + filename: { type: 'string', description: 'Filename with extension' }, + mediaTitle: { type: 'string', description: 'Media title' }, + caption: { type: 'string', description: 'Media caption' }, + altText: { type: 'string', description: 'Alt text' }, + mediaId: { type: 'number', description: 'Media ID' }, + mediaType: { type: 'string', description: 'Media type filter' }, + // Comment inputs + commentPostId: { type: 'number', description: 'Post ID for comment' }, + commentContent: { type: 'string', description: 'Comment content' }, + commentId: { type: 'number', description: 'Comment ID' }, + commentStatus: { type: 'string', description: 'Comment status' }, + // Category inputs + categoryName: { type: 'string', description: 'Category name' }, + categoryDescription: { type: 'string', description: 'Category description' }, + categoryParent: { type: 'number', description: 'Parent category ID' }, + categorySlug: { type: 'string', description: 'Category slug' }, + // Tag inputs + tagName: { type: 'string', description: 'Tag name' }, + tagDescription: { type: 'string', description: 'Tag description' }, + tagSlug: { type: 'string', description: 'Tag slug' }, + // User inputs + userId: { type: 'number', description: 'User ID' }, + roles: { type: 'string', description: 'User roles filter' }, + // Search inputs + query: { type: 'string', description: 'Search query' }, + searchType: { type: 'string', description: 'Content type filter' }, + // List inputs + perPage: { type: 'number', description: 'Results per page' }, + page: { type: 'number', description: 'Page number' }, + search: { type: 'string', description: 'Search filter' }, + orderBy: { type: 'string', description: 'Order by field' }, + order: { type: 'string', description: 'Order direction' }, + listStatus: { type: 'string', description: 'Status filter' }, + force: { type: 'boolean', description: 'Force delete' }, + hideEmpty: { type: 'boolean', description: 'Hide empty taxonomies' }, + }, + outputs: { + // Post outputs + post: { type: 'json', description: 'Post data' }, + posts: { type: 'json', description: 'List of posts' }, + // Page outputs + page: { type: 'json', description: 'Page data' }, + pages: { type: 'json', description: 'List of pages' }, + // Media outputs + media: { type: 'json', description: 'Media data' }, + // Comment outputs + comment: { type: 'json', description: 'Comment data' }, + comments: { type: 'json', description: 'List of comments' }, + // Category outputs + category: { type: 'json', description: 'Category data' }, + categories: { type: 'json', description: 'List of categories' }, + // Tag outputs + tag: { type: 'json', description: 'Tag data' }, + // User outputs + user: { type: 'json', description: 'User data' }, + users: { type: 'json', description: 'List of users' }, + // Search outputs + results: { type: 'json', description: 'Search results' }, + // Common outputs + deleted: { type: 'boolean', description: 'Deletion status' }, + total: { type: 'number', description: 'Total count' }, + totalPages: { type: 'number', description: 'Total pages' }, + }, +} diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts new file mode 100644 index 00000000000..1a3cd0d166e --- /dev/null +++ b/apps/sim/blocks/blocks/zoom.ts @@ -0,0 +1,453 @@ +import { ZoomIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { ZoomResponse } from '@/tools/zoom/types' + +export const ZoomBlock: BlockConfig = { + type: 'zoom', + name: 'Zoom', + description: 'Create and manage Zoom meetings', + authMode: AuthMode.OAuth, + longDescription: + 'Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, and manage meeting settings programmatically.', + docsLink: 'https://docs.sim.ai/tools/zoom', + category: 'tools', + bgColor: '#2D8CFF', + icon: ZoomIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Meeting', id: 'zoom_create_meeting' }, + { label: 'List Meetings', id: 'zoom_list_meetings' }, + { label: 'Get Meeting', id: 'zoom_get_meeting' }, + { label: 'Update Meeting', id: 'zoom_update_meeting' }, + { label: 'Delete Meeting', id: 'zoom_delete_meeting' }, + { label: 'Get Meeting Invitation', id: 'zoom_get_meeting_invitation' }, + ], + value: () => 'zoom_create_meeting', + }, + { + id: 'credential', + title: 'Zoom Account', + type: 'oauth-input', + serviceId: 'zoom', + requiredScopes: [ + 'meeting:write:meeting:admin', + 'meeting:read:meeting:admin', + 'meeting:read:list_meetings:admin', + 'meeting:update:meeting:admin', + 'meeting:delete:meeting:admin', + ], + placeholder: 'Select Zoom account', + required: true, + }, + // User ID for create/list operations + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'me (or user ID/email)', + required: true, + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_list_meetings'], + }, + }, + // Meeting ID for get/update/delete/invitation operations + { + id: 'meetingId', + title: 'Meeting ID', + type: 'short-input', + placeholder: 'Enter meeting ID', + required: true, + condition: { + field: 'operation', + value: [ + 'zoom_get_meeting', + 'zoom_update_meeting', + 'zoom_delete_meeting', + 'zoom_get_meeting_invitation', + ], + }, + }, + // Topic for create/update + { + id: 'topic', + title: 'Topic', + type: 'short-input', + placeholder: 'Meeting topic', + required: true, + condition: { + field: 'operation', + value: ['zoom_create_meeting'], + }, + }, + { + id: 'topicUpdate', + title: 'Topic', + type: 'short-input', + placeholder: 'Meeting topic (optional)', + condition: { + field: 'operation', + value: ['zoom_update_meeting'], + }, + }, + // Meeting type + { + id: 'type', + title: 'Meeting Type', + type: 'dropdown', + options: [ + { label: 'Scheduled', id: '2' }, + { label: 'Instant', id: '1' }, + { label: 'Recurring (no fixed time)', id: '3' }, + { label: 'Recurring (fixed time)', id: '8' }, + ], + value: () => '2', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Start time + { + id: 'startTime', + title: 'Start Time', + type: 'short-input', + placeholder: '2025-06-03T10:00:00Z', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Duration + { + id: 'duration', + title: 'Duration (minutes)', + type: 'short-input', + placeholder: '30', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Timezone + { + id: 'timezone', + title: 'Timezone', + type: 'short-input', + placeholder: 'America/Los_Angeles', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Password + { + id: 'password', + title: 'Password', + type: 'short-input', + placeholder: 'Meeting password', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Agenda + { + id: 'agenda', + title: 'Agenda', + type: 'long-input', + placeholder: 'Meeting agenda', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // Meeting settings + { + id: 'hostVideo', + title: 'Host Video', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + { + id: 'participantVideo', + title: 'Participant Video', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + { + id: 'joinBeforeHost', + title: 'Join Before Host', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + { + id: 'muteUponEntry', + title: 'Mute Upon Entry', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + { + id: 'waitingRoom', + title: 'Waiting Room', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + { + id: 'autoRecording', + title: 'Auto Recording', + type: 'dropdown', + options: [ + { label: 'None', id: 'none' }, + { label: 'Local', id: 'local' }, + { label: 'Cloud', id: 'cloud' }, + ], + value: () => 'none', + condition: { + field: 'operation', + value: ['zoom_create_meeting', 'zoom_update_meeting'], + }, + }, + // List meetings filter + { + id: 'listType', + title: 'Meeting Type Filter', + type: 'dropdown', + options: [ + { label: 'Scheduled', id: 'scheduled' }, + { label: 'Live', id: 'live' }, + { label: 'Upcoming', id: 'upcoming' }, + { label: 'Upcoming Meetings', id: 'upcoming_meetings' }, + { label: 'Previous Meetings', id: 'previous_meetings' }, + ], + value: () => 'scheduled', + condition: { + field: 'operation', + value: ['zoom_list_meetings'], + }, + }, + // Pagination + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: 'Number of results (max 300)', + condition: { + field: 'operation', + value: ['zoom_list_meetings'], + }, + }, + { + id: 'nextPageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for next page', + condition: { + field: 'operation', + value: ['zoom_list_meetings'], + }, + }, + // Delete options + { + id: 'occurrenceId', + title: 'Occurrence ID', + type: 'short-input', + placeholder: 'For recurring meetings', + condition: { + field: 'operation', + value: ['zoom_get_meeting', 'zoom_delete_meeting'], + }, + }, + { + id: 'cancelMeetingReminder', + title: 'Send Cancellation Email', + type: 'switch', + condition: { + field: 'operation', + value: ['zoom_delete_meeting'], + }, + }, + ], + tools: { + access: [ + 'zoom_create_meeting', + 'zoom_list_meetings', + 'zoom_get_meeting', + 'zoom_update_meeting', + 'zoom_delete_meeting', + 'zoom_get_meeting_invitation', + ], + config: { + tool: (params) => { + return params.operation || 'zoom_create_meeting' + }, + params: (params) => { + const baseParams: Record = { + credential: params.credential, + } + + switch (params.operation) { + case 'zoom_create_meeting': + if (!params.userId?.trim()) { + throw new Error('User ID is required.') + } + if (!params.topic?.trim()) { + throw new Error('Topic is required.') + } + return { + ...baseParams, + userId: params.userId.trim(), + topic: params.topic.trim(), + type: params.type ? Number(params.type) : 2, + startTime: params.startTime, + duration: params.duration ? Number(params.duration) : undefined, + timezone: params.timezone, + password: params.password, + agenda: params.agenda, + hostVideo: params.hostVideo, + participantVideo: params.participantVideo, + joinBeforeHost: params.joinBeforeHost, + muteUponEntry: params.muteUponEntry, + waitingRoom: params.waitingRoom, + autoRecording: params.autoRecording !== 'none' ? params.autoRecording : undefined, + } + + case 'zoom_list_meetings': + if (!params.userId?.trim()) { + throw new Error('User ID is required.') + } + return { + ...baseParams, + userId: params.userId.trim(), + type: params.listType, + pageSize: params.pageSize ? Number(params.pageSize) : undefined, + nextPageToken: params.nextPageToken, + } + + case 'zoom_get_meeting': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + occurrenceId: params.occurrenceId, + } + + case 'zoom_update_meeting': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + topic: params.topicUpdate, + type: params.type ? Number(params.type) : undefined, + startTime: params.startTime, + duration: params.duration ? Number(params.duration) : undefined, + timezone: params.timezone, + password: params.password, + agenda: params.agenda, + hostVideo: params.hostVideo, + participantVideo: params.participantVideo, + joinBeforeHost: params.joinBeforeHost, + muteUponEntry: params.muteUponEntry, + waitingRoom: params.waitingRoom, + autoRecording: params.autoRecording !== 'none' ? params.autoRecording : undefined, + } + + case 'zoom_delete_meeting': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + occurrenceId: params.occurrenceId, + cancelMeetingReminder: params.cancelMeetingReminder, + } + + case 'zoom_get_meeting_invitation': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + } + + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Zoom access token' }, + userId: { type: 'string', description: 'User ID or email (use "me" for authenticated user)' }, + meetingId: { type: 'string', description: 'Meeting ID' }, + topic: { type: 'string', description: 'Meeting topic' }, + topicUpdate: { type: 'string', description: 'Meeting topic for update' }, + type: { type: 'string', description: 'Meeting type' }, + startTime: { type: 'string', description: 'Start time in ISO 8601 format' }, + duration: { type: 'string', description: 'Duration in minutes' }, + timezone: { type: 'string', description: 'Timezone' }, + password: { type: 'string', description: 'Meeting password' }, + agenda: { type: 'string', description: 'Meeting agenda' }, + hostVideo: { type: 'boolean', description: 'Host video on' }, + participantVideo: { type: 'boolean', description: 'Participant video on' }, + joinBeforeHost: { type: 'boolean', description: 'Allow join before host' }, + muteUponEntry: { type: 'boolean', description: 'Mute upon entry' }, + waitingRoom: { type: 'boolean', description: 'Enable waiting room' }, + autoRecording: { type: 'string', description: 'Auto recording setting' }, + listType: { type: 'string', description: 'Meeting type filter for list' }, + pageSize: { type: 'string', description: 'Page size for list' }, + nextPageToken: { type: 'string', description: 'Page token for pagination' }, + occurrenceId: { type: 'string', description: 'Occurrence ID for recurring meetings' }, + cancelMeetingReminder: { type: 'boolean', description: 'Send cancellation email' }, + }, + outputs: { + // Meeting outputs + meeting: { type: 'json', description: 'Meeting data' }, + meetings: { type: 'json', description: 'List of meetings' }, + // Specific meeting fields + id: { type: 'number', description: 'Meeting ID' }, + uuid: { type: 'string', description: 'Meeting UUID' }, + topic: { type: 'string', description: 'Meeting topic' }, + join_url: { type: 'string', description: 'Join URL for participants' }, + start_url: { type: 'string', description: 'Start URL for host' }, + start_time: { type: 'string', description: 'Start time' }, + duration: { type: 'number', description: 'Duration in minutes' }, + timezone: { type: 'string', description: 'Timezone' }, + password: { type: 'string', description: 'Meeting password' }, + agenda: { type: 'string', description: 'Meeting agenda' }, + settings: { type: 'json', description: 'Meeting settings' }, + // Invitation + invitation: { type: 'string', description: 'Meeting invitation text' }, + // Pagination + pageInfo: { type: 'json', description: 'Pagination information' }, + // Success indicator + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index d0353d797a9..daacbf6218f 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -1,4 +1,5 @@ import { AgentBlock } from '@/blocks/blocks/agent' +import { AhrefsBlock } from '@/blocks/blocks/ahrefs' import { AirtableBlock } from '@/blocks/blocks/airtable' import { ApiBlock } from '@/blocks/blocks/api' import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger' @@ -6,14 +7,18 @@ import { ApifyBlock } from '@/blocks/blocks/apify' import { ApolloBlock } from '@/blocks/blocks/apollo' import { ArxivBlock } from '@/blocks/blocks/arxiv' import { AsanaBlock } from '@/blocks/blocks/asana' +import { BoxBlock } from '@/blocks/blocks/box' import { BrowserUseBlock } from '@/blocks/blocks/browser_use' import { CalendlyBlock } from '@/blocks/blocks/calendly' import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger' import { ClayBlock } from '@/blocks/blocks/clay' import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock } from '@/blocks/blocks/confluence' +import { DatadogBlock } from '@/blocks/blocks/datadog' import { DiscordBlock } from '@/blocks/blocks/discord' +import { DropboxBlock } from '@/blocks/blocks/dropbox' import { DynamoDBBlock } from '@/blocks/blocks/dynamodb' +import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch' import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs' import { EvaluatorBlock } from '@/blocks/blocks/evaluator' import { ExaBlock } from '@/blocks/blocks/exa' @@ -22,6 +27,7 @@ import { FirecrawlBlock } from '@/blocks/blocks/firecrawl' import { FunctionBlock } from '@/blocks/blocks/function' import { GenericWebhookBlock } from '@/blocks/blocks/generic_webhook' import { GitHubBlock } from '@/blocks/blocks/github' +import { GitLabBlock } from '@/blocks/blocks/gitlab' import { GmailBlock } from '@/blocks/blocks/gmail' import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleCalendarBlock } from '@/blocks/blocks/google_calendar' @@ -30,6 +36,7 @@ import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_form' import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets' import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' +import { GrafanaBlock } from '@/blocks/blocks/grafana' import { GuardrailsBlock } from '@/blocks/blocks/guardrails' import { HubSpotBlock } from '@/blocks/blocks/hubspot' import { HuggingFaceBlock } from '@/blocks/blocks/huggingface' @@ -84,8 +91,10 @@ import { SendGridBlock } from '@/blocks/blocks/sendgrid' import { SentryBlock } from '@/blocks/blocks/sentry' import { SerperBlock } from '@/blocks/blocks/serper' import { SharepointBlock } from '@/blocks/blocks/sharepoint' +import { ShopifyBlock } from '@/blocks/blocks/shopify' import { SlackBlock } from '@/blocks/blocks/slack' import { SmtpBlock } from '@/blocks/blocks/smtp' +import { SSHBlock } from '@/blocks/blocks/ssh' import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' @@ -111,17 +120,20 @@ import { WebflowBlock } from '@/blocks/blocks/webflow' import { WebhookBlock } from '@/blocks/blocks/webhook' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' +import { WordPressBlock } from '@/blocks/blocks/wordpress' import { WorkflowBlock } from '@/blocks/blocks/workflow' import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input' import { XBlock } from '@/blocks/blocks/x' import { YouTubeBlock } from '@/blocks/blocks/youtube' import { ZendeskBlock } from '@/blocks/blocks/zendesk' import { ZepBlock } from '@/blocks/blocks/zep' +import { ZoomBlock } from '@/blocks/blocks/zoom' import type { BlockConfig } from '@/blocks/types' // Registry of all available blocks, alphabetically sorted export const registry: Record = { agent: AgentBlock, + ahrefs: AhrefsBlock, airtable: AirtableBlock, api: ApiBlock, api_trigger: ApiTriggerBlock, @@ -129,14 +141,18 @@ export const registry: Record = { apollo: ApolloBlock, arxiv: ArxivBlock, asana: AsanaBlock, + box: BoxBlock, browser_use: BrowserUseBlock, calendly: CalendlyBlock, chat_trigger: ChatTriggerBlock, clay: ClayBlock, condition: ConditionBlock, confluence: ConfluenceBlock, + datadog: DatadogBlock, discord: DiscordBlock, + dropbox: DropboxBlock, elevenlabs: ElevenLabsBlock, + elasticsearch: ElasticsearchBlock, evaluator: EvaluatorBlock, exa: ExaBlock, file: FileBlock, @@ -144,7 +160,9 @@ export const registry: Record = { function: FunctionBlock, generic_webhook: GenericWebhookBlock, github: GitHubBlock, + gitlab: GitLabBlock, gmail: GmailBlock, + grafana: GrafanaBlock, guardrails: GuardrailsBlock, google_calendar: GoogleCalendarBlock, google_docs: GoogleDocsBlock, @@ -207,8 +225,10 @@ export const registry: Record = { sentry: SentryBlock, serper: SerperBlock, sharepoint: SharepointBlock, + shopify: ShopifyBlock, slack: SlackBlock, smtp: SmtpBlock, + ssh: SSHBlock, stagehand: StagehandBlock, stagehand_agent: StagehandAgentBlock, starter: StarterBlock, @@ -234,12 +254,14 @@ export const registry: Record = { webhook: WebhookBlock, whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, + wordpress: WordPressBlock, workflow: WorkflowBlock, workflow_input: WorkflowInputBlock, x: XBlock, youtube: YouTubeBlock, zep: ZepBlock, zendesk: ZendeskBlock, + zoom: ZoomBlock, } export const getBlock = (type: string): BlockConfig | undefined => registry[type] diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index e8d25497564..0da6f731c16 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -473,6 +473,30 @@ export function GithubIcon(props: SVGProps) { ) } +export function GitLabIcon(props: SVGProps) { + return ( + + + + + + + + + + + ) +} + export function SerperIcon(props: SVGProps) { return ( @@ -649,6 +673,37 @@ export function GmailIcon(props: SVGProps) { ) } +export function GrafanaIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function GoogleDriveIcon(props: SVGProps) { return ( ) { export function TwilioIcon(props: SVGProps) { return ( - + ) { export function LinkupIcon(props: SVGProps) { return ( - - + + + + ) } @@ -3583,6 +3630,22 @@ export function ZendeskIcon(props: SVGProps) { ) } +export function ZoomIcon(props: SVGProps) { + return ( + + + + ) +} + export function PylonIcon(props: SVGProps) { return ( ) { ) } +export function SshIcon(props: SVGProps) { + return ( + + + + + + + + + ) +} + export function ApifyIcon(props: SVGProps) { return ( ) { ) } + +export function WordpressIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +export function AhRefsIcon(props: SVGProps) { + return ( + + + + ) +} + +export function ShopifyIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function BoxCompanyIcon(props: SVGProps) { + return ( + + + + ) +} + +export function DropboxIcon(props: SVGProps) { + return ( + + + + ) +} + +export function ElasticsearchIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function GitlabIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +export function SSHIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function DatadogIcon(props: SVGProps) { + return ( + + + + + + + + ) +} diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 650d9acd085..c79036a60e7 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1423,6 +1423,66 @@ export const auth = betterAuth({ }, }, + { + providerId: 'dropbox', + clientId: env.DROPBOX_CLIENT_ID as string, + clientSecret: env.DROPBOX_CLIENT_SECRET as string, + authorizationUrl: 'https://www.dropbox.com/oauth2/authorize', + tokenUrl: 'https://api.dropboxapi.com/oauth2/token', + scopes: [ + 'account_info.read', + 'files.metadata.read', + 'files.metadata.write', + 'files.content.read', + 'files.content.write', + 'sharing.read', + 'sharing.write', + ], + responseType: 'code', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/dropbox`, + pkce: true, + accessType: 'offline', + prompt: 'consent', + getUserInfo: async (tokens) => { + try { + const response = await fetch( + 'https://api.dropboxapi.com/2/users/get_current_account', + { + method: 'POST', + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + } + ) + + if (!response.ok) { + const errorText = await response.text() + logger.error('Dropbox API error:', { + status: response.status, + statusText: response.statusText, + body: errorText, + }) + throw new Error(`Dropbox API error: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + + return { + id: data.account_id, + email: data.email, + name: data.name?.display_name || data.email, + emailVerified: data.email_verified || false, + createdAt: new Date(), + updatedAt: new Date(), + image: data.profile_photo_url || undefined, + } + } catch (error) { + logger.error('Error in getUserInfo:', error) + throw error + } + }, + }, + { providerId: 'asana', clientId: env.ASANA_CLIENT_ID as string, diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 66cdb0ad47c..211b771ec9d 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -181,6 +181,8 @@ export const env = createEnv({ GITHUB_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret GITHUB_REPO_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for repo access GITHUB_REPO_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret for repo access + GITLAB_CLIENT_ID: z.string().optional(), // GitLab OAuth client ID for GitLab integration + GITLAB_CLIENT_SECRET: z.string().optional(), // GitLab OAuth client secret X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret CONFLUENCE_CLIENT_ID: z.string().optional(), // Atlassian Confluence OAuth client ID @@ -210,6 +212,8 @@ export const env = createEnv({ PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret + DROPBOX_CLIENT_ID: z.string().optional(), // Dropbox OAuth client ID + DROPBOX_CLIENT_SECRET: z.string().optional(), // Dropbox OAuth client secret SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID @@ -219,6 +223,10 @@ export const env = createEnv({ TRELLO_API_KEY: z.string().optional(), // Trello API Key LINKEDIN_CLIENT_ID: z.string().optional(), // LinkedIn OAuth client ID LINKEDIN_CLIENT_SECRET: z.string().optional(), // LinkedIn OAuth client secret + SHOPIFY_CLIENT_ID: z.string().optional(), // Shopify OAuth client ID + SHOPIFY_CLIENT_SECRET: z.string().optional(), // Shopify OAuth client secret + BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID + BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 69d65ffc88e..40d82aac24e 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -2,9 +2,12 @@ import type { ReactNode } from 'react' import { AirtableIcon, AsanaIcon, + BoxCompanyIcon, ConfluenceIcon, // DiscordIcon, + DropboxIcon, GithubIcon, + GitLabIcon, GmailIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -27,12 +30,14 @@ import { PipedriveIcon, RedditIcon, SalesforceIcon, + ShopifyIcon, SlackIcon, // SupabaseIcon, TrelloIcon, WealthboxIcon, WebflowIcon, xIcon, + ZoomIcon, } from '@/components/icons' import { env } from '@/lib/core/config/env' import { createLogger } from '@/lib/logs/console/logger' @@ -42,6 +47,7 @@ const logger = createLogger('OAuth') export type OAuthProvider = | 'google' | 'github' + | 'gitlab' | 'x' // | 'supabase' | 'confluence' @@ -49,6 +55,7 @@ export type OAuthProvider = | 'notion' | 'jira' // | 'discord' + | 'dropbox' | 'microsoft' | 'linear' | 'slack' @@ -61,6 +68,9 @@ export type OAuthProvider = | 'hubspot' | 'salesforce' | 'linkedin' + | 'shopify' + | 'box' + | 'zoom' | string export type OAuthService = @@ -73,6 +83,7 @@ export type OAuthService = | 'google-vault' | 'google-forms' | 'github' + | 'gitlab' | 'x' // | 'supabase' | 'confluence' @@ -80,6 +91,7 @@ export type OAuthService = | 'notion' | 'jira' // | 'discord' + | 'dropbox' | 'microsoft-excel' | 'microsoft-teams' | 'microsoft-planner' @@ -97,6 +109,9 @@ export type OAuthService = | 'hubspot' | 'salesforce' | 'linkedin' + | 'shopify' + | 'box' + | 'zoom' export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -340,6 +355,23 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'github', }, + gitlab: { + id: 'gitlab', + name: 'GitLab', + icon: (props) => GitLabIcon(props), + services: { + gitlab: { + id: 'gitlab', + name: 'GitLab', + description: 'Manage GitLab projects, issues, merge requests, and pipelines.', + providerId: 'gitlab', + icon: (props) => GitLabIcon(props), + baseProviderIcon: (props) => GitLabIcon(props), + scopes: ['api', 'read_user', 'read_repository', 'write_repository'], + }, + }, + defaultService: 'gitlab', + }, x: { id: 'x', name: 'X', @@ -538,6 +570,59 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'linear', }, + dropbox: { + id: 'dropbox', + name: 'Dropbox', + icon: (props) => DropboxIcon(props), + services: { + dropbox: { + id: 'dropbox', + name: 'Dropbox', + description: 'Upload, download, share, and manage files in Dropbox.', + providerId: 'dropbox', + icon: (props) => DropboxIcon(props), + baseProviderIcon: (props) => DropboxIcon(props), + scopes: [ + 'account_info.read', + 'files.metadata.read', + 'files.metadata.write', + 'files.content.read', + 'files.content.write', + 'sharing.read', + 'sharing.write', + ], + }, + }, + defaultService: 'dropbox', + }, + shopify: { + id: 'shopify', + name: 'Shopify', + icon: (props) => ShopifyIcon(props), + services: { + shopify: { + id: 'shopify', + name: 'Shopify', + description: 'Manage products, orders, and customers in your Shopify store.', + providerId: 'shopify', + icon: (props) => ShopifyIcon(props), + baseProviderIcon: (props) => ShopifyIcon(props), + scopes: [ + 'read_products', + 'write_products', + 'read_orders', + 'write_orders', + 'read_customers', + 'write_customers', + 'read_inventory', + 'write_inventory', + 'read_fulfillments', + 'write_fulfillments', + ], + }, + }, + defaultService: 'shopify', + }, slack: { id: 'slack', name: 'Slack', @@ -769,6 +854,46 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'salesforce', }, + box: { + id: 'box', + name: 'Box', + icon: (props) => BoxCompanyIcon(props), + services: { + box: { + id: 'box', + name: 'Box', + description: 'Manage files and folders in Box cloud storage.', + providerId: 'box', + icon: (props) => BoxCompanyIcon(props), + baseProviderIcon: (props) => BoxCompanyIcon(props), + scopes: ['root_readwrite'], + }, + }, + defaultService: 'box', + }, + zoom: { + id: 'zoom', + name: 'Zoom', + icon: (props) => ZoomIcon(props), + services: { + zoom: { + id: 'zoom', + name: 'Zoom', + description: 'Create and manage Zoom meetings, users, and recordings.', + providerId: 'zoom', + icon: (props) => ZoomIcon(props), + baseProviderIcon: (props) => ZoomIcon(props), + scopes: [ + 'meeting:write:meeting:admin', + 'meeting:read:meeting:admin', + 'meeting:read:list_meetings:admin', + 'meeting:update:meeting:admin', + 'meeting:delete:meeting:admin', + ], + }, + }, + defaultService: 'zoom', + }, } export function getServiceByProviderAndId( @@ -1008,6 +1133,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { additionalHeaders: { Accept: 'application/json' }, } } + case 'gitlab': { + const { clientId, clientSecret } = getCredentials( + env.GITLAB_CLIENT_ID, + env.GITLAB_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://gitlab.com/oauth/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: true, + } + } case 'x': { const { clientId, clientSecret } = getCredentials(env.X_CLIENT_ID, env.X_CLIENT_SECRET) return { @@ -1149,6 +1287,18 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { useBasicAuth: true, } } + case 'dropbox': { + const { clientId, clientSecret } = getCredentials( + env.DROPBOX_CLIENT_ID, + env.DROPBOX_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://api.dropboxapi.com/oauth2/token', + clientId, + clientSecret, + useBasicAuth: false, + } + } case 'slack': { const { clientId, clientSecret } = getCredentials( env.SLACK_CLIENT_ID, @@ -1265,6 +1415,31 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'shopify': { + // Shopify access tokens don't expire and don't support refresh tokens + // This configuration is provided for completeness but won't be used for token refresh + const { clientId, clientSecret } = getCredentials( + env.SHOPIFY_CLIENT_ID, + env.SHOPIFY_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://accounts.shopify.com/oauth/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: false, + } + } + case 'box': { + const { clientId, clientSecret } = getCredentials(env.BOX_CLIENT_ID, env.BOX_CLIENT_SECRET) + return { + tokenEndpoint: 'https://api.box.com/oauth2/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: true, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/package.json b/apps/sim/package.json index 44ead7cd234..12d1cdfa5d1 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -122,6 +122,7 @@ "resend": "^4.1.2", "sharp": "0.34.3", "socket.io": "^4.8.1", + "ssh2": "^1.17.0", "stripe": "18.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", @@ -144,6 +145,7 @@ "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", + "@types/ssh2": "^1.15.5", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.0.8", "autoprefixer": "10.4.21", diff --git a/apps/sim/tools/ahrefs/backlinks.ts b/apps/sim/tools/ahrefs/backlinks.ts new file mode 100644 index 00000000000..81a9bd35702 --- /dev/null +++ b/apps/sim/tools/ahrefs/backlinks.ts @@ -0,0 +1,107 @@ +import type { AhrefsBacklinksParams, AhrefsBacklinksResponse } from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const backlinksTool: ToolConfig = { + id: 'ahrefs_backlinks', + name: 'Ahrefs Backlinks', + description: + 'Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain or URL to analyze', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results to skip for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks') + url.searchParams.set('target', params.target) + if (params.mode) url.searchParams.set('mode', params.mode) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.offset) url.searchParams.set('offset', String(params.offset)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get backlinks') + } + + const backlinks = (data.backlinks || []).map((link: any) => ({ + urlFrom: link.url_from || '', + urlTo: link.url_to || '', + anchor: link.anchor || '', + domainRatingSource: link.domain_rating_source ?? 0, + isDofollow: link.is_dofollow ?? false, + firstSeen: link.first_seen || '', + lastVisited: link.last_visited || '', + })) + + return { + success: true, + output: { + backlinks, + }, + } + }, + + outputs: { + backlinks: { + type: 'array', + description: 'List of backlinks pointing to the target', + items: { + type: 'object', + properties: { + urlFrom: { type: 'string', description: 'The URL of the page containing the backlink' }, + urlTo: { type: 'string', description: 'The URL being linked to' }, + anchor: { type: 'string', description: 'The anchor text of the link' }, + domainRatingSource: { + type: 'number', + description: 'Domain Rating of the linking domain', + }, + isDofollow: { type: 'boolean', description: 'Whether the link is dofollow' }, + firstSeen: { type: 'string', description: 'When the backlink was first discovered' }, + lastVisited: { type: 'string', description: 'When the backlink was last checked' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/backlinks_stats.ts b/apps/sim/tools/ahrefs/backlinks_stats.ts new file mode 100644 index 00000000000..0c363534024 --- /dev/null +++ b/apps/sim/tools/ahrefs/backlinks_stats.ts @@ -0,0 +1,86 @@ +import type { AhrefsBacklinksStatsParams, AhrefsBacklinksStatsResponse } from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const backlinksStatsTool: ToolConfig< + AhrefsBacklinksStatsParams, + AhrefsBacklinksStatsResponse +> = { + id: 'ahrefs_backlinks_stats', + name: 'Ahrefs Backlinks Stats', + description: + 'Get backlink statistics for a target domain or URL. Returns totals for different backlink types including dofollow, nofollow, text, image, and redirect links.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain or URL to analyze', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks-stats') + url.searchParams.set('target', params.target) + if (params.mode) url.searchParams.set('mode', params.mode) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get backlinks stats') + } + + return { + success: true, + output: { + stats: { + total: data.live ?? 0, + dofollow: data.live_dofollow ?? 0, + nofollow: data.live_nofollow ?? 0, + text: data.text ?? 0, + image: data.image ?? 0, + redirect: data.redirect ?? 0, + }, + }, + } + }, + + outputs: { + stats: { + type: 'object', + description: 'Backlink statistics summary', + properties: { + total: { type: 'number', description: 'Total number of live backlinks' }, + dofollow: { type: 'number', description: 'Number of dofollow backlinks' }, + nofollow: { type: 'number', description: 'Number of nofollow backlinks' }, + text: { type: 'number', description: 'Number of text backlinks' }, + image: { type: 'number', description: 'Number of image backlinks' }, + redirect: { type: 'number', description: 'Number of redirect backlinks' }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/broken_backlinks.ts b/apps/sim/tools/ahrefs/broken_backlinks.ts new file mode 100644 index 00000000000..f85e4452e48 --- /dev/null +++ b/apps/sim/tools/ahrefs/broken_backlinks.ts @@ -0,0 +1,112 @@ +import type { + AhrefsBrokenBacklinksParams, + AhrefsBrokenBacklinksResponse, +} from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const brokenBacklinksTool: ToolConfig< + AhrefsBrokenBacklinksParams, + AhrefsBrokenBacklinksResponse +> = { + id: 'ahrefs_broken_backlinks', + name: 'Ahrefs Broken Backlinks', + description: + 'Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain or URL to analyze', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results to skip for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/broken-backlinks') + url.searchParams.set('target', params.target) + if (params.mode) url.searchParams.set('mode', params.mode) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.offset) url.searchParams.set('offset', String(params.offset)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get broken backlinks') + } + + const brokenBacklinks = (data.backlinks || []).map((link: any) => ({ + urlFrom: link.url_from || '', + urlTo: link.url_to || '', + httpCode: link.http_code ?? 404, + anchor: link.anchor || '', + domainRatingSource: link.domain_rating_source ?? 0, + })) + + return { + success: true, + output: { + brokenBacklinks, + }, + } + }, + + outputs: { + brokenBacklinks: { + type: 'array', + description: 'List of broken backlinks', + items: { + type: 'object', + properties: { + urlFrom: { + type: 'string', + description: 'The URL of the page containing the broken link', + }, + urlTo: { type: 'string', description: 'The broken URL being linked to' }, + httpCode: { type: 'number', description: 'HTTP status code (e.g., 404, 410)' }, + anchor: { type: 'string', description: 'The anchor text of the link' }, + domainRatingSource: { + type: 'number', + description: 'Domain Rating of the linking domain', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/domain_rating.ts b/apps/sim/tools/ahrefs/domain_rating.ts new file mode 100644 index 00000000000..0576d4bca35 --- /dev/null +++ b/apps/sim/tools/ahrefs/domain_rating.ts @@ -0,0 +1,62 @@ +import type { AhrefsDomainRatingParams, AhrefsDomainRatingResponse } from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const domainRatingTool: ToolConfig = { + id: 'ahrefs_domain_rating', + name: 'Ahrefs Domain Rating', + description: + "Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website's backlink profile on a scale from 0 to 100.", + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain to analyze (e.g., example.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => + `https://api.ahrefs.com/v3/site-explorer/domain-rating?target=${encodeURIComponent(params.target)}`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get domain rating') + } + + return { + success: true, + output: { + domainRating: data.domain_rating ?? 0, + ahrefsRank: data.ahrefs_rank ?? 0, + }, + } + }, + + outputs: { + domainRating: { + type: 'number', + description: 'Domain Rating score (0-100)', + }, + ahrefsRank: { + type: 'number', + description: 'Ahrefs Rank - global ranking based on backlink profile strength', + }, + }, +} diff --git a/apps/sim/tools/ahrefs/index.ts b/apps/sim/tools/ahrefs/index.ts new file mode 100644 index 00000000000..f83ae3d013f --- /dev/null +++ b/apps/sim/tools/ahrefs/index.ts @@ -0,0 +1,17 @@ +import { backlinksTool } from '@/tools/ahrefs/backlinks' +import { backlinksStatsTool } from '@/tools/ahrefs/backlinks_stats' +import { brokenBacklinksTool } from '@/tools/ahrefs/broken_backlinks' +import { domainRatingTool } from '@/tools/ahrefs/domain_rating' +import { keywordOverviewTool } from '@/tools/ahrefs/keyword_overview' +import { organicKeywordsTool } from '@/tools/ahrefs/organic_keywords' +import { referringDomainsTool } from '@/tools/ahrefs/referring_domains' +import { topPagesTool } from '@/tools/ahrefs/top_pages' + +export const ahrefsDomainRatingTool = domainRatingTool +export const ahrefsBacklinksTool = backlinksTool +export const ahrefsBacklinksStatsTool = backlinksStatsTool +export const ahrefsReferringDomainsTool = referringDomainsTool +export const ahrefsOrganicKeywordsTool = organicKeywordsTool +export const ahrefsTopPagesTool = topPagesTool +export const ahrefsKeywordOverviewTool = keywordOverviewTool +export const ahrefsBrokenBacklinksTool = brokenBacklinksTool diff --git a/apps/sim/tools/ahrefs/keyword_overview.ts b/apps/sim/tools/ahrefs/keyword_overview.ts new file mode 100644 index 00000000000..7eb3c884717 --- /dev/null +++ b/apps/sim/tools/ahrefs/keyword_overview.ts @@ -0,0 +1,101 @@ +import type { + AhrefsKeywordOverviewParams, + AhrefsKeywordOverviewResponse, +} from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const keywordOverviewTool: ToolConfig< + AhrefsKeywordOverviewParams, + AhrefsKeywordOverviewResponse +> = { + id: 'ahrefs_keyword_overview', + name: 'Ahrefs Keyword Overview', + description: + 'Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.', + version: '1.0.0', + + params: { + keyword: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The keyword to analyze', + }, + country: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Country code for keyword data (e.g., us, gb, de). Default: us', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/keywords-explorer/overview') + url.searchParams.set('keyword', params.keyword) + url.searchParams.set('country', params.country || 'us') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get keyword overview') + } + + return { + success: true, + output: { + overview: { + keyword: data.keyword || '', + searchVolume: data.volume ?? 0, + keywordDifficulty: data.keyword_difficulty ?? 0, + cpc: data.cpc ?? 0, + clicks: data.clicks ?? 0, + clicksPercentage: data.clicks_percentage ?? 0, + parentTopic: data.parent_topic || '', + trafficPotential: data.traffic_potential ?? 0, + }, + }, + } + }, + + outputs: { + overview: { + type: 'object', + description: 'Keyword metrics overview', + properties: { + keyword: { type: 'string', description: 'The analyzed keyword' }, + searchVolume: { type: 'number', description: 'Monthly search volume' }, + keywordDifficulty: { + type: 'number', + description: 'Keyword difficulty score (0-100)', + }, + cpc: { type: 'number', description: 'Cost per click in USD' }, + clicks: { type: 'number', description: 'Estimated clicks per month' }, + clicksPercentage: { + type: 'number', + description: 'Percentage of searches that result in clicks', + }, + parentTopic: { type: 'string', description: 'The parent topic for this keyword' }, + trafficPotential: { + type: 'number', + description: 'Estimated traffic potential if ranking #1', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/organic_keywords.ts b/apps/sim/tools/ahrefs/organic_keywords.ts new file mode 100644 index 00000000000..87cf3a09dd9 --- /dev/null +++ b/apps/sim/tools/ahrefs/organic_keywords.ts @@ -0,0 +1,118 @@ +import type { + AhrefsOrganicKeywordsParams, + AhrefsOrganicKeywordsResponse, +} from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const organicKeywordsTool: ToolConfig< + AhrefsOrganicKeywordsParams, + AhrefsOrganicKeywordsResponse +> = { + id: 'ahrefs_organic_keywords', + name: 'Ahrefs Organic Keywords', + description: + 'Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain or URL to analyze', + }, + country: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Country code for search results (e.g., us, gb, de). Default: us', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results to skip for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/organic-keywords') + url.searchParams.set('target', params.target) + url.searchParams.set('country', params.country || 'us') + if (params.mode) url.searchParams.set('mode', params.mode) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.offset) url.searchParams.set('offset', String(params.offset)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get organic keywords') + } + + const keywords = (data.keywords || []).map((kw: any) => ({ + keyword: kw.keyword || '', + volume: kw.volume ?? 0, + position: kw.position ?? 0, + url: kw.url || '', + traffic: kw.traffic ?? 0, + keywordDifficulty: kw.keyword_difficulty ?? 0, + })) + + return { + success: true, + output: { + keywords, + }, + } + }, + + outputs: { + keywords: { + type: 'array', + description: 'List of organic keywords the target ranks for', + items: { + type: 'object', + properties: { + keyword: { type: 'string', description: 'The keyword' }, + volume: { type: 'number', description: 'Monthly search volume' }, + position: { type: 'number', description: 'Current ranking position' }, + url: { type: 'string', description: 'The URL that ranks for this keyword' }, + traffic: { type: 'number', description: 'Estimated monthly organic traffic' }, + keywordDifficulty: { + type: 'number', + description: 'Keyword difficulty score (0-100)', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/referring_domains.ts b/apps/sim/tools/ahrefs/referring_domains.ts new file mode 100644 index 00000000000..90b7e6aba25 --- /dev/null +++ b/apps/sim/tools/ahrefs/referring_domains.ts @@ -0,0 +1,114 @@ +import type { + AhrefsReferringDomainsParams, + AhrefsReferringDomainsResponse, +} from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const referringDomainsTool: ToolConfig< + AhrefsReferringDomainsParams, + AhrefsReferringDomainsResponse +> = { + id: 'ahrefs_referring_domains', + name: 'Ahrefs Referring Domains', + description: + 'Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain or URL to analyze', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results to skip for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/referring-domains') + url.searchParams.set('target', params.target) + if (params.mode) url.searchParams.set('mode', params.mode) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.offset) url.searchParams.set('offset', String(params.offset)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get referring domains') + } + + const referringDomains = (data.refdomains || []).map((domain: any) => ({ + domain: domain.domain || '', + domainRating: domain.domain_rating ?? 0, + backlinks: domain.backlinks ?? 0, + dofollowBacklinks: domain.dofollow_backlinks ?? 0, + firstSeen: domain.first_seen || '', + lastVisited: domain.last_visited || '', + })) + + return { + success: true, + output: { + referringDomains, + }, + } + }, + + outputs: { + referringDomains: { + type: 'array', + description: 'List of domains linking to the target', + items: { + type: 'object', + properties: { + domain: { type: 'string', description: 'The referring domain' }, + domainRating: { type: 'number', description: 'Domain Rating of the referring domain' }, + backlinks: { + type: 'number', + description: 'Total number of backlinks from this domain', + }, + dofollowBacklinks: { + type: 'number', + description: 'Number of dofollow backlinks from this domain', + }, + firstSeen: { type: 'string', description: 'When the domain was first seen linking' }, + lastVisited: { type: 'string', description: 'When the domain was last checked' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/top_pages.ts b/apps/sim/tools/ahrefs/top_pages.ts new file mode 100644 index 00000000000..a9ee97e2b79 --- /dev/null +++ b/apps/sim/tools/ahrefs/top_pages.ts @@ -0,0 +1,110 @@ +import type { AhrefsTopPagesParams, AhrefsTopPagesResponse } from '@/tools/ahrefs/types' +import type { ToolConfig } from '@/tools/types' + +export const topPagesTool: ToolConfig = { + id: 'ahrefs_top_pages', + name: 'Ahrefs Top Pages', + description: + 'Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.', + version: '1.0.0', + + params: { + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target domain to analyze', + }, + country: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Country code for traffic data (e.g., us, gb, de). Default: us', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results to skip for pagination', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/top-pages') + url.searchParams.set('target', params.target) + url.searchParams.set('country', params.country || 'us') + if (params.mode) url.searchParams.set('mode', params.mode) + if (params.limit) url.searchParams.set('limit', String(params.limit)) + if (params.offset) url.searchParams.set('offset', String(params.offset)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get top pages') + } + + const pages = (data.pages || []).map((page: any) => ({ + url: page.url || '', + traffic: page.traffic ?? 0, + keywords: page.keywords ?? 0, + topKeyword: page.top_keyword || '', + value: page.value ?? 0, + })) + + return { + success: true, + output: { + pages, + }, + } + }, + + outputs: { + pages: { + type: 'array', + description: 'List of top pages by organic traffic', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The page URL' }, + traffic: { type: 'number', description: 'Estimated monthly organic traffic' }, + keywords: { type: 'number', description: 'Number of keywords the page ranks for' }, + topKeyword: { + type: 'string', + description: 'The top keyword driving traffic to this page', + }, + value: { type: 'number', description: 'Estimated traffic value in USD' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/ahrefs/types.ts b/apps/sim/tools/ahrefs/types.ts new file mode 100644 index 00000000000..58cb3877b75 --- /dev/null +++ b/apps/sim/tools/ahrefs/types.ts @@ -0,0 +1,198 @@ +// Common types for Ahrefs API tools +import type { ToolResponse } from '@/tools/types' + +// Common parameters for all Ahrefs tools +export interface AhrefsBaseParams { + apiKey: string +} + +// Target mode for analysis +export type AhrefsTargetMode = 'domain' | 'prefix' | 'subdomains' | 'exact' + +// Domain Rating tool types +export interface AhrefsDomainRatingParams extends AhrefsBaseParams { + target: string +} + +export interface AhrefsDomainRatingResult { + domain_rating: number + ahrefs_rank: number +} + +export interface AhrefsDomainRatingResponse extends ToolResponse { + output: { + domainRating: number + ahrefsRank: number + } +} + +// Backlinks tool types +export interface AhrefsBacklinksParams extends AhrefsBaseParams { + target: string + mode?: AhrefsTargetMode + limit?: number + offset?: number +} + +export interface AhrefsBacklink { + urlFrom: string + urlTo: string + anchor: string + domainRatingSource: number + isDofollow: boolean + firstSeen: string + lastVisited: string +} + +export interface AhrefsBacklinksResponse extends ToolResponse { + output: { + backlinks: AhrefsBacklink[] + } +} + +// Backlinks Stats tool types +export interface AhrefsBacklinksStatsParams extends AhrefsBaseParams { + target: string + mode?: AhrefsTargetMode +} + +export interface AhrefsBacklinksStatsResult { + total: number + dofollow: number + nofollow: number + text: number + image: number + redirect: number +} + +export interface AhrefsBacklinksStatsResponse extends ToolResponse { + output: { + stats: AhrefsBacklinksStatsResult + } +} + +// Referring Domains tool types +export interface AhrefsReferringDomainsParams extends AhrefsBaseParams { + target: string + mode?: AhrefsTargetMode + limit?: number + offset?: number +} + +export interface AhrefsReferringDomain { + domain: string + domainRating: number + backlinks: number + dofollowBacklinks: number + firstSeen: string + lastVisited: string +} + +export interface AhrefsReferringDomainsResponse extends ToolResponse { + output: { + referringDomains: AhrefsReferringDomain[] + } +} + +// Organic Keywords tool types +export interface AhrefsOrganicKeywordsParams extends AhrefsBaseParams { + target: string + country?: string + mode?: AhrefsTargetMode + limit?: number + offset?: number +} + +export interface AhrefsOrganicKeyword { + keyword: string + volume: number + position: number + url: string + traffic: number + keywordDifficulty: number +} + +export interface AhrefsOrganicKeywordsResponse extends ToolResponse { + output: { + keywords: AhrefsOrganicKeyword[] + } +} + +// Top Pages tool types +export interface AhrefsTopPagesParams extends AhrefsBaseParams { + target: string + country?: string + mode?: AhrefsTargetMode + limit?: number + offset?: number +} + +export interface AhrefsTopPage { + url: string + traffic: number + keywords: number + topKeyword: string + value: number +} + +export interface AhrefsTopPagesResponse extends ToolResponse { + output: { + pages: AhrefsTopPage[] + } +} + +// Keyword Overview tool types +export interface AhrefsKeywordOverviewParams extends AhrefsBaseParams { + keyword: string + country?: string +} + +export interface AhrefsKeywordOverviewResult { + keyword: string + searchVolume: number + keywordDifficulty: number + cpc: number + clicks: number + clicksPercentage: number + parentTopic: string + trafficPotential: number +} + +export interface AhrefsKeywordOverviewResponse extends ToolResponse { + output: { + overview: AhrefsKeywordOverviewResult + } +} + +// Broken Backlinks tool types +export interface AhrefsBrokenBacklinksParams extends AhrefsBaseParams { + target: string + mode?: AhrefsTargetMode + limit?: number + offset?: number +} + +export interface AhrefsBrokenBacklink { + urlFrom: string + urlTo: string + httpCode: number + anchor: string + domainRatingSource: number +} + +export interface AhrefsBrokenBacklinksResponse extends ToolResponse { + output: { + brokenBacklinks: AhrefsBrokenBacklink[] + } +} + +// Union type for all possible responses +export type AhrefsResponse = + | AhrefsDomainRatingResponse + | AhrefsBacklinksResponse + | AhrefsBacklinksStatsResponse + | AhrefsReferringDomainsResponse + | AhrefsOrganicKeywordsResponse + | AhrefsTopPagesResponse + | AhrefsKeywordOverviewResponse + | AhrefsBrokenBacklinksResponse diff --git a/apps/sim/tools/box/copy_file.ts b/apps/sim/tools/box/copy_file.ts new file mode 100644 index 00000000000..7456949b7d4 --- /dev/null +++ b/apps/sim/tools/box/copy_file.ts @@ -0,0 +1,92 @@ +import type { BoxCopyFileParams, BoxCopyFileResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxCopyFileTool: ToolConfig = { + id: 'box_copy_file', + name: 'Box Copy File', + description: 'Create a copy of a file in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to copy', + }, + parentFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination folder', + }, + newName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional new name for the copy', + }, + }, + + request: { + url: (params) => `https://api.box.com/2.0/files/${params.fileId}/copy`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = { + parent: { + id: params.parentFolderId, + }, + } + if (params.newName) { + body.name = params.newName + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to copy file', + output: {}, + } + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'object', + description: 'The copied file', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + parent: { type: 'object', description: 'Parent folder' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/box/create_collaboration.ts b/apps/sim/tools/box/create_collaboration.ts new file mode 100644 index 00000000000..2fb9ec9c7ec --- /dev/null +++ b/apps/sim/tools/box/create_collaboration.ts @@ -0,0 +1,138 @@ +import type { + BoxCreateCollaborationParams, + BoxCreateCollaborationResponse, +} from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxCreateCollaborationTool: ToolConfig< + BoxCreateCollaborationParams, + BoxCreateCollaborationResponse +> = { + id: 'box_create_collaboration', + name: 'Box Create Collaboration', + description: 'Share a file or folder with a user or group in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to share', + }, + itemType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The type of item: "file" or "folder"', + }, + accessibleByLogin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the user to share with', + }, + accessibleById: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the user or group to share with', + }, + accessibleByType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Type of collaborator: "user" or "group"', + }, + role: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Access role: "editor", "viewer", "previewer", "uploader", or "co-owner"', + }, + canViewPath: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the collaborator can see the full path to the folder (folders only)', + }, + }, + + request: { + url: 'https://api.box.com/2.0/collaborations', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const accessibleBy: Record = { + type: params.accessibleByType, + } + + if (params.accessibleByLogin) { + accessibleBy.login = params.accessibleByLogin + } else if (params.accessibleById) { + accessibleBy.id = params.accessibleById + } + + const body: Record = { + item: { + type: params.itemType, + id: params.itemId, + }, + accessible_by: accessibleBy, + role: params.role, + } + + if (params.canViewPath !== undefined && params.itemType === 'folder') { + body.can_view_path = params.canViewPath + } + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to create collaboration', + output: {}, + } + } + + return { + success: true, + output: { + collaboration: data, + }, + } + }, + + outputs: { + collaboration: { + type: 'object', + description: 'The created collaboration', + properties: { + id: { type: 'string', description: 'Collaboration ID' }, + role: { type: 'string', description: 'Access role' }, + status: { type: 'string', description: 'Collaboration status' }, + accessible_by: { type: 'object', description: 'User or group with access' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/box/create_folder.ts b/apps/sim/tools/box/create_folder.ts new file mode 100644 index 00000000000..085c689c8a9 --- /dev/null +++ b/apps/sim/tools/box/create_folder.ts @@ -0,0 +1,81 @@ +import type { BoxCreateFolderParams, BoxCreateFolderResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxCreateFolderTool: ToolConfig = { + id: 'box_create_folder', + name: 'Box Create Folder', + description: 'Create a new folder in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + parentFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the parent folder (use "0" for root folder)', + }, + folderName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the new folder', + }, + }, + + request: { + url: 'https://api.box.com/2.0/folders', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + name: params.folderName, + parent: { + id: params.parentFolderId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to create folder', + output: {}, + } + } + + return { + success: true, + output: { + folder: data, + }, + } + }, + + outputs: { + folder: { + type: 'object', + description: 'The created folder', + properties: { + id: { type: 'string', description: 'Folder ID' }, + name: { type: 'string', description: 'Folder name' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + parent: { type: 'object', description: 'Parent folder' }, + }, + }, + }, +} diff --git a/apps/sim/tools/box/create_shared_link.ts b/apps/sim/tools/box/create_shared_link.ts new file mode 100644 index 00000000000..c77d219bf56 --- /dev/null +++ b/apps/sim/tools/box/create_shared_link.ts @@ -0,0 +1,142 @@ +import type { BoxCreateSharedLinkParams, BoxCreateSharedLinkResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxCreateSharedLinkTool: ToolConfig< + BoxCreateSharedLinkParams, + BoxCreateSharedLinkResponse +> = { + id: 'box_create_shared_link', + name: 'Box Create Shared Link', + description: 'Create a shared link for a file or folder in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder', + }, + itemType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The type of item: "file" or "folder"', + }, + access: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Access level: "open" (anyone), "company", or "collaborators"', + }, + password: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Password to protect the shared link', + }, + unsharedAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Expiration date in ISO 8601 format', + }, + canDownload: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the link allows downloads', + }, + canPreview: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the link allows previews', + }, + }, + + request: { + url: (params) => `https://api.box.com/2.0/${params.itemType}s/${params.itemId}`, + method: 'PUT', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const sharedLink: Record = {} + + if (params.access) { + sharedLink.access = params.access + } + if (params.password) { + sharedLink.password = params.password + } + if (params.unsharedAt) { + sharedLink.unshared_at = params.unsharedAt + } + + // Always include permissions object with defaults (can_download: true, can_preview: true) + // Box API requires explicit permissions for proper shared link creation + sharedLink.permissions = { + can_download: params.canDownload !== undefined ? params.canDownload : true, + can_preview: params.canPreview !== undefined ? params.canPreview : true, + } + + return { + shared_link: sharedLink, + } + }, + }, + + transformResponse: async (response, params) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to create shared link', + output: {}, + } + } + + return { + success: true, + output: { + sharedLink: data.shared_link, + itemId: data.id, + itemType: params?.itemType, + }, + } + }, + + outputs: { + sharedLink: { + type: 'object', + description: 'The shared link details', + properties: { + url: { type: 'string', description: 'The shared link URL' }, + download_url: { type: 'string', description: 'Direct download URL' }, + access: { type: 'string', description: 'Access level' }, + is_password_enabled: { type: 'boolean', description: 'Whether password is enabled' }, + }, + }, + itemId: { + type: 'string', + description: 'The ID of the item', + }, + itemType: { + type: 'string', + description: 'The type of item', + }, + }, +} diff --git a/apps/sim/tools/box/delete_file.ts b/apps/sim/tools/box/delete_file.ts new file mode 100644 index 00000000000..359d618d98f --- /dev/null +++ b/apps/sim/tools/box/delete_file.ts @@ -0,0 +1,82 @@ +import type { BoxDeleteFileParams, BoxDeleteFileResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxDeleteFileTool: ToolConfig = { + id: 'box_delete_file', + name: 'Box Delete File', + description: + 'Delete a file from Box. Standard delete moves to trash. Use permanent flag for files already in trash.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to delete', + }, + permanent: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, permanently delete a file that is already in trash', + }, + }, + + request: { + url: (params) => { + // Standard delete always moves file to trash + // To permanently delete, the file must already be in trash (use /files/{id}/trash endpoint) + // The permanent flag here deletes from trash if the file is already there + if (params.permanent) { + return `https://api.box.com/2.0/files/${params.fileId}/trash` + } + return `https://api.box.com/2.0/files/${params.fileId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response, params) => { + if (response.status === 204) { + return { + success: true, + output: { + success: true, + fileId: params?.fileId, + }, + } + } + + const data = await response.json().catch(() => ({})) + return { + success: false, + error: data.message || data.error_description || 'Failed to delete file', + output: {}, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the file was successfully deleted', + }, + fileId: { + type: 'string', + description: 'The ID of the deleted file', + }, + }, +} diff --git a/apps/sim/tools/box/delete_folder.ts b/apps/sim/tools/box/delete_folder.ts new file mode 100644 index 00000000000..e9a4de9f1f3 --- /dev/null +++ b/apps/sim/tools/box/delete_folder.ts @@ -0,0 +1,76 @@ +import type { BoxDeleteFolderParams, BoxDeleteFolderResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxDeleteFolderTool: ToolConfig = { + id: 'box_delete_folder', + name: 'Box Delete Folder', + description: 'Delete a folder from Box (moves to trash by default)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + folderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the folder to delete', + }, + recursive: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Delete folder contents recursively (default: true)', + }, + }, + + request: { + url: (params) => { + const recursive = params.recursive !== false ? 'true' : 'false' + return `https://api.box.com/2.0/folders/${params.folderId}?recursive=${recursive}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response, params) => { + if (response.status === 204) { + return { + success: true, + output: { + success: true, + folderId: params?.folderId, + }, + } + } + + const data = await response.json().catch(() => ({})) + return { + success: false, + error: data.message || data.error_description || 'Failed to delete folder', + output: {}, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the folder was successfully deleted', + }, + folderId: { + type: 'string', + description: 'The ID of the deleted folder', + }, + }, +} diff --git a/apps/sim/tools/box/download_file.ts b/apps/sim/tools/box/download_file.ts new file mode 100644 index 00000000000..ee28352c829 --- /dev/null +++ b/apps/sim/tools/box/download_file.ts @@ -0,0 +1,120 @@ +import type { BoxDownloadFileParams, BoxDownloadFileResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxDownloadFileTool: ToolConfig = { + id: 'box_download_file', + name: 'Box Download File', + description: 'Get the download URL for a file in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to download', + }, + }, + + request: { + url: (params) => `https://api.box.com/2.0/files/${params.fileId}/content`, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response, params) => { + // Box returns a 302 redirect with the download URL + // If we get a redirect, extract the location + if (response.status === 302 || response.status === 301) { + const downloadUrl = response.headers.get('location') + if (downloadUrl) { + return { + success: true, + output: { + downloadUrl, + fileId: params?.fileId, + }, + } + } + } + + // If we get the content directly (some API implementations) + if (response.ok) { + const contentType = response.headers.get('content-type') || 'application/octet-stream' + const contentDisposition = response.headers.get('content-disposition') + let fileName = 'download' + + if (contentDisposition) { + const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) + if (match) { + fileName = match[1].replace(/['"]/g, '') + } + } + + // For text-based content, we can return the content + if (contentType.includes('text') || contentType.includes('json')) { + const content = await response.text() + return { + success: true, + output: { + content, + fileName, + mimeType: contentType, + }, + } + } + + // For binary content, return info about the file + return { + success: true, + output: { + fileName, + mimeType: contentType, + message: 'Binary file content available. Use the download URL to retrieve the file.', + }, + } + } + + const data = await response.json().catch(() => ({})) + return { + success: false, + error: data.message || data.error_description || 'Failed to download file', + output: {}, + } + }, + + outputs: { + content: { + type: 'string', + description: 'File content (for text files)', + optional: true, + }, + downloadUrl: { + type: 'string', + description: 'URL to download the file', + optional: true, + }, + fileName: { + type: 'string', + description: 'Name of the file', + optional: true, + }, + mimeType: { + type: 'string', + description: 'MIME type of the file', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/box/get_current_user.ts b/apps/sim/tools/box/get_current_user.ts new file mode 100644 index 00000000000..ff3c9e88a69 --- /dev/null +++ b/apps/sim/tools/box/get_current_user.ts @@ -0,0 +1,75 @@ +import type { BoxGetCurrentUserParams, BoxGetCurrentUserResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxGetCurrentUserTool: ToolConfig = + { + id: 'box_get_current_user', + name: 'Box Get Current User', + description: 'Get information about the authenticated Box user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: {}, + + request: { + url: 'https://api.box.com/2.0/users/me', + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to get current user', + output: {}, + } + } + + return { + success: true, + output: { + user: { + id: data.id, + type: data.type, + name: data.name, + login: data.login, + avatar_url: data.avatar_url, + space_used: data.space_used, + space_amount: data.space_amount, + max_upload_size: data.max_upload_size, + }, + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'The authenticated user', + properties: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'User name' }, + login: { type: 'string', description: 'User email/login' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + space_used: { type: 'number', description: 'Space used in bytes' }, + space_amount: { type: 'number', description: 'Total space available in bytes' }, + max_upload_size: { type: 'number', description: 'Maximum upload size in bytes' }, + }, + }, + }, + } diff --git a/apps/sim/tools/box/get_file.ts b/apps/sim/tools/box/get_file.ts new file mode 100644 index 00000000000..eed8029451f --- /dev/null +++ b/apps/sim/tools/box/get_file.ts @@ -0,0 +1,84 @@ +import type { BoxGetFileParams, BoxGetFileResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxGetFileTool: ToolConfig = { + id: 'box_get_file', + name: 'Box Get File', + description: 'Get information about a file in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to retrieve', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated list of fields to include (e.g., "id,name,size,modified_at,shared_link")', + }, + }, + + request: { + url: (params) => { + const fields = + params.fields || + 'id,type,name,size,created_at,modified_at,created_by,modified_by,owned_by,parent,shared_link,description,path_collection' + return `https://api.box.com/2.0/files/${params.fileId}?fields=${encodeURIComponent(fields)}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to get file', + output: {}, + } + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'object', + description: 'The file information', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + size: { type: 'number', description: 'File size in bytes' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + modified_at: { type: 'string', description: 'Last modification timestamp' }, + parent: { type: 'object', description: 'Parent folder' }, + shared_link: { type: 'object', description: 'Shared link information if exists' }, + }, + }, + }, +} diff --git a/apps/sim/tools/box/index.ts b/apps/sim/tools/box/index.ts new file mode 100644 index 00000000000..8da9f4e418e --- /dev/null +++ b/apps/sim/tools/box/index.ts @@ -0,0 +1,16 @@ +// Box Tools + +export { boxCopyFileTool } from './copy_file' +export { boxCreateCollaborationTool } from './create_collaboration' +export { boxCreateFolderTool } from './create_folder' +export { boxCreateSharedLinkTool } from './create_shared_link' +export { boxDeleteFileTool } from './delete_file' +export { boxDeleteFolderTool } from './delete_folder' +export { boxDownloadFileTool } from './download_file' +export { boxGetCurrentUserTool } from './get_current_user' +export { boxGetFileTool } from './get_file' +export { boxListFolderTool } from './list_folder' +export { boxSearchTool } from './search' +// Re-export types +export * from './types' +export { boxUpdateFileTool } from './update_file' diff --git a/apps/sim/tools/box/list_folder.ts b/apps/sim/tools/box/list_folder.ts new file mode 100644 index 00000000000..d954e2ce053 --- /dev/null +++ b/apps/sim/tools/box/list_folder.ts @@ -0,0 +1,112 @@ +import type { BoxListFolderItemsParams, BoxListFolderItemsResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxListFolderTool: ToolConfig = { + id: 'box_list_folder', + name: 'Box List Folder', + description: 'List items in a Box folder', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + folderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the folder to list (use "0" for root folder)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (default: 100, max: 1000)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Offset for pagination (default: 0)', + }, + fields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of fields to include', + }, + }, + + request: { + url: (params) => { + const limit = params.limit || 100 + const offset = params.offset || 0 + const fields = + params.fields || 'id,type,name,size,modified_at,created_at,parent,shared_link,description' + return `https://api.box.com/2.0/folders/${params.folderId}/items?limit=${limit}&offset=${offset}&fields=${encodeURIComponent(fields)}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to list folder', + output: {}, + } + } + + return { + success: true, + output: { + items: data.entries || [], + totalCount: data.total_count, + offset: data.offset, + limit: data.limit, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of files and folders in the folder', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + type: { type: 'string', description: 'Item type (file or folder)' }, + name: { type: 'string', description: 'Item name' }, + size: { type: 'number', description: 'Size in bytes (for files)' }, + modified_at: { type: 'string', description: 'Last modification timestamp' }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of items in the folder', + }, + offset: { + type: 'number', + description: 'Current offset for pagination', + }, + limit: { + type: 'number', + description: 'Maximum items returned', + }, + }, +} diff --git a/apps/sim/tools/box/search.ts b/apps/sim/tools/box/search.ts new file mode 100644 index 00000000000..7ab1a630fda --- /dev/null +++ b/apps/sim/tools/box/search.ts @@ -0,0 +1,145 @@ +import type { BoxSearchParams, BoxSearchResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxSearchTool: ToolConfig = { + id: 'box_search', + name: 'Box Search', + description: 'Search for files and folders in Box', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The search query string', + }, + scope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search scope: "user_content" (default) or "enterprise_content"', + }, + fileExtensions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated file extensions to filter by (e.g., "pdf,docx")', + }, + ancestorFolderIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated folder IDs to search within', + }, + contentTypes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated content types: "name", "description", "file_content", "comments", "tag"', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (default: 30, max: 200)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Offset for pagination (default: 0)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('query', params.query) + + if (params.scope) { + queryParams.append('scope', params.scope) + } + if (params.fileExtensions) { + queryParams.append('file_extensions', params.fileExtensions) + } + if (params.ancestorFolderIds) { + queryParams.append('ancestor_folder_ids', params.ancestorFolderIds) + } + if (params.contentTypes) { + queryParams.append('content_types', params.contentTypes) + } + queryParams.append('limit', String(params.limit || 30)) + queryParams.append('offset', String(params.offset || 0)) + + return `https://api.box.com/2.0/search?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to search', + output: {}, + } + } + + return { + success: true, + output: { + results: data.entries || [], + totalCount: data.total_count, + offset: data.offset, + limit: data.limit, + }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Array of search results', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + type: { type: 'string', description: 'Item type' }, + name: { type: 'string', description: 'Item name' }, + parent: { type: 'object', description: 'Parent folder' }, + modified_at: { type: 'string', description: 'Last modification timestamp' }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of search results', + }, + offset: { + type: 'number', + description: 'Current offset', + }, + limit: { + type: 'number', + description: 'Maximum results returned', + }, + }, +} diff --git a/apps/sim/tools/box/types.ts b/apps/sim/tools/box/types.ts new file mode 100644 index 00000000000..dc3672f4e30 --- /dev/null +++ b/apps/sim/tools/box/types.ts @@ -0,0 +1,619 @@ +import type { ToolResponse } from '@/tools/types' + +// ===== Core Types ===== + +export interface BoxFile { + id: string + type: 'file' + name: string + size?: number + created_at?: string + modified_at?: string + content_created_at?: string + content_modified_at?: string + created_by?: BoxUser + modified_by?: BoxUser + owned_by?: BoxUser + shared_link?: BoxSharedLink | null + parent?: BoxFolder + path_collection?: { + total_count: number + entries: BoxFolder[] + } + description?: string + sha1?: string + file_version?: { + id: string + type: 'file_version' + sha1: string + } + etag?: string + sequence_id?: string +} + +export interface BoxFolder { + id: string + type: 'folder' + name: string + size?: number + created_at?: string + modified_at?: string + created_by?: BoxUser + modified_by?: BoxUser + owned_by?: BoxUser + shared_link?: BoxSharedLink | null + parent?: BoxFolder | null + path_collection?: { + total_count: number + entries: BoxFolder[] + } + description?: string + item_collection?: { + total_count: number + entries: (BoxFile | BoxFolder)[] + offset: number + limit: number + } + etag?: string + sequence_id?: string +} + +export interface BoxUser { + id: string + type: 'user' + name: string + login?: string + avatar_url?: string +} + +export interface BoxSharedLink { + url: string + download_url?: string + vanity_url?: string + vanity_name?: string + access: 'open' | 'company' | 'collaborators' + effective_access?: 'open' | 'company' | 'collaborators' + effective_permission?: 'can_download' | 'can_preview' + unshared_at?: string + is_password_enabled?: boolean + permissions?: { + can_download: boolean + can_preview: boolean + } + download_count?: number + preview_count?: number +} + +export interface BoxCollaboration { + id: string + type: 'collaboration' + role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' | 'owner' + status: 'accepted' | 'pending' | 'rejected' + created_at?: string + modified_at?: string + accessible_by?: BoxUser | BoxGroup + created_by?: BoxUser + invite_email?: string + item?: BoxFile | BoxFolder +} + +export interface BoxGroup { + id: string + type: 'group' + name: string + description?: string +} + +export interface BoxComment { + id: string + type: 'comment' + message: string + created_at: string + modified_at?: string + created_by: BoxUser + item?: BoxFile +} + +export interface BoxTask { + id: string + type: 'task' + message?: string + due_at?: string + created_at: string + completion_rule?: 'all_assignees' | 'any_assignee' + action?: 'review' | 'complete' + item?: BoxFile + task_assignment_collection?: { + total_count: number + entries: BoxTaskAssignment[] + } +} + +export interface BoxTaskAssignment { + id: string + type: 'task_assignment' + status?: 'incomplete' | 'approved' | 'rejected' | 'completed' + message?: string + assigned_to?: BoxUser + assigned_at?: string + completed_at?: string +} + +export interface BoxFileVersion { + id: string + type: 'file_version' + sha1: string + name: string + size: number + created_at: string + modified_at: string + modified_by: BoxUser + trashed_at?: string + trashed_by?: BoxUser + purged_at?: string +} + +export interface BoxSearchResult { + id: string + type: 'file' | 'folder' | 'web_link' + name: string + parent?: BoxFolder + modified_at?: string + size?: number +} + +// ===== Request Params ===== + +export interface BoxBaseParams { + accessToken?: string +} + +// File Operations +export interface BoxGetFileParams extends BoxBaseParams { + fileId: string + fields?: string +} + +export interface BoxUploadFileParams extends BoxBaseParams { + parentFolderId: string + fileName: string + fileContent: string +} + +export interface BoxDownloadFileParams extends BoxBaseParams { + fileId: string +} + +export interface BoxUpdateFileParams extends BoxBaseParams { + fileId: string + name?: string + description?: string + parentId?: string + tags?: string[] +} + +export interface BoxCopyFileParams extends BoxBaseParams { + fileId: string + parentFolderId: string + newName?: string +} + +export interface BoxDeleteFileParams extends BoxBaseParams { + fileId: string + permanent?: boolean +} + +export interface BoxRestoreFileParams extends BoxBaseParams { + fileId: string + newName?: string + parentFolderId?: string +} + +// Folder Operations +export interface BoxGetFolderParams extends BoxBaseParams { + folderId: string + fields?: string +} + +export interface BoxListFolderItemsParams extends BoxBaseParams { + folderId: string + limit?: number + offset?: number + fields?: string +} + +export interface BoxCreateFolderParams extends BoxBaseParams { + parentFolderId: string + folderName: string +} + +export interface BoxUpdateFolderParams extends BoxBaseParams { + folderId: string + name?: string + description?: string + parentId?: string +} + +export interface BoxCopyFolderParams extends BoxBaseParams { + folderId: string + parentFolderId: string + newName?: string +} + +export interface BoxDeleteFolderParams extends BoxBaseParams { + folderId: string + recursive?: boolean +} + +// Search Operations +export interface BoxSearchParams extends BoxBaseParams { + query: string + scope?: 'user_content' | 'enterprise_content' + fileExtensions?: string + ancestorFolderIds?: string + limit?: number + offset?: number + contentTypes?: string +} + +// Collaboration Operations +export interface BoxCreateCollaborationParams extends BoxBaseParams { + itemId: string + itemType: 'file' | 'folder' + accessibleByLogin?: string + accessibleById?: string + accessibleByType: 'user' | 'group' + role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' + canViewPath?: boolean +} + +export interface BoxListCollaborationsParams extends BoxBaseParams { + itemId: string + itemType: 'file' | 'folder' + limit?: number + offset?: number +} + +export interface BoxUpdateCollaborationParams extends BoxBaseParams { + collaborationId: string + role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' + status?: 'accepted' | 'pending' | 'rejected' +} + +export interface BoxDeleteCollaborationParams extends BoxBaseParams { + collaborationId: string +} + +// Shared Link Operations +export interface BoxCreateSharedLinkParams extends BoxBaseParams { + itemId: string + itemType: 'file' | 'folder' + access?: 'open' | 'company' | 'collaborators' + password?: string + unsharedAt?: string + canDownload?: boolean + canPreview?: boolean +} + +export interface BoxGetSharedLinkParams extends BoxBaseParams { + itemId: string + itemType: 'file' | 'folder' +} + +export interface BoxDeleteSharedLinkParams extends BoxBaseParams { + itemId: string + itemType: 'file' | 'folder' +} + +// Comment Operations +export interface BoxCreateCommentParams extends BoxBaseParams { + fileId: string + message: string +} + +export interface BoxListCommentsParams extends BoxBaseParams { + fileId: string + limit?: number + offset?: number +} + +export interface BoxUpdateCommentParams extends BoxBaseParams { + commentId: string + message: string +} + +export interface BoxDeleteCommentParams extends BoxBaseParams { + commentId: string +} + +// Task Operations +export interface BoxCreateTaskParams extends BoxBaseParams { + fileId: string + message?: string + dueAt?: string + action?: 'review' | 'complete' +} + +export interface BoxGetTaskParams extends BoxBaseParams { + taskId: string +} + +export interface BoxUpdateTaskParams extends BoxBaseParams { + taskId: string + message?: string + dueAt?: string +} + +export interface BoxDeleteTaskParams extends BoxBaseParams { + taskId: string +} + +// File Version Operations +export interface BoxListFileVersionsParams extends BoxBaseParams { + fileId: string + limit?: number + offset?: number +} + +export interface BoxPromoteFileVersionParams extends BoxBaseParams { + fileId: string + versionId: string +} + +// User Operations +export interface BoxGetCurrentUserParams extends BoxBaseParams {} + +// ===== Response Types ===== + +export interface BoxGetFileResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxUploadFileResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxDownloadFileResponse extends ToolResponse { + output: { + content?: string + fileName?: string + mimeType?: string + } +} + +export interface BoxUpdateFileResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxCopyFileResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxDeleteFileResponse extends ToolResponse { + output: { + success?: boolean + fileId?: string + } +} + +export interface BoxRestoreFileResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxGetFolderResponse extends ToolResponse { + output: { + folder?: BoxFolder + } +} + +export interface BoxListFolderItemsResponse extends ToolResponse { + output: { + items?: (BoxFile | BoxFolder)[] + totalCount?: number + offset?: number + limit?: number + } +} + +export interface BoxCreateFolderResponse extends ToolResponse { + output: { + folder?: BoxFolder + } +} + +export interface BoxUpdateFolderResponse extends ToolResponse { + output: { + folder?: BoxFolder + } +} + +export interface BoxCopyFolderResponse extends ToolResponse { + output: { + folder?: BoxFolder + } +} + +export interface BoxDeleteFolderResponse extends ToolResponse { + output: { + success?: boolean + folderId?: string + } +} + +export interface BoxSearchResponse extends ToolResponse { + output: { + results?: BoxSearchResult[] + totalCount?: number + offset?: number + limit?: number + } +} + +export interface BoxCreateCollaborationResponse extends ToolResponse { + output: { + collaboration?: BoxCollaboration + } +} + +export interface BoxListCollaborationsResponse extends ToolResponse { + output: { + collaborations?: BoxCollaboration[] + totalCount?: number + } +} + +export interface BoxUpdateCollaborationResponse extends ToolResponse { + output: { + collaboration?: BoxCollaboration + } +} + +export interface BoxDeleteCollaborationResponse extends ToolResponse { + output: { + success?: boolean + collaborationId?: string + } +} + +export interface BoxCreateSharedLinkResponse extends ToolResponse { + output: { + sharedLink?: BoxSharedLink + itemId?: string + itemType?: string + } +} + +export interface BoxGetSharedLinkResponse extends ToolResponse { + output: { + sharedLink?: BoxSharedLink | null + itemId?: string + itemType?: string + } +} + +export interface BoxDeleteSharedLinkResponse extends ToolResponse { + output: { + success?: boolean + itemId?: string + itemType?: string + } +} + +export interface BoxCreateCommentResponse extends ToolResponse { + output: { + comment?: BoxComment + } +} + +export interface BoxListCommentsResponse extends ToolResponse { + output: { + comments?: BoxComment[] + totalCount?: number + } +} + +export interface BoxUpdateCommentResponse extends ToolResponse { + output: { + comment?: BoxComment + } +} + +export interface BoxDeleteCommentResponse extends ToolResponse { + output: { + success?: boolean + commentId?: string + } +} + +export interface BoxCreateTaskResponse extends ToolResponse { + output: { + task?: BoxTask + } +} + +export interface BoxGetTaskResponse extends ToolResponse { + output: { + task?: BoxTask + } +} + +export interface BoxUpdateTaskResponse extends ToolResponse { + output: { + task?: BoxTask + } +} + +export interface BoxDeleteTaskResponse extends ToolResponse { + output: { + success?: boolean + taskId?: string + } +} + +export interface BoxListFileVersionsResponse extends ToolResponse { + output: { + versions?: BoxFileVersion[] + totalCount?: number + } +} + +export interface BoxPromoteFileVersionResponse extends ToolResponse { + output: { + file?: BoxFile + } +} + +export interface BoxGetCurrentUserResponse extends ToolResponse { + output: { + user?: BoxUser & { + space_used?: number + space_amount?: number + max_upload_size?: number + } + } +} + +// Union type for all Box responses +export type BoxResponse = + | BoxGetFileResponse + | BoxUploadFileResponse + | BoxDownloadFileResponse + | BoxUpdateFileResponse + | BoxCopyFileResponse + | BoxDeleteFileResponse + | BoxRestoreFileResponse + | BoxGetFolderResponse + | BoxListFolderItemsResponse + | BoxCreateFolderResponse + | BoxUpdateFolderResponse + | BoxCopyFolderResponse + | BoxDeleteFolderResponse + | BoxSearchResponse + | BoxCreateCollaborationResponse + | BoxListCollaborationsResponse + | BoxUpdateCollaborationResponse + | BoxDeleteCollaborationResponse + | BoxCreateSharedLinkResponse + | BoxGetSharedLinkResponse + | BoxDeleteSharedLinkResponse + | BoxCreateCommentResponse + | BoxListCommentsResponse + | BoxUpdateCommentResponse + | BoxDeleteCommentResponse + | BoxCreateTaskResponse + | BoxGetTaskResponse + | BoxUpdateTaskResponse + | BoxDeleteTaskResponse + | BoxListFileVersionsResponse + | BoxPromoteFileVersionResponse + | BoxGetCurrentUserResponse diff --git a/apps/sim/tools/box/update_file.ts b/apps/sim/tools/box/update_file.ts new file mode 100644 index 00000000000..621c752efc9 --- /dev/null +++ b/apps/sim/tools/box/update_file.ts @@ -0,0 +1,111 @@ +import type { BoxUpdateFileParams, BoxUpdateFileResponse } from '@/tools/box/types' +import type { ToolConfig } from '@/tools/types' + +export const boxUpdateFileTool: ToolConfig = { + id: 'box_update_file', + name: 'Box Update File', + description: 'Update file information in Box (rename, move, or update description)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'box', + }, + + params: { + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the file', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the file', + }, + parentId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the new parent folder (to move the file)', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Array of tags to set on the file', + items: { type: 'string' }, + }, + }, + + request: { + url: (params) => `https://api.box.com/2.0/files/${params.fileId}`, + method: 'PUT', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Box API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = {} + if (params.name) { + body.name = params.name + } + if (params.description !== undefined) { + body.description = params.description + } + if (params.parentId) { + body.parent = { id: params.parentId } + } + if (params.tags) { + body.tags = params.tags + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || data.error_description || 'Failed to update file', + output: {}, + } + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'object', + description: 'The updated file', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + description: { type: 'string', description: 'File description' }, + parent: { type: 'object', description: 'Parent folder' }, + modified_at: { type: 'string', description: 'Last modification timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/cancel_downtime.ts b/apps/sim/tools/datadog/cancel_downtime.ts new file mode 100644 index 00000000000..95338b67c48 --- /dev/null +++ b/apps/sim/tools/datadog/cancel_downtime.ts @@ -0,0 +1,76 @@ +import type { CancelDowntimeParams, CancelDowntimeResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const cancelDowntimeTool: ToolConfig = { + id: 'datadog_cancel_downtime', + name: 'Datadog Cancel Downtime', + description: 'Cancel a scheduled downtime.', + version: '1.0.0', + + params: { + downtimeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the downtime to cancel', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v2/downtime/${params.downtimeId}` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok && response.status !== 204) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + success: false, + }, + error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`, + } + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the downtime was successfully canceled', + }, + }, +} diff --git a/apps/sim/tools/datadog/create_downtime.ts b/apps/sim/tools/datadog/create_downtime.ts new file mode 100644 index 00000000000..6b84d7170d5 --- /dev/null +++ b/apps/sim/tools/datadog/create_downtime.ts @@ -0,0 +1,178 @@ +import type { CreateDowntimeParams, CreateDowntimeResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const createDowntimeTool: ToolConfig = { + id: 'datadog_create_downtime', + name: 'Datadog Create Downtime', + description: 'Schedule a downtime to suppress monitor notifications during maintenance windows.', + version: '1.0.0', + + params: { + scope: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Scope to apply downtime to (e.g., "host:myhost", "env:production", or "*" for all)', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Message to display during downtime', + }, + start: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp for downtime start (defaults to now)', + }, + end: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp for downtime end', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Timezone for the downtime (e.g., "America/New_York")', + }, + monitorId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Specific monitor ID to mute', + }, + monitorTags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated monitor tags to match (e.g., "team:backend,priority:high")', + }, + muteFirstRecoveryNotification: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Mute the first recovery notification', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v2/downtime` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + body: (params) => { + const schedule: Record = {} + if (params.start) schedule.start = new Date(params.start * 1000).toISOString() + if (params.end) schedule.end = new Date(params.end * 1000).toISOString() + if (params.timezone) schedule.timezone = params.timezone + + const body: Record = { + data: { + type: 'downtime', + attributes: { + scope: params.scope, + schedule: Object.keys(schedule).length > 0 ? schedule : undefined, + }, + }, + } + + if (params.message) body.data.attributes.message = params.message + if (params.muteFirstRecoveryNotification !== undefined) { + body.data.attributes.mute_first_recovery_notification = params.muteFirstRecoveryNotification + } + + if (params.monitorId) { + body.data.attributes.monitor_identifier = { + monitor_id: Number.parseInt(params.monitorId, 10), + } + } else if (params.monitorTags) { + body.data.attributes.monitor_identifier = { + monitor_tags: params.monitorTags + .split(',') + .map((t: string) => t.trim()) + .filter((t: string) => t.length > 0), + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + downtime: {} as any, + }, + error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + const attrs = data.data?.attributes || {} + return { + success: true, + output: { + downtime: { + id: data.data?.id, + scope: attrs.scope ? [attrs.scope] : [], + message: attrs.message, + start: attrs.schedule?.start + ? new Date(attrs.schedule.start).getTime() / 1000 + : undefined, + end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined, + timezone: attrs.schedule?.timezone, + disabled: attrs.disabled, + active: attrs.status === 'active', + created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined, + modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined, + }, + }, + } + }, + + outputs: { + downtime: { + type: 'object', + description: 'The created downtime details', + properties: { + id: { type: 'number', description: 'Downtime ID' }, + scope: { type: 'array', description: 'Downtime scope' }, + message: { type: 'string', description: 'Downtime message' }, + start: { type: 'number', description: 'Start time (Unix timestamp)' }, + end: { type: 'number', description: 'End time (Unix timestamp)' }, + active: { type: 'boolean', description: 'Whether downtime is currently active' }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/create_event.ts b/apps/sim/tools/datadog/create_event.ts new file mode 100644 index 00000000000..2d71cedbfae --- /dev/null +++ b/apps/sim/tools/datadog/create_event.ts @@ -0,0 +1,169 @@ +import type { CreateEventParams, CreateEventResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const createEventTool: ToolConfig = { + id: 'datadog_create_event', + name: 'Datadog Create Event', + description: + 'Post an event to the Datadog event stream. Use for deployment notifications, alerts, or any significant occurrences.', + version: '1.0.0', + + params: { + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Event title', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Event body/description. Supports markdown.', + }, + alertType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Alert type: error, warning, info, success, user_update, recommendation, or snapshot', + }, + priority: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Event priority: normal or low', + }, + host: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Host name to associate with this event', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags (e.g., "env:production,service:api")', + }, + aggregationKey: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Key to aggregate events together', + }, + sourceTypeName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Source type name for the event', + }, + dateHappened: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp when the event occurred (defaults to now)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v1/events` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + }), + body: (params) => { + const body: Record = { + title: params.title, + text: params.text, + } + + if (params.alertType) body.alert_type = params.alertType + if (params.priority) body.priority = params.priority + if (params.host) body.host = params.host + if (params.aggregationKey) body.aggregation_key = params.aggregationKey + if (params.sourceTypeName) body.source_type_name = params.sourceTypeName + if (params.dateHappened) body.date_happened = params.dateHappened + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t: string) => t.trim()) + .filter((t: string) => t.length > 0) + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + event: {} as any, + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + return { + success: true, + output: { + event: { + id: data.event?.id, + title: data.event?.title, + text: data.event?.text, + date_happened: data.event?.date_happened, + priority: data.event?.priority, + alert_type: data.event?.alert_type, + host: data.event?.host, + tags: data.event?.tags, + url: data.event?.url, + }, + }, + } + }, + + outputs: { + event: { + type: 'object', + description: 'The created event details', + properties: { + id: { type: 'number', description: 'Event ID' }, + title: { type: 'string', description: 'Event title' }, + text: { type: 'string', description: 'Event text' }, + date_happened: { type: 'number', description: 'Unix timestamp when event occurred' }, + priority: { type: 'string', description: 'Event priority' }, + alert_type: { type: 'string', description: 'Alert type' }, + host: { type: 'string', description: 'Associated host' }, + tags: { type: 'array', description: 'Event tags' }, + url: { type: 'string', description: 'URL to view the event in Datadog' }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/create_monitor.ts b/apps/sim/tools/datadog/create_monitor.ts new file mode 100644 index 00000000000..416e2a7314d --- /dev/null +++ b/apps/sim/tools/datadog/create_monitor.ts @@ -0,0 +1,169 @@ +import type { CreateMonitorParams, CreateMonitorResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const createMonitorTool: ToolConfig = { + id: 'datadog_create_monitor', + name: 'Datadog Create Monitor', + description: + 'Create a new monitor/alert in Datadog. Monitors can track metrics, service checks, events, and more.', + version: '1.0.0', + + params: { + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Monitor name', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Monitor type: metric alert, service check, event alert, process alert, log alert, query alert, composite, synthetics alert, slo alert', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Monitor query (e.g., "avg(last_5m):avg:system.cpu.idle{*} < 20")', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Message to include with notifications. Can include @-mentions and markdown.', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Monitor priority (1-5, where 1 is highest)', + }, + options: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON string of monitor options (thresholds, notify_no_data, renotify_interval, etc.)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v1/monitor` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + body: (params) => { + const body: Record = { + name: params.name, + type: params.type, + query: params.query, + } + + if (params.message) body.message = params.message + if (params.priority) body.priority = params.priority + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t: string) => t.trim()) + .filter((t: string) => t.length > 0) + } + + if (params.options) { + try { + body.options = + typeof params.options === 'string' ? JSON.parse(params.options) : params.options + } catch { + // If options parsing fails, skip it + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + monitor: {} as any, + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + return { + success: true, + output: { + monitor: { + id: data.id, + name: data.name, + type: data.type, + query: data.query, + message: data.message, + tags: data.tags, + priority: data.priority, + options: data.options, + overall_state: data.overall_state, + created: data.created, + modified: data.modified, + creator: data.creator, + }, + }, + } + }, + + outputs: { + monitor: { + type: 'object', + description: 'The created monitor details', + properties: { + id: { type: 'number', description: 'Monitor ID' }, + name: { type: 'string', description: 'Monitor name' }, + type: { type: 'string', description: 'Monitor type' }, + query: { type: 'string', description: 'Monitor query' }, + message: { type: 'string', description: 'Notification message' }, + tags: { type: 'array', description: 'Monitor tags' }, + priority: { type: 'number', description: 'Monitor priority' }, + overall_state: { type: 'string', description: 'Current monitor state' }, + created: { type: 'string', description: 'Creation timestamp' }, + modified: { type: 'string', description: 'Last modification timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/get_monitor.ts b/apps/sim/tools/datadog/get_monitor.ts new file mode 100644 index 00000000000..1025b93f754 --- /dev/null +++ b/apps/sim/tools/datadog/get_monitor.ts @@ -0,0 +1,120 @@ +import type { GetMonitorParams, GetMonitorResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const getMonitorTool: ToolConfig = { + id: 'datadog_get_monitor', + name: 'Datadog Get Monitor', + description: 'Retrieve details of a specific monitor by ID.', + version: '1.0.0', + + params: { + monitorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the monitor to retrieve', + }, + groupStates: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated group states to include: alert, warn, no data, ok', + }, + withDowntimes: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include downtime data with the monitor', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + const queryParams = new URLSearchParams() + + if (params.groupStates) queryParams.set('group_states', params.groupStates) + if (params.withDowntimes) queryParams.set('with_downtimes', 'true') + + const queryString = queryParams.toString() + return `https://api.${site}/api/v1/monitor/${params.monitorId}${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + monitor: {} as any, + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + return { + success: true, + output: { + monitor: { + id: data.id, + name: data.name, + type: data.type, + query: data.query, + message: data.message, + tags: data.tags, + priority: data.priority, + options: data.options, + overall_state: data.overall_state, + created: data.created, + modified: data.modified, + creator: data.creator, + }, + }, + } + }, + + outputs: { + monitor: { + type: 'object', + description: 'The monitor details', + properties: { + id: { type: 'number', description: 'Monitor ID' }, + name: { type: 'string', description: 'Monitor name' }, + type: { type: 'string', description: 'Monitor type' }, + query: { type: 'string', description: 'Monitor query' }, + message: { type: 'string', description: 'Notification message' }, + tags: { type: 'array', description: 'Monitor tags' }, + priority: { type: 'number', description: 'Monitor priority' }, + overall_state: { type: 'string', description: 'Current monitor state' }, + created: { type: 'string', description: 'Creation timestamp' }, + modified: { type: 'string', description: 'Last modification timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/index.ts b/apps/sim/tools/datadog/index.ts new file mode 100644 index 00000000000..2568a9bb3e0 --- /dev/null +++ b/apps/sim/tools/datadog/index.ts @@ -0,0 +1,25 @@ +import { cancelDowntimeTool } from '@/tools/datadog/cancel_downtime' +import { createDowntimeTool } from '@/tools/datadog/create_downtime' +import { createEventTool } from '@/tools/datadog/create_event' +import { createMonitorTool } from '@/tools/datadog/create_monitor' +import { getMonitorTool } from '@/tools/datadog/get_monitor' +import { listDowntimesTool } from '@/tools/datadog/list_downtimes' +import { listMonitorsTool } from '@/tools/datadog/list_monitors' +import { muteMonitorTool } from '@/tools/datadog/mute_monitor' +import { queryLogsTool } from '@/tools/datadog/query_logs' +import { queryTimeseriesTool } from '@/tools/datadog/query_timeseries' +import { sendLogsTool } from '@/tools/datadog/send_logs' +import { submitMetricsTool } from '@/tools/datadog/submit_metrics' + +export const datadogSubmitMetricsTool = submitMetricsTool +export const datadogQueryTimeseriesTool = queryTimeseriesTool +export const datadogCreateEventTool = createEventTool +export const datadogCreateMonitorTool = createMonitorTool +export const datadogGetMonitorTool = getMonitorTool +export const datadogListMonitorsTool = listMonitorsTool +export const datadogMuteMonitorTool = muteMonitorTool +export const datadogQueryLogsTool = queryLogsTool +export const datadogSendLogsTool = sendLogsTool +export const datadogCreateDowntimeTool = createDowntimeTool +export const datadogListDowntimesTool = listDowntimesTool +export const datadogCancelDowntimeTool = cancelDowntimeTool diff --git a/apps/sim/tools/datadog/list_downtimes.ts b/apps/sim/tools/datadog/list_downtimes.ts new file mode 100644 index 00000000000..7a5de1fe40b --- /dev/null +++ b/apps/sim/tools/datadog/list_downtimes.ts @@ -0,0 +1,116 @@ +import type { ListDowntimesParams, ListDowntimesResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const listDowntimesTool: ToolConfig = { + id: 'datadog_list_downtimes', + name: 'Datadog List Downtimes', + description: 'List all scheduled downtimes in Datadog.', + version: '1.0.0', + + params: { + currentOnly: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Only return currently active downtimes', + }, + monitorId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by monitor ID', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + const queryParams = new URLSearchParams() + + if (params.currentOnly) queryParams.set('current_only', 'true') + if (params.monitorId) queryParams.set('monitor_id', params.monitorId) + + const queryString = queryParams.toString() + return `https://api.${site}/api/v2/downtime${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + downtimes: [], + }, + error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + const downtimes = (data.data || []).map((d: any) => { + const attrs = d.attributes || {} + return { + id: d.id, + scope: attrs.scope ? [attrs.scope] : [], + message: attrs.message, + start: attrs.schedule?.start ? new Date(attrs.schedule.start).getTime() / 1000 : undefined, + end: attrs.schedule?.end ? new Date(attrs.schedule.end).getTime() / 1000 : undefined, + timezone: attrs.schedule?.timezone, + disabled: attrs.disabled, + active: attrs.status === 'active', + created: attrs.created ? new Date(attrs.created).getTime() / 1000 : undefined, + modified: attrs.modified ? new Date(attrs.modified).getTime() / 1000 : undefined, + } + }) + + return { + success: true, + output: { + downtimes, + }, + } + }, + + outputs: { + downtimes: { + type: 'array', + description: 'List of downtimes', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Downtime ID' }, + scope: { type: 'array', description: 'Downtime scope' }, + message: { type: 'string', description: 'Downtime message' }, + start: { type: 'number', description: 'Start time (Unix timestamp)' }, + end: { type: 'number', description: 'End time (Unix timestamp)' }, + active: { type: 'boolean', description: 'Whether downtime is currently active' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/list_monitors.ts b/apps/sim/tools/datadog/list_monitors.ts new file mode 100644 index 00000000000..a9619273bbd --- /dev/null +++ b/apps/sim/tools/datadog/list_monitors.ts @@ -0,0 +1,150 @@ +import type { ListMonitorsParams, ListMonitorsResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const listMonitorsTool: ToolConfig = { + id: 'datadog_list_monitors', + name: 'Datadog List Monitors', + description: 'List all monitors in Datadog with optional filtering by name, tags, or state.', + version: '1.0.0', + + params: { + groupStates: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated group states to filter by: alert, warn, no data, ok', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter monitors by name (partial match)', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags to filter by', + }, + monitorTags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of monitor tags to filter by', + }, + withDowntimes: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include downtime data with monitors', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination (0-indexed)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of monitors per page (max 1000)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + const queryParams = new URLSearchParams() + + if (params.groupStates) queryParams.set('group_states', params.groupStates) + if (params.name) queryParams.set('name', params.name) + if (params.tags) queryParams.set('tags', params.tags) + if (params.monitorTags) queryParams.set('monitor_tags', params.monitorTags) + if (params.withDowntimes) queryParams.set('with_downtimes', 'true') + if (params.page !== undefined) queryParams.set('page', String(params.page)) + if (params.pageSize) queryParams.set('page_size', String(params.pageSize)) + + const queryString = queryParams.toString() + return `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + monitors: [], + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + const monitors = (Array.isArray(data) ? data : []).map((m: any) => ({ + id: m.id, + name: m.name, + type: m.type, + query: m.query, + message: m.message, + tags: m.tags, + priority: m.priority, + options: m.options, + overall_state: m.overall_state, + created: m.created, + modified: m.modified, + creator: m.creator, + })) + + return { + success: true, + output: { + monitors, + }, + } + }, + + outputs: { + monitors: { + type: 'array', + description: 'List of monitors', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Monitor ID' }, + name: { type: 'string', description: 'Monitor name' }, + type: { type: 'string', description: 'Monitor type' }, + query: { type: 'string', description: 'Monitor query' }, + overall_state: { type: 'string', description: 'Current state' }, + tags: { type: 'array', description: 'Tags' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/datadog/mute_monitor.ts b/apps/sim/tools/datadog/mute_monitor.ts new file mode 100644 index 00000000000..99c78837eec --- /dev/null +++ b/apps/sim/tools/datadog/mute_monitor.ts @@ -0,0 +1,94 @@ +import type { MuteMonitorParams, MuteMonitorResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const muteMonitorTool: ToolConfig = { + id: 'datadog_mute_monitor', + name: 'Datadog Mute Monitor', + description: 'Mute a monitor to temporarily suppress notifications.', + version: '1.0.0', + + params: { + monitorId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the monitor to mute', + }, + scope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Scope to mute (e.g., "host:myhost"). If not specified, mutes all scopes.', + }, + end: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Unix timestamp when the mute should end. If not specified, mutes indefinitely.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v1/monitor/${params.monitorId}/mute` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + body: (params) => { + const body: Record = {} + if (params.scope) body.scope = params.scope + if (params.end) body.end = params.end + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + success: false, + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the monitor was successfully muted', + }, + }, +} diff --git a/apps/sim/tools/datadog/query_logs.ts b/apps/sim/tools/datadog/query_logs.ts new file mode 100644 index 00000000000..ed6406c542f --- /dev/null +++ b/apps/sim/tools/datadog/query_logs.ts @@ -0,0 +1,169 @@ +import type { QueryLogsParams, QueryLogsResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const queryLogsTool: ToolConfig = { + id: 'datadog_query_logs', + name: 'Datadog Query Logs', + description: + 'Search and retrieve logs from Datadog. Use for troubleshooting, analysis, or monitoring.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Log search query (e.g., "service:web-app status:error")', + }, + from: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start time in ISO-8601 format or relative (e.g., "now-1h")', + }, + to: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End time in ISO-8601 format or relative (e.g., "now")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of logs to return (default: 50, max: 1000)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: timestamp (oldest first) or -timestamp (newest first)', + }, + indexes: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of log indexes to search', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v2/logs/events/search` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + body: (params) => { + const body: Record = { + filter: { + query: params.query, + from: params.from, + to: params.to, + }, + page: { + limit: params.limit || 50, + }, + } + + if (params.sort) { + body.sort = params.sort + } + + if (params.indexes) { + body.filter.indexes = params.indexes + .split(',') + .map((i: string) => i.trim()) + .filter((i: string) => i.length > 0) + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + logs: [], + }, + error: errorData.errors?.[0]?.detail || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + const logs = (data.data || []).map((log: any) => ({ + id: log.id, + content: { + timestamp: log.attributes?.timestamp, + host: log.attributes?.host, + service: log.attributes?.service, + message: log.attributes?.message, + status: log.attributes?.status, + attributes: log.attributes?.attributes, + tags: log.attributes?.tags, + }, + })) + + return { + success: true, + output: { + logs, + nextLogId: data.meta?.page?.after, + }, + } + }, + + outputs: { + logs: { + type: 'array', + description: 'List of log entries', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Log ID' }, + content: { + type: 'object', + description: 'Log content', + properties: { + timestamp: { type: 'string', description: 'Log timestamp' }, + host: { type: 'string', description: 'Host name' }, + service: { type: 'string', description: 'Service name' }, + message: { type: 'string', description: 'Log message' }, + status: { type: 'string', description: 'Log status/level' }, + }, + }, + }, + }, + }, + nextLogId: { + type: 'string', + description: 'Cursor for pagination', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/datadog/query_timeseries.ts b/apps/sim/tools/datadog/query_timeseries.ts new file mode 100644 index 00000000000..acf57395bbd --- /dev/null +++ b/apps/sim/tools/datadog/query_timeseries.ts @@ -0,0 +1,110 @@ +import type { QueryTimeseriesParams, QueryTimeseriesResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const queryTimeseriesTool: ToolConfig = { + id: 'datadog_query_timeseries', + name: 'Datadog Query Timeseries', + description: + 'Query metric timeseries data from Datadog. Use for analyzing trends, creating reports, or retrieving metric values.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Datadog metrics query (e.g., "avg:system.cpu.user{*}")', + }, + from: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Start time as Unix timestamp in seconds', + }, + to: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'End time as Unix timestamp in seconds', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + const queryParams = new URLSearchParams({ + query: params.query, + from: String(params.from), + to: String(params.to), + }) + return `https://api.${site}/api/v1/query?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + 'DD-APPLICATION-KEY': params.applicationKey, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + series: [], + status: 'error', + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json() + const series = (data.series || []).map((s: any) => ({ + metric: s.metric || s.expression, + tags: s.tag_set || [], + points: (s.pointlist || []).map((p: [number, number]) => ({ + timestamp: p[0] / 1000, // Convert from milliseconds to seconds + value: p[1], + })), + })) + + return { + success: true, + output: { + series, + status: data.status || 'ok', + }, + } + }, + + outputs: { + series: { + type: 'array', + description: 'Array of timeseries data with metric name, tags, and data points', + }, + status: { + type: 'string', + description: 'Query status', + }, + }, +} diff --git a/apps/sim/tools/datadog/send_logs.ts b/apps/sim/tools/datadog/send_logs.ts new file mode 100644 index 00000000000..dd480d928ff --- /dev/null +++ b/apps/sim/tools/datadog/send_logs.ts @@ -0,0 +1,100 @@ +import type { SendLogsParams, SendLogsResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const sendLogsTool: ToolConfig = { + id: 'datadog_send_logs', + name: 'Datadog Send Logs', + description: 'Send log entries to Datadog for centralized logging and analysis.', + version: '1.0.0', + + params: { + logs: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of log entries. Each entry should have message and optionally ddsource, ddtags, hostname, service.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + // Logs API uses a different subdomain + const logsHost = + site === 'datadoghq.com' + ? 'http-intake.logs.datadoghq.com' + : site === 'datadoghq.eu' + ? 'http-intake.logs.datadoghq.eu' + : `http-intake.logs.${site}` + return `https://${logsHost}/api/v2/logs` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + }), + body: (params) => { + let logs: any[] + try { + logs = typeof params.logs === 'string' ? JSON.parse(params.logs) : params.logs + } catch { + throw new Error('Invalid JSON in logs parameter') + } + + // Ensure each log entry has the required format + return logs.map((log: any) => ({ + ddsource: log.ddsource || 'custom', + ddtags: log.ddtags || '', + hostname: log.hostname || '', + message: log.message, + service: log.service || '', + })) + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + success: false, + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the logs were sent successfully', + }, + }, +} diff --git a/apps/sim/tools/datadog/submit_metrics.ts b/apps/sim/tools/datadog/submit_metrics.ts new file mode 100644 index 00000000000..f2c3f492257 --- /dev/null +++ b/apps/sim/tools/datadog/submit_metrics.ts @@ -0,0 +1,107 @@ +import type { SubmitMetricsParams, SubmitMetricsResponse } from '@/tools/datadog/types' +import type { ToolConfig } from '@/tools/types' + +export const submitMetricsTool: ToolConfig = { + id: 'datadog_submit_metrics', + name: 'Datadog Submit Metrics', + description: + 'Submit custom metrics to Datadog. Use for tracking application performance, business metrics, or custom monitoring data.', + version: '1.0.0', + + params: { + series: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of metric series to submit. Each series should include metric name, type (gauge/rate/count), points (timestamp/value pairs), and optional tags.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog API key', + }, + applicationKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Datadog Application key', + }, + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Datadog site/region (default: datadoghq.com)', + }, + }, + + request: { + url: (params) => { + const site = params.site || 'datadoghq.com' + return `https://api.${site}/api/v2/series` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'DD-API-KEY': params.apiKey, + }), + body: (params) => { + let series: any[] + try { + series = typeof params.series === 'string' ? JSON.parse(params.series) : params.series + } catch { + throw new Error('Invalid JSON in series parameter') + } + + // Transform to Datadog API v2 format + const formattedSeries = series.map((s: any) => ({ + metric: s.metric, + type: s.type === 'gauge' ? 0 : s.type === 'rate' ? 1 : s.type === 'count' ? 2 : 3, + points: s.points.map((p: any) => ({ + timestamp: p.timestamp, + value: p.value, + })), + tags: s.tags || [], + unit: s.unit, + resources: s.resources || [{ name: 'host', type: 'host' }], + })) + + return { series: formattedSeries } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + output: { + success: false, + errors: [errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`], + }, + error: errorData.errors?.[0] || `HTTP ${response.status}: ${response.statusText}`, + } + } + + const data = await response.json().catch(() => ({})) + return { + success: true, + output: { + success: true, + errors: data.errors || [], + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the metrics were submitted successfully', + }, + errors: { + type: 'array', + description: 'Any errors that occurred during submission', + }, + }, +} diff --git a/apps/sim/tools/datadog/types.ts b/apps/sim/tools/datadog/types.ts new file mode 100644 index 00000000000..5c230abbf5e --- /dev/null +++ b/apps/sim/tools/datadog/types.ts @@ -0,0 +1,778 @@ +// Common types for Datadog tools +import type { ToolResponse } from '@/tools/types' + +// Datadog Site/Region options +export type DatadogSite = + | 'datadoghq.com' + | 'us3.datadoghq.com' + | 'us5.datadoghq.com' + | 'datadoghq.eu' + | 'ap1.datadoghq.com' + | 'ddog-gov.com' + +// Common parameters for all Datadog tools +export interface DatadogBaseParams { + apiKey: string + applicationKey: string + site?: DatadogSite +} + +// ======================== +// METRICS TYPES +// ======================== + +export type MetricType = 'gauge' | 'rate' | 'count' | 'distribution' + +export interface MetricPoint { + timestamp: number + value: number +} + +export interface MetricSeries { + metric: string + type?: MetricType + points: MetricPoint[] + tags?: string[] + unit?: string + resources?: { name: string; type: string }[] +} + +export interface SubmitMetricsParams extends DatadogBaseParams { + series: string // JSON string of MetricSeries[] +} + +export interface SubmitMetricsOutput { + success: boolean + errors?: string[] +} + +export interface SubmitMetricsResponse extends ToolResponse { + output: SubmitMetricsOutput +} + +export interface QueryTimeseriesParams extends DatadogBaseParams { + query: string + from: number // Unix timestamp in seconds + to: number // Unix timestamp in seconds +} + +export interface TimeseriesPoint { + timestamp: number + value: number +} + +export interface TimeseriesResult { + metric: string + tags: string[] + points: TimeseriesPoint[] +} + +export interface QueryTimeseriesOutput { + series: TimeseriesResult[] + status: string +} + +export interface QueryTimeseriesResponse extends ToolResponse { + output: QueryTimeseriesOutput +} + +export interface ListMetricsParams extends DatadogBaseParams { + from?: number // Unix timestamp - only return metrics active since this time + host?: string // Filter by host name + tags?: string // Filter by tags (comma-separated) +} + +export interface ListMetricsOutput { + metrics: string[] +} + +export interface ListMetricsResponse extends ToolResponse { + output: ListMetricsOutput +} + +export interface GetMetricMetadataParams extends DatadogBaseParams { + metricName: string +} + +export interface MetricMetadata { + description?: string + short_name?: string + unit?: string + per_unit?: string + type?: string + integration?: string +} + +export interface GetMetricMetadataOutput { + metadata: MetricMetadata +} + +export interface GetMetricMetadataResponse extends ToolResponse { + output: GetMetricMetadataOutput +} + +// ======================== +// EVENTS TYPES +// ======================== + +export type EventAlertType = + | 'error' + | 'warning' + | 'info' + | 'success' + | 'user_update' + | 'recommendation' + | 'snapshot' +export type EventPriority = 'normal' | 'low' + +export interface CreateEventParams extends DatadogBaseParams { + title: string + text: string + alertType?: EventAlertType + priority?: EventPriority + host?: string + tags?: string // Comma-separated tags + aggregationKey?: string + sourceTypeName?: string + dateHappened?: number // Unix timestamp +} + +export interface EventData { + id: number + title: string + text: string + date_happened: number + priority: string + alert_type: string + host?: string + tags?: string[] + url?: string +} + +export interface CreateEventOutput { + event: EventData +} + +export interface CreateEventResponse extends ToolResponse { + output: CreateEventOutput +} + +export interface GetEventParams extends DatadogBaseParams { + eventId: string +} + +export interface GetEventOutput { + event: EventData +} + +export interface GetEventResponse extends ToolResponse { + output: GetEventOutput +} + +export interface QueryEventsParams extends DatadogBaseParams { + start: number // Unix timestamp + end: number // Unix timestamp + priority?: EventPriority + sources?: string // Comma-separated source names + tags?: string // Comma-separated tags + unaggregated?: boolean + excludeAggregate?: boolean + page?: number +} + +export interface QueryEventsOutput { + events: EventData[] +} + +export interface QueryEventsResponse extends ToolResponse { + output: QueryEventsOutput +} + +// ======================== +// MONITORS TYPES +// ======================== + +export type MonitorType = + | 'metric alert' + | 'service check' + | 'event alert' + | 'process alert' + | 'log alert' + | 'query alert' + | 'composite' + | 'synthetics alert' + | 'trace-analytics alert' + | 'slo alert' + +export interface MonitorThresholds { + critical?: number + critical_recovery?: number + warning?: number + warning_recovery?: number + ok?: number +} + +export interface MonitorOptions { + notify_no_data?: boolean + no_data_timeframe?: number + notify_audit?: boolean + renotify_interval?: number + escalation_message?: string + thresholds?: MonitorThresholds + include_tags?: boolean + require_full_window?: boolean + timeout_h?: number + evaluation_delay?: number + new_group_delay?: number + min_location_failed?: number +} + +export interface CreateMonitorParams extends DatadogBaseParams { + name: string + type: MonitorType + query: string + message?: string + tags?: string // Comma-separated tags + priority?: number // 1-5 + options?: string // JSON string of MonitorOptions +} + +export interface MonitorData { + id: number + name: string + type: string + query: string + message?: string + tags?: string[] + priority?: number + options?: MonitorOptions + overall_state?: string + created?: string + modified?: string + creator?: { email: string; handle: string; name: string } +} + +export interface CreateMonitorOutput { + monitor: MonitorData +} + +export interface CreateMonitorResponse extends ToolResponse { + output: CreateMonitorOutput +} + +export interface GetMonitorParams extends DatadogBaseParams { + monitorId: string + groupStates?: string // Comma-separated states: alert, warn, no data + withDowntimes?: boolean +} + +export interface GetMonitorOutput { + monitor: MonitorData +} + +export interface GetMonitorResponse extends ToolResponse { + output: GetMonitorOutput +} + +export interface UpdateMonitorParams extends DatadogBaseParams { + monitorId: string + name?: string + query?: string + message?: string + tags?: string // Comma-separated tags + priority?: number + options?: string // JSON string of MonitorOptions +} + +export interface UpdateMonitorOutput { + monitor: MonitorData +} + +export interface UpdateMonitorResponse extends ToolResponse { + output: UpdateMonitorOutput +} + +export interface DeleteMonitorParams extends DatadogBaseParams { + monitorId: string + force?: boolean +} + +export interface DeleteMonitorOutput { + deleted_monitor_id: number +} + +export interface DeleteMonitorResponse extends ToolResponse { + output: DeleteMonitorOutput +} + +export interface ListMonitorsParams extends DatadogBaseParams { + groupStates?: string // Comma-separated states + name?: string // Filter by name + tags?: string // Filter by tags (comma-separated) + monitorTags?: string // Filter by monitor tags + withDowntimes?: boolean + idOffset?: number + page?: number + pageSize?: number +} + +export interface ListMonitorsOutput { + monitors: MonitorData[] +} + +export interface ListMonitorsResponse extends ToolResponse { + output: ListMonitorsOutput +} + +export interface MuteMonitorParams extends DatadogBaseParams { + monitorId: string + scope?: string // Scope to mute (e.g., "host:myhost") + end?: number // Unix timestamp when mute ends +} + +export interface MuteMonitorOutput { + success: boolean +} + +export interface MuteMonitorResponse extends ToolResponse { + output: MuteMonitorOutput +} + +export interface UnmuteMonitorParams extends DatadogBaseParams { + monitorId: string + scope?: string + allScopes?: boolean +} + +export interface UnmuteMonitorOutput { + success: boolean +} + +export interface UnmuteMonitorResponse extends ToolResponse { + output: UnmuteMonitorOutput +} + +// ======================== +// LOGS TYPES +// ======================== + +export interface LogEntry { + ddsource?: string + ddtags?: string + hostname?: string + message: string + service?: string +} + +export interface SendLogsParams extends DatadogBaseParams { + logs: string // JSON string of LogEntry[] +} + +export interface SendLogsOutput { + success: boolean +} + +export interface SendLogsResponse extends ToolResponse { + output: SendLogsOutput +} + +export interface QueryLogsParams extends DatadogBaseParams { + query: string + from: string // ISO-8601 or relative (now-1h) + to: string // ISO-8601 or relative (now) + limit?: number + sort?: 'timestamp' | '-timestamp' + indexes?: string // Comma-separated index names +} + +export interface LogData { + id: string + content: { + timestamp: string + host?: string + service?: string + message: string + status?: string + attributes?: Record + tags?: string[] + } +} + +export interface QueryLogsOutput { + logs: LogData[] + nextLogId?: string +} + +export interface QueryLogsResponse extends ToolResponse { + output: QueryLogsOutput +} + +// ======================== +// DOWNTIME TYPES +// ======================== + +export interface CreateDowntimeParams extends DatadogBaseParams { + scope: string // Scope to apply downtime (e.g., "host:myhost" or "*") + message?: string + start?: number // Unix timestamp, defaults to now + end?: number // Unix timestamp + timezone?: string + monitorId?: string // Monitor ID to mute + monitorTags?: string // Comma-separated tags to match monitors + muteFirstRecoveryNotification?: boolean + notifyEndTypes?: string // Comma-separated: "canceled", "expired" + recurrence?: string // JSON string of recurrence config +} + +export interface DowntimeData { + id: number + scope: string[] + message?: string + start?: number + end?: number + timezone?: string + monitor_id?: number + monitor_tags?: string[] + mute_first_recovery_notification?: boolean + disabled?: boolean + created?: number + modified?: number + creator_id?: number + canceled?: number + active?: boolean +} + +export interface CreateDowntimeOutput { + downtime: DowntimeData +} + +export interface CreateDowntimeResponse extends ToolResponse { + output: CreateDowntimeOutput +} + +export interface ListDowntimesParams extends DatadogBaseParams { + currentOnly?: boolean + withCreator?: boolean + monitorId?: string +} + +export interface ListDowntimesOutput { + downtimes: DowntimeData[] +} + +export interface ListDowntimesResponse extends ToolResponse { + output: ListDowntimesOutput +} + +export interface CancelDowntimeParams extends DatadogBaseParams { + downtimeId: string +} + +export interface CancelDowntimeOutput { + success: boolean +} + +export interface CancelDowntimeResponse extends ToolResponse { + output: CancelDowntimeOutput +} + +// ======================== +// SLO TYPES +// ======================== + +export type SloType = 'metric' | 'monitor' | 'time_slice' + +export interface SloThreshold { + timeframe: '7d' | '30d' | '90d' | 'custom' + target: number // Target percentage (e.g., 99.9) + target_display?: string + warning?: number + warning_display?: string +} + +export interface CreateSloParams extends DatadogBaseParams { + name: string + type: SloType + description?: string + tags?: string // Comma-separated tags + thresholds: string // JSON string of SloThreshold[] + // For metric-based SLO + query?: string // JSON string of { numerator: string, denominator: string } + // For monitor-based SLO + monitorIds?: string // Comma-separated monitor IDs + groups?: string // Comma-separated group names +} + +export interface SloData { + id: string + name: string + type: string + description?: string + tags?: string[] + thresholds: SloThreshold[] + creator?: { email: string; handle: string; name: string } + created_at?: number + modified_at?: number +} + +export interface CreateSloOutput { + slo: SloData +} + +export interface CreateSloResponse extends ToolResponse { + output: CreateSloOutput +} + +export interface GetSloHistoryParams extends DatadogBaseParams { + sloId: string + fromTs: number // Unix timestamp + toTs: number // Unix timestamp + target?: number // Target SLO percentage +} + +export interface SloHistoryData { + from_ts: number + to_ts: number + type: string + type_id: number + sli_value?: number + overall: { + name: string + sli_value: number + span_precision: number + precision: { [key: string]: number } + } + series?: { + times: number[] + values: number[] + } +} + +export interface GetSloHistoryOutput { + history: SloHistoryData +} + +export interface GetSloHistoryResponse extends ToolResponse { + output: GetSloHistoryOutput +} + +// ======================== +// DASHBOARD TYPES +// ======================== + +export type DashboardLayoutType = 'ordered' | 'free' + +export interface CreateDashboardParams extends DatadogBaseParams { + title: string + layoutType: DashboardLayoutType + description?: string + widgets?: string // JSON string of widget definitions + isReadOnly?: boolean + notifyList?: string // Comma-separated user handles to notify + templateVariables?: string // JSON string of template variable definitions + tags?: string // Comma-separated tags +} + +export interface DashboardData { + id: string + title: string + layout_type: string + description?: string + url?: string + author_handle?: string + created_at?: string + modified_at?: string + is_read_only?: boolean + tags?: string[] +} + +export interface CreateDashboardOutput { + dashboard: DashboardData +} + +export interface CreateDashboardResponse extends ToolResponse { + output: CreateDashboardOutput +} + +export interface GetDashboardParams extends DatadogBaseParams { + dashboardId: string +} + +export interface GetDashboardOutput { + dashboard: DashboardData +} + +export interface GetDashboardResponse extends ToolResponse { + output: GetDashboardOutput +} + +export interface ListDashboardsParams extends DatadogBaseParams { + filterShared?: boolean + filterDeleted?: boolean + count?: number + start?: number +} + +export interface DashboardSummary { + id: string + title: string + description?: string + layout_type: string + url?: string + author_handle?: string + created_at?: string + modified_at?: string + is_read_only?: boolean + popularity?: number +} + +export interface ListDashboardsOutput { + dashboards: DashboardSummary[] + total?: number +} + +export interface ListDashboardsResponse extends ToolResponse { + output: ListDashboardsOutput +} + +// ======================== +// HOSTS TYPES +// ======================== + +export interface ListHostsParams extends DatadogBaseParams { + filter?: string // Filter hosts by name, alias, or tag + sortField?: string // Field to sort by + sortDir?: 'asc' | 'desc' + start?: number // Starting offset + count?: number // Max hosts to return + from?: number // Unix timestamp - hosts seen in last N seconds + includeMutedHostsData?: boolean + includeHostsMetadata?: boolean +} + +export interface HostData { + name: string + id: number + aliases?: string[] + apps?: string[] + aws_name?: string + host_name?: string + is_muted?: boolean + last_reported_time?: number + meta?: { + agent_version?: string + cpu_cores?: number + gohai?: string + machine?: string + platform?: string + } + metrics?: { + cpu?: number + iowait?: number + load?: number + } + mute_timeout?: number + sources?: string[] + tags_by_source?: Record + up?: boolean +} + +export interface ListHostsOutput { + hosts: HostData[] + total_matching?: number + total_returned?: number +} + +export interface ListHostsResponse extends ToolResponse { + output: ListHostsOutput +} + +// ======================== +// INCIDENTS TYPES +// ======================== + +export type IncidentSeverity = 'SEV-1' | 'SEV-2' | 'SEV-3' | 'SEV-4' | 'SEV-5' | 'UNKNOWN' +export type IncidentState = 'active' | 'stable' | 'resolved' + +export interface CreateIncidentParams extends DatadogBaseParams { + title: string + customerImpacted: boolean + severity?: IncidentSeverity + fields?: string // JSON string of additional fields +} + +export interface IncidentData { + id: string + type: string + attributes: { + title: string + customer_impacted: boolean + severity?: IncidentSeverity + state?: IncidentState + created?: string + modified?: string + resolved?: string + detected?: string + customer_impact_scope?: string + customer_impact_start?: string + customer_impact_end?: string + public_id?: number + time_to_detect?: number + time_to_internal_response?: number + time_to_repair?: number + time_to_resolve?: number + } +} + +export interface CreateIncidentOutput { + incident: IncidentData +} + +export interface CreateIncidentResponse extends ToolResponse { + output: CreateIncidentOutput +} + +export interface ListIncidentsParams extends DatadogBaseParams { + query?: string + pageSize?: number + pageOffset?: number + include?: string // Comma-separated: users, attachments +} + +export interface ListIncidentsOutput { + incidents: IncidentData[] +} + +export interface ListIncidentsResponse extends ToolResponse { + output: ListIncidentsOutput +} + +// Union type for all Datadog responses +export type DatadogResponse = + | SubmitMetricsResponse + | QueryTimeseriesResponse + | ListMetricsResponse + | GetMetricMetadataResponse + | CreateEventResponse + | GetEventResponse + | QueryEventsResponse + | CreateMonitorResponse + | GetMonitorResponse + | UpdateMonitorResponse + | DeleteMonitorResponse + | ListMonitorsResponse + | MuteMonitorResponse + | UnmuteMonitorResponse + | SendLogsResponse + | QueryLogsResponse + | CreateDowntimeResponse + | ListDowntimesResponse + | CancelDowntimeResponse + | CreateSloResponse + | GetSloHistoryResponse + | CreateDashboardResponse + | GetDashboardResponse + | ListDashboardsResponse + | ListHostsResponse + | CreateIncidentResponse + | ListIncidentsResponse diff --git a/apps/sim/tools/dropbox/copy.ts b/apps/sim/tools/dropbox/copy.ts new file mode 100644 index 00000000000..8a6471f0314 --- /dev/null +++ b/apps/sim/tools/dropbox/copy.ts @@ -0,0 +1,87 @@ +import type { DropboxCopyParams, DropboxCopyResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxCopyTool: ToolConfig = { + id: 'dropbox_copy', + name: 'Dropbox Copy', + description: 'Copy a file or folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + fromPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The source path of the file or folder to copy', + }, + toPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The destination path for the copied file or folder', + }, + autorename: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, rename the file if there is a conflict at destination', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/copy_v2', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + from_path: params.fromPath, + to_path: params.toPath, + autorename: params.autorename ?? false, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to copy file/folder', + output: {}, + } + } + + return { + success: true, + output: { + metadata: data.metadata, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Metadata of the copied item', + properties: { + '.tag': { type: 'string', description: 'Type: file or folder' }, + id: { type: 'string', description: 'Unique identifier' }, + name: { type: 'string', description: 'Name of the copied item' }, + path_display: { type: 'string', description: 'Display path' }, + size: { type: 'number', description: 'Size in bytes (files only)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/create_folder.ts b/apps/sim/tools/dropbox/create_folder.ts new file mode 100644 index 00000000000..a107747c7d4 --- /dev/null +++ b/apps/sim/tools/dropbox/create_folder.ts @@ -0,0 +1,82 @@ +import type { DropboxCreateFolderParams, DropboxCreateFolderResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxCreateFolderTool: ToolConfig< + DropboxCreateFolderParams, + DropboxCreateFolderResponse +> = { + id: 'dropbox_create_folder', + name: 'Dropbox Create Folder', + description: 'Create a new folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path where the folder should be created (e.g., /new-folder)', + }, + autorename: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, rename the folder if there is a conflict', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/create_folder_v2', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path, + autorename: params.autorename ?? false, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to create folder', + output: {}, + } + } + + return { + success: true, + output: { + folder: data.metadata, + }, + } + }, + + outputs: { + folder: { + type: 'object', + description: 'The created folder metadata', + properties: { + id: { type: 'string', description: 'Unique identifier for the folder' }, + name: { type: 'string', description: 'Name of the folder' }, + path_display: { type: 'string', description: 'Display path of the folder' }, + path_lower: { type: 'string', description: 'Lowercase path of the folder' }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/create_shared_link.ts b/apps/sim/tools/dropbox/create_shared_link.ts new file mode 100644 index 00000000000..e27df0451a5 --- /dev/null +++ b/apps/sim/tools/dropbox/create_shared_link.ts @@ -0,0 +1,131 @@ +import type { + DropboxCreateSharedLinkParams, + DropboxCreateSharedLinkResponse, +} from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxCreateSharedLinkTool: ToolConfig< + DropboxCreateSharedLinkParams, + DropboxCreateSharedLinkResponse +> = { + id: 'dropbox_create_shared_link', + name: 'Dropbox Create Shared Link', + description: 'Create a shareable link for a file or folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the file or folder to share', + }, + requestedVisibility: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Visibility: public, team_only, or password', + }, + linkPassword: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for the shared link (only if visibility is password)', + }, + expires: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Expiration date in ISO 8601 format (e.g., 2025-12-31T23:59:59Z)', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/sharing/create_shared_link_with_settings', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = { + path: params.path, + } + + const settings: Record = {} + + if (params.requestedVisibility) { + settings.requested_visibility = { '.tag': params.requestedVisibility } + } + + if (params.linkPassword) { + settings.link_password = params.linkPassword + } + + if (params.expires) { + settings.expires = params.expires + } + + if (Object.keys(settings).length > 0) { + body.settings = settings + } + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + // Check if a shared link already exists + if (data.error_summary?.includes('shared_link_already_exists')) { + return { + success: false, + error: + 'A shared link already exists for this path. Use list_shared_links to get the existing link.', + output: {}, + } + } + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to create shared link', + output: {}, + } + } + + return { + success: true, + output: { + sharedLink: data, + }, + } + }, + + outputs: { + sharedLink: { + type: 'object', + description: 'The created shared link', + properties: { + url: { type: 'string', description: 'The shared link URL' }, + name: { type: 'string', description: 'Name of the shared item' }, + path_lower: { type: 'string', description: 'Lowercase path of the shared item' }, + expires: { type: 'string', description: 'Expiration date if set' }, + link_permissions: { + type: 'object', + description: 'Permissions for the shared link', + }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/delete.ts b/apps/sim/tools/dropbox/delete.ts new file mode 100644 index 00000000000..d1e6a67c86d --- /dev/null +++ b/apps/sim/tools/dropbox/delete.ts @@ -0,0 +1,76 @@ +import type { DropboxDeleteParams, DropboxDeleteResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxDeleteTool: ToolConfig = { + id: 'dropbox_delete', + name: 'Dropbox Delete', + description: 'Delete a file or folder in Dropbox (moves to trash)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the file or folder to delete', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/delete_v2', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to delete file/folder', + output: {}, + } + } + + return { + success: true, + output: { + metadata: data.metadata, + deleted: true, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Metadata of the deleted item', + properties: { + '.tag': { type: 'string', description: 'Type: file, folder, or deleted' }, + name: { type: 'string', description: 'Name of the deleted item' }, + path_display: { type: 'string', description: 'Display path' }, + }, + }, + deleted: { + type: 'boolean', + description: 'Whether the deletion was successful', + }, + }, +} diff --git a/apps/sim/tools/dropbox/download.ts b/apps/sim/tools/dropbox/download.ts new file mode 100644 index 00000000000..e489b3d21c1 --- /dev/null +++ b/apps/sim/tools/dropbox/download.ts @@ -0,0 +1,82 @@ +import type { DropboxDownloadParams, DropboxDownloadResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxDownloadTool: ToolConfig = { + id: 'dropbox_download', + name: 'Dropbox Download File', + description: 'Download a file from Dropbox and get a temporary link', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the file to download (e.g., /folder/document.pdf)', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/get_temporary_link', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to download file', + output: {}, + } + } + + return { + success: true, + output: { + file: data.metadata, + content: '', // Content will be available via the temporary link + temporaryLink: data.link, + }, + } + }, + + outputs: { + file: { + type: 'object', + description: 'The file metadata', + properties: { + id: { type: 'string', description: 'Unique identifier for the file' }, + name: { type: 'string', description: 'Name of the file' }, + path_display: { type: 'string', description: 'Display path of the file' }, + size: { type: 'number', description: 'Size of the file in bytes' }, + }, + }, + temporaryLink: { + type: 'string', + description: 'Temporary link to download the file (valid for ~4 hours)', + }, + content: { + type: 'string', + description: 'Base64 encoded file content (if fetched)', + }, + }, +} diff --git a/apps/sim/tools/dropbox/get_metadata.ts b/apps/sim/tools/dropbox/get_metadata.ts new file mode 100644 index 00000000000..88df55d24d3 --- /dev/null +++ b/apps/sim/tools/dropbox/get_metadata.ts @@ -0,0 +1,95 @@ +import type { DropboxGetMetadataParams, DropboxGetMetadataResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxGetMetadataTool: ToolConfig< + DropboxGetMetadataParams, + DropboxGetMetadataResponse +> = { + id: 'dropbox_get_metadata', + name: 'Dropbox Get Metadata', + description: 'Get metadata for a file or folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the file or folder to get metadata for', + }, + includeMediaInfo: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, include media info for photos/videos', + }, + includeDeleted: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, include deleted files in results', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/get_metadata', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path, + include_media_info: params.includeMediaInfo ?? false, + include_deleted: params.includeDeleted ?? false, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to get metadata', + output: {}, + } + } + + return { + success: true, + output: { + metadata: data, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Metadata for the file or folder', + properties: { + '.tag': { type: 'string', description: 'Type: file, folder, or deleted' }, + id: { type: 'string', description: 'Unique identifier' }, + name: { type: 'string', description: 'Name of the item' }, + path_display: { type: 'string', description: 'Display path' }, + path_lower: { type: 'string', description: 'Lowercase path' }, + size: { type: 'number', description: 'Size in bytes (files only)' }, + client_modified: { type: 'string', description: 'Client modification time' }, + server_modified: { type: 'string', description: 'Server modification time' }, + rev: { type: 'string', description: 'Revision identifier' }, + content_hash: { type: 'string', description: 'Content hash' }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/index.ts b/apps/sim/tools/dropbox/index.ts new file mode 100644 index 00000000000..617232fecf4 --- /dev/null +++ b/apps/sim/tools/dropbox/index.ts @@ -0,0 +1,10 @@ +export { dropboxCopyTool } from '@/tools/dropbox/copy' +export { dropboxCreateFolderTool } from '@/tools/dropbox/create_folder' +export { dropboxCreateSharedLinkTool } from '@/tools/dropbox/create_shared_link' +export { dropboxDeleteTool } from '@/tools/dropbox/delete' +export { dropboxDownloadTool } from '@/tools/dropbox/download' +export { dropboxGetMetadataTool } from '@/tools/dropbox/get_metadata' +export { dropboxListFolderTool } from '@/tools/dropbox/list_folder' +export { dropboxMoveTool } from '@/tools/dropbox/move' +export { dropboxSearchTool } from '@/tools/dropbox/search' +export { dropboxUploadTool } from '@/tools/dropbox/upload' diff --git a/apps/sim/tools/dropbox/list_folder.ts b/apps/sim/tools/dropbox/list_folder.ts new file mode 100644 index 00000000000..2ba7b983d2e --- /dev/null +++ b/apps/sim/tools/dropbox/list_folder.ts @@ -0,0 +1,115 @@ +import type { DropboxListFolderParams, DropboxListFolderResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxListFolderTool: ToolConfig = + { + id: 'dropbox_list_folder', + name: 'Dropbox List Folder', + description: 'List the contents of a folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the folder to list (use "" for root)', + }, + recursive: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, list contents recursively', + }, + includeDeleted: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, include deleted files/folders', + }, + includeMediaInfo: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, include media info for photos/videos', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 500)', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/list_folder', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path === '/' ? '' : params.path, + recursive: params.recursive ?? false, + include_deleted: params.includeDeleted ?? false, + include_media_info: params.includeMediaInfo ?? false, + limit: params.limit, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to list folder', + output: {}, + } + } + + return { + success: true, + output: { + entries: data.entries, + cursor: data.cursor, + hasMore: data.has_more, + }, + } + }, + + outputs: { + entries: { + type: 'array', + description: 'List of files and folders in the directory', + items: { + type: 'object', + properties: { + '.tag': { type: 'string', description: 'Type: file, folder, or deleted' }, + id: { type: 'string', description: 'Unique identifier' }, + name: { type: 'string', description: 'Name of the file/folder' }, + path_display: { type: 'string', description: 'Display path' }, + size: { type: 'number', description: 'Size in bytes (files only)' }, + }, + }, + }, + cursor: { + type: 'string', + description: 'Cursor for pagination', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more results', + }, + }, + } diff --git a/apps/sim/tools/dropbox/move.ts b/apps/sim/tools/dropbox/move.ts new file mode 100644 index 00000000000..bbdf57081f4 --- /dev/null +++ b/apps/sim/tools/dropbox/move.ts @@ -0,0 +1,87 @@ +import type { DropboxMoveParams, DropboxMoveResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxMoveTool: ToolConfig = { + id: 'dropbox_move', + name: 'Dropbox Move', + description: 'Move or rename a file or folder in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + fromPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The source path of the file or folder to move', + }, + toPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The destination path for the moved file or folder', + }, + autorename: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, rename the file if there is a conflict at destination', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/move_v2', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + from_path: params.fromPath, + to_path: params.toPath, + autorename: params.autorename ?? false, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to move file/folder', + output: {}, + } + } + + return { + success: true, + output: { + metadata: data.metadata, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Metadata of the moved item', + properties: { + '.tag': { type: 'string', description: 'Type: file or folder' }, + id: { type: 'string', description: 'Unique identifier' }, + name: { type: 'string', description: 'Name of the moved item' }, + path_display: { type: 'string', description: 'Display path' }, + size: { type: 'number', description: 'Size in bytes (files only)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/search.ts b/apps/sim/tools/dropbox/search.ts new file mode 100644 index 00000000000..eaf127ce6e3 --- /dev/null +++ b/apps/sim/tools/dropbox/search.ts @@ -0,0 +1,135 @@ +import type { DropboxSearchParams, DropboxSearchResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxSearchTool: ToolConfig = { + id: 'dropbox_search', + name: 'Dropbox Search', + description: 'Search for files and folders in Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The search query', + }, + path: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Limit search to a specific folder path', + }, + fileExtensions: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of file extensions to filter by (e.g., pdf,xlsx)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 100)', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/search_v2', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = { + query: params.query, + } + + const options: Record = {} + + if (params.path) { + options.path = params.path + } + + if (params.fileExtensions) { + const extensions = params.fileExtensions + .split(',') + .map((ext) => ext.trim()) + .filter((ext) => ext.length > 0) + if (extensions.length > 0) { + options.file_extensions = extensions + } + } + + if (params.maxResults) { + options.max_results = params.maxResults + } + + if (Object.keys(options).length > 0) { + body.options = options + } + + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to search files', + output: {}, + } + } + + return { + success: true, + output: { + matches: data.matches || [], + hasMore: data.has_more || false, + cursor: data.cursor, + }, + } + }, + + outputs: { + matches: { + type: 'array', + description: 'Search results', + items: { + type: 'object', + properties: { + match_type: { + type: 'object', + description: 'Type of match: filename, content, or both', + }, + metadata: { + type: 'object', + description: 'File or folder metadata', + }, + }, + }, + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more results', + }, + cursor: { + type: 'string', + description: 'Cursor for pagination', + }, + }, +} diff --git a/apps/sim/tools/dropbox/types.ts b/apps/sim/tools/dropbox/types.ts new file mode 100644 index 00000000000..b48f30cc838 --- /dev/null +++ b/apps/sim/tools/dropbox/types.ts @@ -0,0 +1,244 @@ +import type { ToolResponse } from '@/tools/types' + +// ===== Core Types ===== + +export interface DropboxFileMetadata { + '.tag': 'file' + id: string + name: string + path_display: string + path_lower: string + size: number + client_modified: string + server_modified: string + rev: string + content_hash?: string + is_downloadable?: boolean +} + +export interface DropboxFolderMetadata { + '.tag': 'folder' + id: string + name: string + path_display: string + path_lower: string +} + +export interface DropboxDeletedMetadata { + '.tag': 'deleted' + name: string + path_display: string + path_lower: string +} + +export type DropboxMetadata = DropboxFileMetadata | DropboxFolderMetadata | DropboxDeletedMetadata + +export interface DropboxSharedLinkMetadata { + url: string + name: string + path_lower: string + link_permissions: { + can_revoke: boolean + resolved_visibility: { + '.tag': 'public' | 'team_only' | 'password' | 'team_and_password' | 'shared_folder_only' + } + revoke_failure_reason?: { + '.tag': string + } + } + expires?: string + id?: string +} + +export interface DropboxSearchMatch { + match_type: { + '.tag': 'filename' | 'content' | 'both' + } + metadata: { + '.tag': 'metadata' + metadata: DropboxMetadata + } +} + +// ===== Base Params ===== + +export interface DropboxBaseParams { + accessToken?: string +} + +// ===== Upload Params ===== + +export interface DropboxUploadParams extends DropboxBaseParams { + path: string + fileContent: string // Base64 encoded file content + fileName?: string + mode?: 'add' | 'overwrite' + autorename?: boolean + mute?: boolean +} + +export interface DropboxUploadResponse extends ToolResponse { + output: { + file?: DropboxFileMetadata + } +} + +// ===== Download Params ===== + +export interface DropboxDownloadParams extends DropboxBaseParams { + path: string +} + +export interface DropboxDownloadResponse extends ToolResponse { + output: { + file?: DropboxFileMetadata + content?: string // Base64 encoded file content + temporaryLink?: string + } +} + +// ===== List Folder Params ===== + +export interface DropboxListFolderParams extends DropboxBaseParams { + path: string + recursive?: boolean + includeDeleted?: boolean + includeMediaInfo?: boolean + limit?: number +} + +export interface DropboxListFolderResponse extends ToolResponse { + output: { + entries?: DropboxMetadata[] + cursor?: string + hasMore?: boolean + } +} + +// ===== Create Folder Params ===== + +export interface DropboxCreateFolderParams extends DropboxBaseParams { + path: string + autorename?: boolean +} + +export interface DropboxCreateFolderResponse extends ToolResponse { + output: { + folder?: DropboxFolderMetadata + } +} + +// ===== Delete Params ===== + +export interface DropboxDeleteParams extends DropboxBaseParams { + path: string +} + +export interface DropboxDeleteResponse extends ToolResponse { + output: { + metadata?: DropboxMetadata + deleted?: boolean + } +} + +// ===== Copy Params ===== + +export interface DropboxCopyParams extends DropboxBaseParams { + fromPath: string + toPath: string + autorename?: boolean +} + +export interface DropboxCopyResponse extends ToolResponse { + output: { + metadata?: DropboxMetadata + } +} + +// ===== Move Params ===== + +export interface DropboxMoveParams extends DropboxBaseParams { + fromPath: string + toPath: string + autorename?: boolean +} + +export interface DropboxMoveResponse extends ToolResponse { + output: { + metadata?: DropboxMetadata + } +} + +// ===== Get Metadata Params ===== + +export interface DropboxGetMetadataParams extends DropboxBaseParams { + path: string + includeMediaInfo?: boolean + includeDeleted?: boolean +} + +export interface DropboxGetMetadataResponse extends ToolResponse { + output: { + metadata?: DropboxMetadata + } +} + +// ===== Create Shared Link Params ===== + +export interface DropboxCreateSharedLinkParams extends DropboxBaseParams { + path: string + requestedVisibility?: 'public' | 'team_only' | 'password' + linkPassword?: string + expires?: string +} + +export interface DropboxCreateSharedLinkResponse extends ToolResponse { + output: { + sharedLink?: DropboxSharedLinkMetadata + } +} + +// ===== Search Params ===== + +export interface DropboxSearchParams extends DropboxBaseParams { + query: string + path?: string + fileExtensions?: string + maxResults?: number +} + +export interface DropboxSearchResponse extends ToolResponse { + output: { + matches?: DropboxSearchMatch[] + hasMore?: boolean + cursor?: string + } +} + +// ===== Get Temporary Link Params ===== + +export interface DropboxGetTemporaryLinkParams extends DropboxBaseParams { + path: string +} + +export interface DropboxGetTemporaryLinkResponse extends ToolResponse { + output: { + metadata?: DropboxFileMetadata + link?: string + } +} + +// ===== Combined Response Type ===== + +export type DropboxResponse = + | DropboxUploadResponse + | DropboxDownloadResponse + | DropboxListFolderResponse + | DropboxCreateFolderResponse + | DropboxDeleteResponse + | DropboxCopyResponse + | DropboxMoveResponse + | DropboxGetMetadataResponse + | DropboxCreateSharedLinkResponse + | DropboxSearchResponse + | DropboxGetTemporaryLinkResponse diff --git a/apps/sim/tools/dropbox/upload.ts b/apps/sim/tools/dropbox/upload.ts new file mode 100644 index 00000000000..55732276383 --- /dev/null +++ b/apps/sim/tools/dropbox/upload.ts @@ -0,0 +1,119 @@ +import type { DropboxUploadParams, DropboxUploadResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxUploadTool: ToolConfig = { + id: 'dropbox_upload', + name: 'Dropbox Upload File', + description: 'Upload a file to Dropbox', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The path in Dropbox where the file should be saved (e.g., /folder/document.pdf)', + }, + fileContent: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The base64 encoded content of the file to upload', + }, + fileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional filename (used if path is a folder)', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Write mode: add (default) or overwrite', + }, + autorename: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, rename the file if there is a conflict', + }, + mute: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: "If true, don't notify the user about this upload", + }, + }, + + request: { + url: 'https://content.dropboxapi.com/2/files/upload', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + + const dropboxApiArg = { + path: params.path, + mode: params.mode || 'add', + autorename: params.autorename ?? true, + mute: params.mute ?? false, + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/octet-stream', + 'Dropbox-API-Arg': JSON.stringify(dropboxApiArg), + } + }, + body: (params) => { + // The body should be the raw binary data + // In this case we're passing the base64 content which will be decoded + return params.fileContent + }, + }, + + transformResponse: async (response, params) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to upload file', + output: {}, + } + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'object', + description: 'The uploaded file metadata', + properties: { + id: { type: 'string', description: 'Unique identifier for the file' }, + name: { type: 'string', description: 'Name of the file' }, + path_display: { type: 'string', description: 'Display path of the file' }, + path_lower: { type: 'string', description: 'Lowercase path of the file' }, + size: { type: 'number', description: 'Size of the file in bytes' }, + client_modified: { type: 'string', description: 'Client modification time' }, + server_modified: { type: 'string', description: 'Server modification time' }, + rev: { type: 'string', description: 'Revision identifier' }, + content_hash: { type: 'string', description: 'Content hash for the file' }, + }, + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/bulk.ts b/apps/sim/tools/elasticsearch/bulk.ts new file mode 100644 index 00000000000..368bd2f3d1e --- /dev/null +++ b/apps/sim/tools/elasticsearch/bulk.ts @@ -0,0 +1,181 @@ +import type { + ElasticsearchBulkParams, + ElasticsearchBulkResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchBulkParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchBulkParams): Record { + const headers: Record = { + 'Content-Type': 'application/x-ndjson', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const bulkTool: ToolConfig = { + id: 'elasticsearch_bulk', + name: 'Elasticsearch Bulk Operations', + description: + 'Perform multiple index, create, delete, or update operations in a single request for high performance.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: false, + description: 'Default index for operations that do not specify one', + }, + operations: { + type: 'string', + required: true, + description: 'Bulk operations as NDJSON string (newline-delimited JSON)', + }, + refresh: { + type: 'string', + required: false, + description: 'Refresh policy: true, false, or wait_for', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = params.index + ? `${baseUrl}/${encodeURIComponent(params.index)}/_bulk` + : `${baseUrl}/_bulk` + + if (params.refresh) { + url += `?refresh=${params.refresh}` + } + + return url + }, + method: 'POST', + headers: (params) => buildAuthHeaders(params), + body: (params) => { + // The body should be NDJSON format - we pass it as raw string + // Ensure it ends with a newline + // Note: The executor in tools/utils.ts handles NDJSON content-type specially + // and accepts string bodies directly + let operations = params.operations.trim() + if (!operations.endsWith('\n')) { + operations += '\n' + } + return operations as unknown as Record + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { took: 0, errors: true, items: [] }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + took: data.took, + errors: data.errors, + items: data.items, + }, + } + }, + + outputs: { + took: { + type: 'number', + description: 'Time in milliseconds the bulk operation took', + }, + errors: { + type: 'boolean', + description: 'Whether any operation had an error', + }, + items: { + type: 'array', + description: 'Results for each operation', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/cluster_health.ts b/apps/sim/tools/elasticsearch/cluster_health.ts new file mode 100644 index 00000000000..22b346be882 --- /dev/null +++ b/apps/sim/tools/elasticsearch/cluster_health.ts @@ -0,0 +1,212 @@ +import type { + ElasticsearchClusterHealthParams, + ElasticsearchClusterHealthResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchClusterHealthParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchClusterHealthParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const clusterHealthTool: ToolConfig< + ElasticsearchClusterHealthParams, + ElasticsearchClusterHealthResponse +> = { + id: 'elasticsearch_cluster_health', + name: 'Elasticsearch Cluster Health', + description: 'Get the health status of the Elasticsearch cluster.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + waitForStatus: { + type: 'string', + required: false, + description: 'Wait until cluster reaches this status: green, yellow, or red', + }, + timeout: { + type: 'string', + required: false, + description: 'Timeout for the wait operation (e.g., 30s, 1m)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = `${baseUrl}/_cluster/health` + + const queryParams: string[] = [] + if (params.waitForStatus) { + queryParams.push(`wait_for_status=${params.waitForStatus}`) + } + if (params.timeout) { + queryParams.push(`timeout=${encodeURIComponent(params.timeout)}`) + } + if (queryParams.length > 0) { + url += `?${queryParams.join('&')}` + } + + return url + }, + method: 'GET', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { + cluster_name: '', + status: 'red' as const, + timed_out: true, + number_of_nodes: 0, + number_of_data_nodes: 0, + active_primary_shards: 0, + active_shards: 0, + relocating_shards: 0, + initializing_shards: 0, + unassigned_shards: 0, + delayed_unassigned_shards: 0, + number_of_pending_tasks: 0, + number_of_in_flight_fetch: 0, + task_max_waiting_in_queue_millis: 0, + active_shards_percent_as_number: 0, + }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + cluster_name: data.cluster_name, + status: data.status, + timed_out: data.timed_out, + number_of_nodes: data.number_of_nodes, + number_of_data_nodes: data.number_of_data_nodes, + active_primary_shards: data.active_primary_shards, + active_shards: data.active_shards, + relocating_shards: data.relocating_shards, + initializing_shards: data.initializing_shards, + unassigned_shards: data.unassigned_shards, + delayed_unassigned_shards: data.delayed_unassigned_shards, + number_of_pending_tasks: data.number_of_pending_tasks, + number_of_in_flight_fetch: data.number_of_in_flight_fetch, + task_max_waiting_in_queue_millis: data.task_max_waiting_in_queue_millis, + active_shards_percent_as_number: data.active_shards_percent_as_number, + }, + } + }, + + outputs: { + cluster_name: { + type: 'string', + description: 'Name of the cluster', + }, + status: { + type: 'string', + description: 'Cluster health status: green, yellow, or red', + }, + number_of_nodes: { + type: 'number', + description: 'Total number of nodes in the cluster', + }, + number_of_data_nodes: { + type: 'number', + description: 'Number of data nodes', + }, + active_shards: { + type: 'number', + description: 'Number of active shards', + }, + unassigned_shards: { + type: 'number', + description: 'Number of unassigned shards', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/cluster_stats.ts b/apps/sim/tools/elasticsearch/cluster_stats.ts new file mode 100644 index 00000000000..97de2a5b291 --- /dev/null +++ b/apps/sim/tools/elasticsearch/cluster_stats.ts @@ -0,0 +1,177 @@ +import type { + ElasticsearchClusterStatsParams, + ElasticsearchClusterStatsResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchClusterStatsParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchClusterStatsParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const clusterStatsTool: ToolConfig< + ElasticsearchClusterStatsParams, + ElasticsearchClusterStatsResponse +> = { + id: 'elasticsearch_cluster_stats', + name: 'Elasticsearch Cluster Stats', + description: 'Get comprehensive statistics about the Elasticsearch cluster.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/_cluster/stats` + }, + method: 'GET', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { + cluster_name: '', + cluster_uuid: '', + status: 'red', + nodes: { + count: { total: 0, data: 0, master: 0 }, + versions: [], + }, + indices: { + count: 0, + docs: { count: 0, deleted: 0 }, + store: { size_in_bytes: 0 }, + shards: { total: 0, primaries: 0 }, + }, + }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + cluster_name: data.cluster_name, + cluster_uuid: data.cluster_uuid, + status: data.status, + nodes: { + count: data.nodes?.count || { total: 0, data: 0, master: 0 }, + versions: data.nodes?.versions || [], + }, + indices: { + count: data.indices?.count || 0, + docs: data.indices?.docs || { count: 0, deleted: 0 }, + store: data.indices?.store || { size_in_bytes: 0 }, + shards: data.indices?.shards || { total: 0, primaries: 0 }, + }, + }, + } + }, + + outputs: { + cluster_name: { + type: 'string', + description: 'Name of the cluster', + }, + status: { + type: 'string', + description: 'Cluster health status', + }, + nodes: { + type: 'object', + description: 'Node statistics including count and versions', + }, + indices: { + type: 'object', + description: 'Index statistics including document count and store size', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/count.ts b/apps/sim/tools/elasticsearch/count.ts new file mode 100644 index 00000000000..3ae586ea4c8 --- /dev/null +++ b/apps/sim/tools/elasticsearch/count.ts @@ -0,0 +1,164 @@ +import type { + ElasticsearchCountParams, + ElasticsearchCountResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchCountParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchCountParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const countTool: ToolConfig = { + id: 'elasticsearch_count', + name: 'Elasticsearch Count', + description: 'Count documents matching a query in Elasticsearch.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name to count documents in', + }, + query: { + type: 'string', + required: false, + description: 'Optional query to filter documents (JSON string)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/${encodeURIComponent(params.index)}/_count` + }, + method: 'POST', + headers: (params) => buildAuthHeaders(params), + body: (params) => { + if (params.query) { + try { + return { query: JSON.parse(params.query) } + } catch { + return {} + } + } + return {} + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { + count: 0, + _shards: { total: 0, successful: 0, skipped: 0, failed: 0 }, + }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + count: data.count, + _shards: data._shards, + }, + } + }, + + outputs: { + count: { + type: 'number', + description: 'Number of documents matching the query', + }, + _shards: { + type: 'object', + description: 'Shard statistics', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/create_index.ts b/apps/sim/tools/elasticsearch/create_index.ts new file mode 100644 index 00000000000..d9d5fb8af21 --- /dev/null +++ b/apps/sim/tools/elasticsearch/create_index.ts @@ -0,0 +1,185 @@ +import type { + ElasticsearchCreateIndexParams, + ElasticsearchIndexResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchCreateIndexParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchCreateIndexParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const createIndexTool: ToolConfig< + ElasticsearchCreateIndexParams, + ElasticsearchIndexResponse +> = { + id: 'elasticsearch_create_index', + name: 'Elasticsearch Create Index', + description: 'Create a new index with optional settings and mappings.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name to create', + }, + settings: { + type: 'string', + required: false, + description: 'Index settings as JSON string', + }, + mappings: { + type: 'string', + required: false, + description: 'Index mappings as JSON string', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/${encodeURIComponent(params.index)}` + }, + method: 'PUT', + headers: (params) => buildAuthHeaders(params), + body: (params) => { + const body: Record = {} + + if (params.settings) { + try { + body.settings = JSON.parse(params.settings) + } catch { + // Ignore invalid settings + } + } + + if (params.mappings) { + try { + body.mappings = JSON.parse(params.mappings) + } catch { + // Ignore invalid mappings + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { acknowledged: false }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + acknowledged: data.acknowledged, + shards_acknowledged: data.shards_acknowledged, + index: data.index, + }, + } + }, + + outputs: { + acknowledged: { + type: 'boolean', + description: 'Whether the request was acknowledged', + }, + shards_acknowledged: { + type: 'boolean', + description: 'Whether the shards were acknowledged', + }, + index: { + type: 'string', + description: 'Created index name', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/delete_document.ts b/apps/sim/tools/elasticsearch/delete_document.ts new file mode 100644 index 00000000000..17d8b22f721 --- /dev/null +++ b/apps/sim/tools/elasticsearch/delete_document.ts @@ -0,0 +1,186 @@ +import type { + ElasticsearchDeleteDocumentParams, + ElasticsearchDocumentResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchDeleteDocumentParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchDeleteDocumentParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const deleteDocumentTool: ToolConfig< + ElasticsearchDeleteDocumentParams, + ElasticsearchDocumentResponse +> = { + id: 'elasticsearch_delete_document', + name: 'Elasticsearch Delete Document', + description: 'Delete a document from Elasticsearch by ID.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name', + }, + documentId: { + type: 'string', + required: true, + description: 'Document ID to delete', + }, + refresh: { + type: 'string', + required: false, + description: 'Refresh policy: true, false, or wait_for', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc/${encodeURIComponent(params.documentId)}` + + if (params.refresh) { + url += `?refresh=${params.refresh}` + } + + return url + }, + method: 'DELETE', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + if (response.status === 404) { + return { + success: true, + output: { + _index: '', + _id: '', + result: 'not_found', + }, + } + } + + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { _index: '', _id: '' }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + _index: data._index, + _id: data._id, + _version: data._version, + result: data.result, + }, + } + }, + + outputs: { + _index: { + type: 'string', + description: 'Index name', + }, + _id: { + type: 'string', + description: 'Document ID', + }, + _version: { + type: 'number', + description: 'Document version', + }, + result: { + type: 'string', + description: 'Operation result (deleted or not_found)', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/delete_index.ts b/apps/sim/tools/elasticsearch/delete_index.ts new file mode 100644 index 00000000000..e2b839db3ab --- /dev/null +++ b/apps/sim/tools/elasticsearch/delete_index.ts @@ -0,0 +1,144 @@ +import type { + ElasticsearchDeleteIndexParams, + ElasticsearchIndexResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchDeleteIndexParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchDeleteIndexParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const deleteIndexTool: ToolConfig< + ElasticsearchDeleteIndexParams, + ElasticsearchIndexResponse +> = { + id: 'elasticsearch_delete_index', + name: 'Elasticsearch Delete Index', + description: 'Delete an index and all its documents. This operation is irreversible.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name to delete', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/${encodeURIComponent(params.index)}` + }, + method: 'DELETE', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { acknowledged: false }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + acknowledged: data.acknowledged, + }, + } + }, + + outputs: { + acknowledged: { + type: 'boolean', + description: 'Whether the deletion was acknowledged', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/get_document.ts b/apps/sim/tools/elasticsearch/get_document.ts new file mode 100644 index 00000000000..e79bc099e3b --- /dev/null +++ b/apps/sim/tools/elasticsearch/get_document.ts @@ -0,0 +1,203 @@ +import type { + ElasticsearchDocumentResponse, + ElasticsearchGetDocumentParams, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchGetDocumentParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchGetDocumentParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const getDocumentTool: ToolConfig< + ElasticsearchGetDocumentParams, + ElasticsearchDocumentResponse +> = { + id: 'elasticsearch_get_document', + name: 'Elasticsearch Get Document', + description: 'Retrieve a document by ID from Elasticsearch.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name', + }, + documentId: { + type: 'string', + required: true, + description: 'Document ID to retrieve', + }, + sourceIncludes: { + type: 'string', + required: false, + description: 'Comma-separated list of fields to include', + }, + sourceExcludes: { + type: 'string', + required: false, + description: 'Comma-separated list of fields to exclude', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc/${encodeURIComponent(params.documentId)}` + + const queryParams: string[] = [] + if (params.sourceIncludes) { + queryParams.push(`_source_includes=${encodeURIComponent(params.sourceIncludes)}`) + } + if (params.sourceExcludes) { + queryParams.push(`_source_excludes=${encodeURIComponent(params.sourceExcludes)}`) + } + if (queryParams.length > 0) { + url += `?${queryParams.join('&')}` + } + + return url + }, + method: 'GET', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + if (response.status === 404) { + return { + success: true, + output: { + _index: '', + _id: '', + found: false, + }, + } + } + + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { _index: '', _id: '' }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + _index: data._index, + _id: data._id, + _version: data._version, + found: data.found, + _source: data._source, + }, + } + }, + + outputs: { + _index: { + type: 'string', + description: 'Index name', + }, + _id: { + type: 'string', + description: 'Document ID', + }, + _version: { + type: 'number', + description: 'Document version', + }, + found: { + type: 'boolean', + description: 'Whether the document was found', + }, + _source: { + type: 'json', + description: 'Document content', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/get_index.ts b/apps/sim/tools/elasticsearch/get_index.ts new file mode 100644 index 00000000000..b53eb588821 --- /dev/null +++ b/apps/sim/tools/elasticsearch/get_index.ts @@ -0,0 +1,140 @@ +import type { + ElasticsearchGetIndexParams, + ElasticsearchIndexInfoResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchGetIndexParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchGetIndexParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const getIndexTool: ToolConfig = + { + id: 'elasticsearch_get_index', + name: 'Elasticsearch Get Index', + description: 'Retrieve index information including settings, mappings, and aliases.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name to retrieve info for', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/${encodeURIComponent(params.index)}` + }, + method: 'GET', + headers: (params) => buildAuthHeaders(params), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: {}, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: data, + } + }, + + outputs: { + index: { + type: 'json', + description: 'Index information including aliases, mappings, and settings', + }, + }, + } diff --git a/apps/sim/tools/elasticsearch/index.ts b/apps/sim/tools/elasticsearch/index.ts new file mode 100644 index 00000000000..e625add9170 --- /dev/null +++ b/apps/sim/tools/elasticsearch/index.ts @@ -0,0 +1,27 @@ +// Elasticsearch tools exports +import { bulkTool } from '@/tools/elasticsearch/bulk' +import { clusterHealthTool } from '@/tools/elasticsearch/cluster_health' +import { clusterStatsTool } from '@/tools/elasticsearch/cluster_stats' +import { countTool } from '@/tools/elasticsearch/count' +import { createIndexTool } from '@/tools/elasticsearch/create_index' +import { deleteDocumentTool } from '@/tools/elasticsearch/delete_document' +import { deleteIndexTool } from '@/tools/elasticsearch/delete_index' +import { getDocumentTool } from '@/tools/elasticsearch/get_document' +import { getIndexTool } from '@/tools/elasticsearch/get_index' +import { indexDocumentTool } from '@/tools/elasticsearch/index_document' +import { searchTool } from '@/tools/elasticsearch/search' +import { updateDocumentTool } from '@/tools/elasticsearch/update_document' + +// Export individual tools with elasticsearch prefix +export const elasticsearchSearchTool = searchTool +export const elasticsearchIndexDocumentTool = indexDocumentTool +export const elasticsearchGetDocumentTool = getDocumentTool +export const elasticsearchUpdateDocumentTool = updateDocumentTool +export const elasticsearchDeleteDocumentTool = deleteDocumentTool +export const elasticsearchBulkTool = bulkTool +export const elasticsearchCountTool = countTool +export const elasticsearchCreateIndexTool = createIndexTool +export const elasticsearchDeleteIndexTool = deleteIndexTool +export const elasticsearchGetIndexTool = getIndexTool +export const elasticsearchClusterHealthTool = clusterHealthTool +export const elasticsearchClusterStatsTool = clusterStatsTool diff --git a/apps/sim/tools/elasticsearch/index_document.ts b/apps/sim/tools/elasticsearch/index_document.ts new file mode 100644 index 00000000000..6081c0620bd --- /dev/null +++ b/apps/sim/tools/elasticsearch/index_document.ts @@ -0,0 +1,188 @@ +import type { + ElasticsearchDocumentResponse, + ElasticsearchIndexDocumentParams, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchIndexDocumentParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchIndexDocumentParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const indexDocumentTool: ToolConfig< + ElasticsearchIndexDocumentParams, + ElasticsearchDocumentResponse +> = { + id: 'elasticsearch_index_document', + name: 'Elasticsearch Index Document', + description: 'Index (create or update) a document in Elasticsearch.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Target index name', + }, + documentId: { + type: 'string', + required: false, + description: 'Document ID (auto-generated if not provided)', + }, + document: { + type: 'string', + required: true, + description: 'Document body as JSON string', + }, + refresh: { + type: 'string', + required: false, + description: 'Refresh policy: true, false, or wait_for', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = `${baseUrl}/${encodeURIComponent(params.index)}/_doc` + if (params.documentId) { + url += `/${encodeURIComponent(params.documentId)}` + } + if (params.refresh) { + url += `?refresh=${params.refresh}` + } + return url + }, + method: (params) => (params.documentId ? 'PUT' : 'POST'), + headers: (params) => buildAuthHeaders(params), + body: (params) => { + try { + return JSON.parse(params.document) + } catch { + throw new Error('Invalid JSON document') + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { _index: '', _id: '' }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + _index: data._index, + _id: data._id, + _version: data._version, + result: data.result, + }, + } + }, + + outputs: { + _index: { + type: 'string', + description: 'Index where the document was stored', + }, + _id: { + type: 'string', + description: 'Document ID', + }, + _version: { + type: 'number', + description: 'Document version', + }, + result: { + type: 'string', + description: 'Operation result (created or updated)', + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/search.ts b/apps/sim/tools/elasticsearch/search.ts new file mode 100644 index 00000000000..754f489e01a --- /dev/null +++ b/apps/sim/tools/elasticsearch/search.ts @@ -0,0 +1,254 @@ +import type { + ElasticsearchSearchParams, + ElasticsearchSearchResponse, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +// Helper to build base URL from connection params +function buildBaseUrl(params: ElasticsearchSearchParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + // Parse Cloud ID: format is "name:base64data" + // The base64 data contains: es_host$kibana_host ($ separated) + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + // Cloud endpoints are always HTTPS with port 443 + return `https://${parts[0]}.${esHost}` + } + } catch { + // If decoding fails, try using cloudId directly as host + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') // Remove trailing slash +} + +// Helper to build auth headers +function buildAuthHeaders(params: ElasticsearchSearchParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const searchTool: ToolConfig = { + id: 'elasticsearch_search', + name: 'Elasticsearch Search', + description: + 'Search documents in Elasticsearch using Query DSL. Returns matching documents with scores and metadata.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name to search', + }, + query: { + type: 'string', + required: false, + description: 'Query DSL as JSON string', + }, + from: { + type: 'number', + required: false, + description: 'Starting offset for pagination (default: 0)', + }, + size: { + type: 'number', + required: false, + description: 'Number of results to return (default: 10)', + }, + sort: { + type: 'string', + required: false, + description: 'Sort specification as JSON string', + }, + sourceIncludes: { + type: 'string', + required: false, + description: 'Comma-separated list of fields to include in _source', + }, + sourceExcludes: { + type: 'string', + required: false, + description: 'Comma-separated list of fields to exclude from _source', + }, + trackTotalHits: { + type: 'boolean', + required: false, + description: 'Track accurate total hit count (default: true)', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + return `${baseUrl}/${encodeURIComponent(params.index)}/_search` + }, + method: 'POST', + headers: (params) => buildAuthHeaders(params), + body: (params) => { + const body: Record = {} + + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch { + // If not valid JSON, treat as simple match query + body.query = { match_all: {} } + } + } + + if (params.from !== undefined) body.from = params.from + if (params.size !== undefined) body.size = params.size + + if (params.sort) { + try { + body.sort = JSON.parse(params.sort) + } catch { + // Ignore invalid sort + } + } + + if (params.sourceIncludes || params.sourceExcludes) { + body._source = {} + if (params.sourceIncludes) { + ;(body._source as Record).includes = params.sourceIncludes + .split(',') + .map((s) => s.trim()) + } + if (params.sourceExcludes) { + ;(body._source as Record).excludes = params.sourceExcludes + .split(',') + .map((s) => s.trim()) + } + } + + if (params.trackTotalHits !== undefined) { + body.track_total_hits = params.trackTotalHits + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { + took: 0, + timed_out: false, + hits: { total: { value: 0, relation: 'eq' }, max_score: null, hits: [] }, + }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + took: data.took, + timed_out: data.timed_out, + hits: { + total: data.hits.total, + max_score: data.hits.max_score, + hits: data.hits.hits.map((hit: Record) => ({ + _index: hit._index, + _id: hit._id, + _score: hit._score, + _source: hit._source, + })), + }, + aggregations: data.aggregations, + }, + } + }, + + outputs: { + took: { + type: 'number', + description: 'Time in milliseconds the search took', + }, + timed_out: { + type: 'boolean', + description: 'Whether the search timed out', + }, + hits: { + type: 'object', + description: 'Search results with total count and matching documents', + }, + aggregations: { + type: 'json', + description: 'Aggregation results if any', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/elasticsearch/types.ts b/apps/sim/tools/elasticsearch/types.ts new file mode 100644 index 00000000000..9a5516994e0 --- /dev/null +++ b/apps/sim/tools/elasticsearch/types.ts @@ -0,0 +1,278 @@ +// Common types for Elasticsearch tools +import type { ToolResponse } from '@/tools/types' + +// Base params for all Elasticsearch tools +export interface ElasticsearchBaseParams { + // Connection configuration + deploymentType: 'self_hosted' | 'cloud' + host?: string // For self-hosted + cloudId?: string // For Elastic Cloud + // Authentication + authMethod: 'api_key' | 'basic_auth' + apiKey?: string + username?: string + password?: string +} + +// Document Operations +export interface ElasticsearchIndexDocumentParams extends ElasticsearchBaseParams { + index: string + documentId?: string + document: string // JSON string + refresh?: 'true' | 'false' | 'wait_for' +} + +export interface ElasticsearchGetDocumentParams extends ElasticsearchBaseParams { + index: string + documentId: string + sourceIncludes?: string + sourceExcludes?: string +} + +export interface ElasticsearchUpdateDocumentParams extends ElasticsearchBaseParams { + index: string + documentId: string + document: string // JSON string (partial document) + retryOnConflict?: number +} + +export interface ElasticsearchDeleteDocumentParams extends ElasticsearchBaseParams { + index: string + documentId: string + refresh?: 'true' | 'false' | 'wait_for' +} + +export interface ElasticsearchBulkParams extends ElasticsearchBaseParams { + index?: string + operations: string // NDJSON string + refresh?: 'true' | 'false' | 'wait_for' +} + +// Search Operations +export interface ElasticsearchSearchParams extends ElasticsearchBaseParams { + index: string + query?: string // JSON string + from?: number + size?: number + sort?: string // JSON string + sourceIncludes?: string + sourceExcludes?: string + trackTotalHits?: boolean +} + +export interface ElasticsearchCountParams extends ElasticsearchBaseParams { + index: string + query?: string // JSON string +} + +// Index Management Operations +export interface ElasticsearchCreateIndexParams extends ElasticsearchBaseParams { + index: string + settings?: string // JSON string + mappings?: string // JSON string +} + +export interface ElasticsearchDeleteIndexParams extends ElasticsearchBaseParams { + index: string +} + +export interface ElasticsearchGetIndexParams extends ElasticsearchBaseParams { + index: string +} + +export interface ElasticsearchIndexExistsParams extends ElasticsearchBaseParams { + index: string +} + +export interface ElasticsearchRefreshIndexParams extends ElasticsearchBaseParams { + index: string +} + +export interface ElasticsearchIndexStatsParams extends ElasticsearchBaseParams { + index: string +} + +// Mapping Operations +export interface ElasticsearchPutMappingParams extends ElasticsearchBaseParams { + index: string + mappings: string // JSON string +} + +export interface ElasticsearchGetMappingParams extends ElasticsearchBaseParams { + index: string +} + +// Cluster Operations +export interface ElasticsearchClusterHealthParams extends ElasticsearchBaseParams { + waitForStatus?: 'green' | 'yellow' | 'red' + timeout?: string +} + +export interface ElasticsearchClusterStatsParams extends ElasticsearchBaseParams {} + +// Response types +export interface ElasticsearchDocumentResponse extends ToolResponse { + output: { + _index: string + _id: string + _version?: number + result?: 'created' | 'updated' | 'deleted' | 'not_found' | 'noop' + _source?: Record + found?: boolean + } +} + +export interface ElasticsearchSearchResponse extends ToolResponse { + output: { + took: number + timed_out: boolean + hits: { + total: { value: number; relation: string } + max_score: number | null + hits: Array<{ + _index: string + _id: string + _score: number | null + _source: Record + }> + } + aggregations?: Record + } +} + +export interface ElasticsearchCountResponse extends ToolResponse { + output: { + count: number + _shards: { + total: number + successful: number + skipped: number + failed: number + } + } +} + +export interface ElasticsearchBulkResponse extends ToolResponse { + output: { + took: number + errors: boolean + items: Array<{ + index?: { _index: string; _id: string; result: string; status: number } + create?: { _index: string; _id: string; result: string; status: number } + update?: { _index: string; _id: string; result: string; status: number } + delete?: { _index: string; _id: string; result: string; status: number } + }> + } +} + +export interface ElasticsearchIndexResponse extends ToolResponse { + output: { + acknowledged: boolean + shards_acknowledged?: boolean + index?: string + } +} + +export interface ElasticsearchIndexInfoResponse extends ToolResponse { + output: Record< + string, + { + aliases: Record + mappings: Record + settings: Record + } + > +} + +export interface ElasticsearchIndexExistsResponse extends ToolResponse { + output: { + exists: boolean + } +} + +export interface ElasticsearchMappingResponse extends ToolResponse { + output: Record }> +} + +export interface ElasticsearchClusterHealthResponse extends ToolResponse { + output: { + cluster_name: string + status: 'green' | 'yellow' | 'red' + timed_out: boolean + number_of_nodes: number + number_of_data_nodes: number + active_primary_shards: number + active_shards: number + relocating_shards: number + initializing_shards: number + unassigned_shards: number + delayed_unassigned_shards: number + number_of_pending_tasks: number + number_of_in_flight_fetch: number + task_max_waiting_in_queue_millis: number + active_shards_percent_as_number: number + } +} + +export interface ElasticsearchClusterStatsResponse extends ToolResponse { + output: { + cluster_name: string + cluster_uuid: string + status: string + nodes: { + count: { total: number; data: number; master: number } + versions: string[] + } + indices: { + count: number + docs: { count: number; deleted: number } + store: { size_in_bytes: number } + shards: { total: number; primaries: number } + } + } +} + +export interface ElasticsearchRefreshResponse extends ToolResponse { + output: { + _shards: { + total: number + successful: number + failed: number + } + } +} + +export interface ElasticsearchIndexStatsResponse extends ToolResponse { + output: { + _all: { + primaries: { + docs: { count: number; deleted: number } + store: { size_in_bytes: number } + indexing: { index_total: number } + search: { query_total: number } + } + total: { + docs: { count: number; deleted: number } + store: { size_in_bytes: number } + indexing: { index_total: number } + search: { query_total: number } + } + } + indices: Record + } +} + +// Union type for all Elasticsearch responses +export type ElasticsearchResponse = + | ElasticsearchDocumentResponse + | ElasticsearchSearchResponse + | ElasticsearchCountResponse + | ElasticsearchBulkResponse + | ElasticsearchIndexResponse + | ElasticsearchIndexInfoResponse + | ElasticsearchIndexExistsResponse + | ElasticsearchMappingResponse + | ElasticsearchClusterHealthResponse + | ElasticsearchClusterStatsResponse + | ElasticsearchRefreshResponse + | ElasticsearchIndexStatsResponse diff --git a/apps/sim/tools/elasticsearch/update_document.ts b/apps/sim/tools/elasticsearch/update_document.ts new file mode 100644 index 00000000000..3d1a1d468c0 --- /dev/null +++ b/apps/sim/tools/elasticsearch/update_document.ts @@ -0,0 +1,187 @@ +import type { + ElasticsearchDocumentResponse, + ElasticsearchUpdateDocumentParams, +} from '@/tools/elasticsearch/types' +import type { ToolConfig } from '@/tools/types' + +function buildBaseUrl(params: ElasticsearchUpdateDocumentParams): string { + if (params.deploymentType === 'cloud' && params.cloudId) { + const parts = params.cloudId.split(':') + if (parts.length >= 2) { + try { + const decoded = Buffer.from(parts[1], 'base64').toString('utf-8') + const [esHost] = decoded.split('$') + if (esHost) { + return `https://${parts[0]}.${esHost}` + } + } catch { + // Fallback + } + } + throw new Error('Invalid Cloud ID format') + } + + if (!params.host) { + throw new Error('Host is required for self-hosted deployments') + } + + return params.host.replace(/\/$/, '') +} + +function buildAuthHeaders(params: ElasticsearchUpdateDocumentParams): Record { + const headers: Record = { + 'Content-Type': 'application/json', + } + + if (params.authMethod === 'api_key' && params.apiKey) { + headers.Authorization = `ApiKey ${params.apiKey}` + } else if (params.authMethod === 'basic_auth' && params.username && params.password) { + const credentials = Buffer.from(`${params.username}:${params.password}`).toString('base64') + headers.Authorization = `Basic ${credentials}` + } else { + throw new Error('Invalid authentication configuration') + } + + return headers +} + +export const updateDocumentTool: ToolConfig< + ElasticsearchUpdateDocumentParams, + ElasticsearchDocumentResponse +> = { + id: 'elasticsearch_update_document', + name: 'Elasticsearch Update Document', + description: 'Partially update a document in Elasticsearch using doc merge.', + version: '1.0.0', + + params: { + deploymentType: { + type: 'string', + required: true, + description: 'Deployment type: self_hosted or cloud', + }, + host: { + type: 'string', + required: false, + description: 'Elasticsearch host URL (for self-hosted)', + }, + cloudId: { + type: 'string', + required: false, + description: 'Elastic Cloud ID (for cloud deployments)', + }, + authMethod: { + type: 'string', + required: true, + description: 'Authentication method: api_key or basic_auth', + }, + apiKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Elasticsearch API key', + }, + username: { + type: 'string', + required: false, + description: 'Username for basic auth', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for basic auth', + }, + index: { + type: 'string', + required: true, + description: 'Index name', + }, + documentId: { + type: 'string', + required: true, + description: 'Document ID to update', + }, + document: { + type: 'string', + required: true, + description: 'Partial document to merge as JSON string', + }, + retryOnConflict: { + type: 'number', + required: false, + description: 'Number of retries on version conflict', + }, + }, + + request: { + url: (params) => { + const baseUrl = buildBaseUrl(params) + let url = `${baseUrl}/${encodeURIComponent(params.index)}/_update/${encodeURIComponent(params.documentId)}` + + if (params.retryOnConflict !== undefined) { + url += `?retry_on_conflict=${params.retryOnConflict}` + } + + return url + }, + method: 'POST', + headers: (params) => buildAuthHeaders(params), + body: (params) => { + try { + return { doc: JSON.parse(params.document) } + } catch { + throw new Error('Invalid JSON document') + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `Elasticsearch error: ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.error?.reason || errorJson.error?.type || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + return { + success: false, + output: { _index: '', _id: '' }, + error: errorMessage, + } + } + + const data = await response.json() + + return { + success: true, + output: { + _index: data._index, + _id: data._id, + _version: data._version, + result: data.result, + }, + } + }, + + outputs: { + _index: { + type: 'string', + description: 'Index name', + }, + _id: { + type: 'string', + description: 'Document ID', + }, + _version: { + type: 'number', + description: 'New document version', + }, + result: { + type: 'string', + description: 'Operation result (updated or noop)', + }, + }, +} diff --git a/apps/sim/tools/gitlab/cancel_pipeline.ts b/apps/sim/tools/gitlab/cancel_pipeline.ts new file mode 100644 index 00000000000..6f4fb8cecb0 --- /dev/null +++ b/apps/sim/tools/gitlab/cancel_pipeline.ts @@ -0,0 +1,73 @@ +import type { GitLabCancelPipelineParams, GitLabCancelPipelineResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCancelPipelineTool: ToolConfig< + GitLabCancelPipelineParams, + GitLabCancelPipelineResponse +> = { + id: 'gitlab_cancel_pipeline', + name: 'GitLab Cancel Pipeline', + description: 'Cancel a running GitLab pipeline', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + pipelineId: { + type: 'number', + required: true, + description: 'Pipeline ID', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines/${params.pipelineId}/cancel` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const pipeline = await response.json() + + return { + success: true, + output: { + pipeline, + }, + } + }, + + outputs: { + pipeline: { + type: 'object', + description: 'The cancelled GitLab pipeline', + }, + }, +} diff --git a/apps/sim/tools/gitlab/create_issue.ts b/apps/sim/tools/gitlab/create_issue.ts new file mode 100644 index 00000000000..2078d67ada9 --- /dev/null +++ b/apps/sim/tools/gitlab/create_issue.ts @@ -0,0 +1,116 @@ +import type { GitLabCreateIssueParams, GitLabCreateIssueResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreateIssueTool: ToolConfig = + { + id: 'gitlab_create_issue', + name: 'GitLab Create Issue', + description: 'Create a new issue in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + title: { + type: 'string', + required: true, + description: 'Issue title', + }, + description: { + type: 'string', + required: false, + description: 'Issue description (Markdown supported)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + assigneeIds: { + type: 'array', + required: false, + description: 'Array of user IDs to assign', + }, + milestoneId: { + type: 'number', + required: false, + description: 'Milestone ID to assign', + }, + dueDate: { + type: 'string', + required: false, + description: 'Due date in YYYY-MM-DD format', + }, + confidential: { + type: 'boolean', + required: false, + description: 'Whether the issue is confidential', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = { + title: params.title, + } + + if (params.description) body.description = params.description + if (params.labels) body.labels = params.labels + if (params.assigneeIds) body.assignee_ids = params.assigneeIds + if (params.milestoneId) body.milestone_id = params.milestoneId + if (params.dueDate) body.due_date = params.dueDate + if (params.confidential !== undefined) body.confidential = params.confidential + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const issue = await response.json() + + return { + success: true, + output: { + issue, + }, + } + }, + + outputs: { + issue: { + type: 'object', + description: 'The created GitLab issue', + }, + }, + } diff --git a/apps/sim/tools/gitlab/create_issue_note.ts b/apps/sim/tools/gitlab/create_issue_note.ts new file mode 100644 index 00000000000..2e175a1118c --- /dev/null +++ b/apps/sim/tools/gitlab/create_issue_note.ts @@ -0,0 +1,82 @@ +import type { GitLabCreateIssueNoteParams, GitLabCreateNoteResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreateIssueNoteTool: ToolConfig< + GitLabCreateIssueNoteParams, + GitLabCreateNoteResponse +> = { + id: 'gitlab_create_issue_note', + name: 'GitLab Create Issue Comment', + description: 'Add a comment to a GitLab issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + issueIid: { + type: 'number', + required: true, + description: 'Issue internal ID (IID)', + }, + body: { + type: 'string', + required: true, + description: 'Comment body (Markdown supported)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}/notes` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + body: params.body, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const note = await response.json() + + return { + success: true, + output: { + note, + }, + } + }, + + outputs: { + note: { + type: 'object', + description: 'The created comment', + }, + }, +} diff --git a/apps/sim/tools/gitlab/create_merge_request.ts b/apps/sim/tools/gitlab/create_merge_request.ts new file mode 100644 index 00000000000..e58b09fcddf --- /dev/null +++ b/apps/sim/tools/gitlab/create_merge_request.ts @@ -0,0 +1,141 @@ +import type { + GitLabCreateMergeRequestParams, + GitLabCreateMergeRequestResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreateMergeRequestTool: ToolConfig< + GitLabCreateMergeRequestParams, + GitLabCreateMergeRequestResponse +> = { + id: 'gitlab_create_merge_request', + name: 'GitLab Create Merge Request', + description: 'Create a new merge request in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + sourceBranch: { + type: 'string', + required: true, + description: 'Source branch name', + }, + targetBranch: { + type: 'string', + required: true, + description: 'Target branch name', + }, + title: { + type: 'string', + required: true, + description: 'Merge request title', + }, + description: { + type: 'string', + required: false, + description: 'Merge request description (Markdown supported)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + assigneeIds: { + type: 'array', + required: false, + description: 'Array of user IDs to assign', + }, + milestoneId: { + type: 'number', + required: false, + description: 'Milestone ID to assign', + }, + removeSourceBranch: { + type: 'boolean', + required: false, + description: 'Delete source branch after merge', + }, + squash: { + type: 'boolean', + required: false, + description: 'Squash commits on merge', + }, + draft: { + type: 'boolean', + required: false, + description: 'Mark as draft (work in progress)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = { + source_branch: params.sourceBranch, + target_branch: params.targetBranch, + title: params.title, + } + + if (params.description) body.description = params.description + if (params.labels) body.labels = params.labels + if (params.assigneeIds && params.assigneeIds.length > 0) + body.assignee_ids = params.assigneeIds + if (params.milestoneId) body.milestone_id = params.milestoneId + if (params.removeSourceBranch !== undefined) + body.remove_source_branch = params.removeSourceBranch + if (params.squash !== undefined) body.squash = params.squash + if (params.draft !== undefined) body.draft = params.draft + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const mergeRequest = await response.json() + + return { + success: true, + output: { + mergeRequest, + }, + } + }, + + outputs: { + mergeRequest: { + type: 'object', + description: 'The created GitLab merge request', + }, + }, +} diff --git a/apps/sim/tools/gitlab/create_merge_request_note.ts b/apps/sim/tools/gitlab/create_merge_request_note.ts new file mode 100644 index 00000000000..b5778d20eeb --- /dev/null +++ b/apps/sim/tools/gitlab/create_merge_request_note.ts @@ -0,0 +1,85 @@ +import type { + GitLabCreateMergeRequestNoteParams, + GitLabCreateNoteResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreateMergeRequestNoteTool: ToolConfig< + GitLabCreateMergeRequestNoteParams, + GitLabCreateNoteResponse +> = { + id: 'gitlab_create_merge_request_note', + name: 'GitLab Create Merge Request Comment', + description: 'Add a comment to a GitLab merge request', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + mergeRequestIid: { + type: 'number', + required: true, + description: 'Merge request internal ID (IID)', + }, + body: { + type: 'string', + required: true, + description: 'Comment body (Markdown supported)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/notes` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + body: params.body, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const note = await response.json() + + return { + success: true, + output: { + note, + }, + } + }, + + outputs: { + note: { + type: 'object', + description: 'The created comment', + }, + }, +} diff --git a/apps/sim/tools/gitlab/create_pipeline.ts b/apps/sim/tools/gitlab/create_pipeline.ts new file mode 100644 index 00000000000..2214331e103 --- /dev/null +++ b/apps/sim/tools/gitlab/create_pipeline.ts @@ -0,0 +1,91 @@ +import type { GitLabCreatePipelineParams, GitLabCreatePipelineResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreatePipelineTool: ToolConfig< + GitLabCreatePipelineParams, + GitLabCreatePipelineResponse +> = { + id: 'gitlab_create_pipeline', + name: 'GitLab Create Pipeline', + description: 'Trigger a new pipeline in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + ref: { + type: 'string', + required: true, + description: 'Branch or tag to run the pipeline on', + }, + variables: { + type: 'array', + required: false, + description: + 'Array of variables for the pipeline (each with key, value, and optional variable_type)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/pipeline` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = { + ref: params.ref, + } + + if (params.variables && params.variables.length > 0) { + body.variables = params.variables + } + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const pipeline = await response.json() + + return { + success: true, + output: { + pipeline, + }, + } + }, + + outputs: { + pipeline: { + type: 'object', + description: 'The created GitLab pipeline', + }, + }, +} diff --git a/apps/sim/tools/gitlab/delete_issue.ts b/apps/sim/tools/gitlab/delete_issue.ts new file mode 100644 index 00000000000..c56907e3cbc --- /dev/null +++ b/apps/sim/tools/gitlab/delete_issue.ts @@ -0,0 +1,69 @@ +import type { GitLabDeleteIssueParams, GitLabDeleteIssueResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabDeleteIssueTool: ToolConfig = + { + id: 'gitlab_delete_issue', + name: 'GitLab Delete Issue', + description: 'Delete an issue from a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + issueIid: { + type: 'number', + required: true, + description: 'Issue internal ID (IID)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the issue was deleted successfully', + }, + }, + } diff --git a/apps/sim/tools/gitlab/get_issue.ts b/apps/sim/tools/gitlab/get_issue.ts new file mode 100644 index 00000000000..7b0547f32d5 --- /dev/null +++ b/apps/sim/tools/gitlab/get_issue.ts @@ -0,0 +1,70 @@ +import type { GitLabGetIssueParams, GitLabGetIssueResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabGetIssueTool: ToolConfig = { + id: 'gitlab_get_issue', + name: 'GitLab Get Issue', + description: 'Get details of a specific GitLab issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + issueIid: { + type: 'number', + required: true, + description: 'Issue internal ID (IID)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const issue = await response.json() + + return { + success: true, + output: { + issue, + }, + } + }, + + outputs: { + issue: { + type: 'object', + description: 'The GitLab issue details', + }, + }, +} diff --git a/apps/sim/tools/gitlab/get_merge_request.ts b/apps/sim/tools/gitlab/get_merge_request.ts new file mode 100644 index 00000000000..fe5ecd6caaf --- /dev/null +++ b/apps/sim/tools/gitlab/get_merge_request.ts @@ -0,0 +1,76 @@ +import type { + GitLabGetMergeRequestParams, + GitLabGetMergeRequestResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabGetMergeRequestTool: ToolConfig< + GitLabGetMergeRequestParams, + GitLabGetMergeRequestResponse +> = { + id: 'gitlab_get_merge_request', + name: 'GitLab Get Merge Request', + description: 'Get details of a specific GitLab merge request', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + mergeRequestIid: { + type: 'number', + required: true, + description: 'Merge request internal ID (IID)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const mergeRequest = await response.json() + + return { + success: true, + output: { + mergeRequest, + }, + } + }, + + outputs: { + mergeRequest: { + type: 'object', + description: 'The GitLab merge request details', + }, + }, +} diff --git a/apps/sim/tools/gitlab/get_pipeline.ts b/apps/sim/tools/gitlab/get_pipeline.ts new file mode 100644 index 00000000000..a4d9c0c5213 --- /dev/null +++ b/apps/sim/tools/gitlab/get_pipeline.ts @@ -0,0 +1,71 @@ +import type { GitLabGetPipelineParams, GitLabGetPipelineResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabGetPipelineTool: ToolConfig = + { + id: 'gitlab_get_pipeline', + name: 'GitLab Get Pipeline', + description: 'Get details of a specific GitLab pipeline', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + pipelineId: { + type: 'number', + required: true, + description: 'Pipeline ID', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines/${params.pipelineId}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const pipeline = await response.json() + + return { + success: true, + output: { + pipeline, + }, + } + }, + + outputs: { + pipeline: { + type: 'object', + description: 'The GitLab pipeline details', + }, + }, + } diff --git a/apps/sim/tools/gitlab/get_project.ts b/apps/sim/tools/gitlab/get_project.ts new file mode 100644 index 00000000000..84b13e53925 --- /dev/null +++ b/apps/sim/tools/gitlab/get_project.ts @@ -0,0 +1,65 @@ +import type { GitLabGetProjectParams, GitLabGetProjectResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabGetProjectTool: ToolConfig = { + id: 'gitlab_get_project', + name: 'GitLab Get Project', + description: 'Get details of a specific GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path (e.g., "namespace/project")', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const project = await response.json() + + return { + success: true, + output: { + project, + }, + } + }, + + outputs: { + project: { + type: 'object', + description: 'The GitLab project details', + }, + }, +} diff --git a/apps/sim/tools/gitlab/index.ts b/apps/sim/tools/gitlab/index.ts new file mode 100644 index 00000000000..0133af843ab --- /dev/null +++ b/apps/sim/tools/gitlab/index.ts @@ -0,0 +1,45 @@ +import { gitlabCancelPipelineTool } from '@/tools/gitlab/cancel_pipeline' +import { gitlabCreateIssueTool } from '@/tools/gitlab/create_issue' +import { gitlabCreateIssueNoteTool } from '@/tools/gitlab/create_issue_note' +import { gitlabCreateMergeRequestTool } from '@/tools/gitlab/create_merge_request' +import { gitlabCreateMergeRequestNoteTool } from '@/tools/gitlab/create_merge_request_note' +import { gitlabCreatePipelineTool } from '@/tools/gitlab/create_pipeline' +import { gitlabDeleteIssueTool } from '@/tools/gitlab/delete_issue' +import { gitlabGetIssueTool } from '@/tools/gitlab/get_issue' +import { gitlabGetMergeRequestTool } from '@/tools/gitlab/get_merge_request' +import { gitlabGetPipelineTool } from '@/tools/gitlab/get_pipeline' +import { gitlabGetProjectTool } from '@/tools/gitlab/get_project' +import { gitlabListIssuesTool } from '@/tools/gitlab/list_issues' +import { gitlabListMergeRequestsTool } from '@/tools/gitlab/list_merge_requests' +import { gitlabListPipelinesTool } from '@/tools/gitlab/list_pipelines' +import { gitlabListProjectsTool } from '@/tools/gitlab/list_projects' +import { gitlabMergeMergeRequestTool } from '@/tools/gitlab/merge_merge_request' +import { gitlabRetryPipelineTool } from '@/tools/gitlab/retry_pipeline' +import { gitlabUpdateIssueTool } from '@/tools/gitlab/update_issue' +import { gitlabUpdateMergeRequestTool } from '@/tools/gitlab/update_merge_request' + +export { + // Projects + gitlabListProjectsTool, + gitlabGetProjectTool, + // Issues + gitlabListIssuesTool, + gitlabGetIssueTool, + gitlabCreateIssueTool, + gitlabUpdateIssueTool, + gitlabDeleteIssueTool, + gitlabCreateIssueNoteTool, + // Merge Requests + gitlabListMergeRequestsTool, + gitlabGetMergeRequestTool, + gitlabCreateMergeRequestTool, + gitlabUpdateMergeRequestTool, + gitlabMergeMergeRequestTool, + gitlabCreateMergeRequestNoteTool, + // Pipelines + gitlabListPipelinesTool, + gitlabGetPipelineTool, + gitlabCreatePipelineTool, + gitlabRetryPipelineTool, + gitlabCancelPipelineTool, +} diff --git a/apps/sim/tools/gitlab/list_issues.ts b/apps/sim/tools/gitlab/list_issues.ts new file mode 100644 index 00000000000..e8112e0322c --- /dev/null +++ b/apps/sim/tools/gitlab/list_issues.ts @@ -0,0 +1,129 @@ +import type { GitLabListIssuesParams, GitLabListIssuesResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabListIssuesTool: ToolConfig = { + id: 'gitlab_list_issues', + name: 'GitLab List Issues', + description: 'List issues in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + state: { + type: 'string', + required: false, + description: 'Filter by state (opened, closed, all)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + assigneeId: { + type: 'number', + required: false, + description: 'Filter by assignee user ID', + }, + milestoneTitle: { + type: 'string', + required: false, + description: 'Filter by milestone title', + }, + search: { + type: 'string', + required: false, + description: 'Search issues by title and description', + }, + orderBy: { + type: 'string', + required: false, + description: 'Order by field (created_at, updated_at)', + }, + sort: { + type: 'string', + required: false, + description: 'Sort direction (asc, desc)', + }, + perPage: { + type: 'number', + required: false, + description: 'Number of results per page (default 20, max 100)', + }, + page: { + type: 'number', + required: false, + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + const queryParams = new URLSearchParams() + + if (params.state) queryParams.append('state', params.state) + if (params.labels) queryParams.append('labels', params.labels) + if (params.assigneeId) queryParams.append('assignee_id', String(params.assigneeId)) + if (params.milestoneTitle) queryParams.append('milestone', params.milestoneTitle) + if (params.search) queryParams.append('search', params.search) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.sort) queryParams.append('sort', params.sort) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + + const query = queryParams.toString() + return `https://gitlab.com/api/v4/projects/${encodedId}/issues${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const issues = await response.json() + const total = response.headers.get('x-total') + + return { + success: true, + output: { + issues, + total: total ? Number.parseInt(total, 10) : issues.length, + }, + } + }, + + outputs: { + issues: { + type: 'array', + description: 'List of GitLab issues', + }, + total: { + type: 'number', + description: 'Total number of issues', + }, + }, +} diff --git a/apps/sim/tools/gitlab/list_merge_requests.ts b/apps/sim/tools/gitlab/list_merge_requests.ts new file mode 100644 index 00000000000..322c7e0341a --- /dev/null +++ b/apps/sim/tools/gitlab/list_merge_requests.ts @@ -0,0 +1,129 @@ +import type { + GitLabListMergeRequestsParams, + GitLabListMergeRequestsResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabListMergeRequestsTool: ToolConfig< + GitLabListMergeRequestsParams, + GitLabListMergeRequestsResponse +> = { + id: 'gitlab_list_merge_requests', + name: 'GitLab List Merge Requests', + description: 'List merge requests in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + state: { + type: 'string', + required: false, + description: 'Filter by state (opened, closed, merged, all)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + sourceBranch: { + type: 'string', + required: false, + description: 'Filter by source branch', + }, + targetBranch: { + type: 'string', + required: false, + description: 'Filter by target branch', + }, + orderBy: { + type: 'string', + required: false, + description: 'Order by field (created_at, updated_at)', + }, + sort: { + type: 'string', + required: false, + description: 'Sort direction (asc, desc)', + }, + perPage: { + type: 'number', + required: false, + description: 'Number of results per page (default 20, max 100)', + }, + page: { + type: 'number', + required: false, + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + const queryParams = new URLSearchParams() + + if (params.state) queryParams.append('state', params.state) + if (params.labels) queryParams.append('labels', params.labels) + if (params.sourceBranch) queryParams.append('source_branch', params.sourceBranch) + if (params.targetBranch) queryParams.append('target_branch', params.targetBranch) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.sort) queryParams.append('sort', params.sort) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + + const query = queryParams.toString() + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const mergeRequests = await response.json() + const total = response.headers.get('x-total') + + return { + success: true, + output: { + mergeRequests, + total: total ? Number.parseInt(total, 10) : mergeRequests.length, + }, + } + }, + + outputs: { + mergeRequests: { + type: 'array', + description: 'List of GitLab merge requests', + }, + total: { + type: 'number', + description: 'Total number of merge requests', + }, + }, +} diff --git a/apps/sim/tools/gitlab/list_pipelines.ts b/apps/sim/tools/gitlab/list_pipelines.ts new file mode 100644 index 00000000000..7e4812bce21 --- /dev/null +++ b/apps/sim/tools/gitlab/list_pipelines.ts @@ -0,0 +1,115 @@ +import type { GitLabListPipelinesParams, GitLabListPipelinesResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabListPipelinesTool: ToolConfig< + GitLabListPipelinesParams, + GitLabListPipelinesResponse +> = { + id: 'gitlab_list_pipelines', + name: 'GitLab List Pipelines', + description: 'List pipelines in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + ref: { + type: 'string', + required: false, + description: 'Filter by ref (branch or tag)', + }, + status: { + type: 'string', + required: false, + description: + 'Filter by status (created, waiting_for_resource, preparing, pending, running, success, failed, canceled, skipped, manual, scheduled)', + }, + orderBy: { + type: 'string', + required: false, + description: 'Order by field (id, status, ref, updated_at, user_id)', + }, + sort: { + type: 'string', + required: false, + description: 'Sort direction (asc, desc)', + }, + perPage: { + type: 'number', + required: false, + description: 'Number of results per page (default 20, max 100)', + }, + page: { + type: 'number', + required: false, + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + const queryParams = new URLSearchParams() + + if (params.ref) queryParams.append('ref', params.ref) + if (params.status) queryParams.append('status', params.status) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.sort) queryParams.append('sort', params.sort) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + + const query = queryParams.toString() + return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const pipelines = await response.json() + const total = response.headers.get('x-total') + + return { + success: true, + output: { + pipelines, + total: total ? Number.parseInt(total, 10) : pipelines.length, + }, + } + }, + + outputs: { + pipelines: { + type: 'array', + description: 'List of GitLab pipelines', + }, + total: { + type: 'number', + description: 'Total number of pipelines', + }, + }, +} diff --git a/apps/sim/tools/gitlab/list_projects.ts b/apps/sim/tools/gitlab/list_projects.ts new file mode 100644 index 00000000000..3eeec1515ae --- /dev/null +++ b/apps/sim/tools/gitlab/list_projects.ts @@ -0,0 +1,119 @@ +import type { GitLabListProjectsParams, GitLabListProjectsResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabListProjectsTool: ToolConfig< + GitLabListProjectsParams, + GitLabListProjectsResponse +> = { + id: 'gitlab_list_projects', + name: 'GitLab List Projects', + description: 'List GitLab projects accessible to the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + owned: { + type: 'boolean', + required: false, + description: 'Limit to projects owned by the current user', + }, + membership: { + type: 'boolean', + required: false, + description: 'Limit to projects the current user is a member of', + }, + search: { + type: 'string', + required: false, + description: 'Search projects by name', + }, + visibility: { + type: 'string', + required: false, + description: 'Filter by visibility (public, internal, private)', + }, + orderBy: { + type: 'string', + required: false, + description: 'Order by field (id, name, path, created_at, updated_at, last_activity_at)', + }, + sort: { + type: 'string', + required: false, + description: 'Sort direction (asc, desc)', + }, + perPage: { + type: 'number', + required: false, + description: 'Number of results per page (default 20, max 100)', + }, + page: { + type: 'number', + required: false, + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.owned) queryParams.append('owned', 'true') + if (params.membership) queryParams.append('membership', 'true') + if (params.search) queryParams.append('search', params.search) + if (params.visibility) queryParams.append('visibility', params.visibility) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.sort) queryParams.append('sort', params.sort) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + + const query = queryParams.toString() + return `https://gitlab.com/api/v4/projects${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const projects = await response.json() + const total = response.headers.get('x-total') + + return { + success: true, + output: { + projects, + total: total ? Number.parseInt(total, 10) : projects.length, + }, + } + }, + + outputs: { + projects: { + type: 'array', + description: 'List of GitLab projects', + }, + total: { + type: 'number', + description: 'Total number of projects', + }, + }, +} diff --git a/apps/sim/tools/gitlab/merge_merge_request.ts b/apps/sim/tools/gitlab/merge_merge_request.ts new file mode 100644 index 00000000000..41fe7998e45 --- /dev/null +++ b/apps/sim/tools/gitlab/merge_merge_request.ts @@ -0,0 +1,115 @@ +import type { + GitLabMergeMergeRequestParams, + GitLabMergeMergeRequestResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabMergeMergeRequestTool: ToolConfig< + GitLabMergeMergeRequestParams, + GitLabMergeMergeRequestResponse +> = { + id: 'gitlab_merge_merge_request', + name: 'GitLab Merge Merge Request', + description: 'Merge a merge request in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + mergeRequestIid: { + type: 'number', + required: true, + description: 'Merge request internal ID (IID)', + }, + mergeCommitMessage: { + type: 'string', + required: false, + description: 'Custom merge commit message', + }, + squashCommitMessage: { + type: 'string', + required: false, + description: 'Custom squash commit message', + }, + squash: { + type: 'boolean', + required: false, + description: 'Squash commits before merging', + }, + shouldRemoveSourceBranch: { + type: 'boolean', + required: false, + description: 'Delete source branch after merge', + }, + mergeWhenPipelineSucceeds: { + type: 'boolean', + required: false, + description: 'Merge when pipeline succeeds', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/merge` + }, + method: 'PUT', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.mergeCommitMessage) body.merge_commit_message = params.mergeCommitMessage + if (params.squashCommitMessage) body.squash_commit_message = params.squashCommitMessage + if (params.squash !== undefined) body.squash = params.squash + if (params.shouldRemoveSourceBranch !== undefined) + body.should_remove_source_branch = params.shouldRemoveSourceBranch + if (params.mergeWhenPipelineSucceeds !== undefined) + body.merge_when_pipeline_succeeds = params.mergeWhenPipelineSucceeds + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const mergeRequest = await response.json() + + return { + success: true, + output: { + mergeRequest, + }, + } + }, + + outputs: { + mergeRequest: { + type: 'object', + description: 'The merged GitLab merge request', + }, + }, +} diff --git a/apps/sim/tools/gitlab/retry_pipeline.ts b/apps/sim/tools/gitlab/retry_pipeline.ts new file mode 100644 index 00000000000..6d65918d36c --- /dev/null +++ b/apps/sim/tools/gitlab/retry_pipeline.ts @@ -0,0 +1,73 @@ +import type { GitLabRetryPipelineParams, GitLabRetryPipelineResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabRetryPipelineTool: ToolConfig< + GitLabRetryPipelineParams, + GitLabRetryPipelineResponse +> = { + id: 'gitlab_retry_pipeline', + name: 'GitLab Retry Pipeline', + description: 'Retry a failed GitLab pipeline', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + pipelineId: { + type: 'number', + required: true, + description: 'Pipeline ID', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines/${params.pipelineId}/retry` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const pipeline = await response.json() + + return { + success: true, + output: { + pipeline, + }, + } + }, + + outputs: { + pipeline: { + type: 'object', + description: 'The retried GitLab pipeline', + }, + }, +} diff --git a/apps/sim/tools/gitlab/types.ts b/apps/sim/tools/gitlab/types.ts new file mode 100644 index 00000000000..f7d113f8967 --- /dev/null +++ b/apps/sim/tools/gitlab/types.ts @@ -0,0 +1,651 @@ +import type { ToolResponse } from '@/tools/types' + +// ===== Core Types ===== + +export interface GitLabProject { + id: number + name: string + path: string + path_with_namespace: string + description?: string + visibility: string + web_url: string + default_branch?: string + created_at: string + last_activity_at: string + namespace?: { + id: number + name: string + path: string + kind: string + } + owner?: { + id: number + name: string + username: string + } +} + +export interface GitLabIssue { + id: number + iid: number + project_id: number + title: string + description?: string + state: string + created_at: string + updated_at: string + closed_at?: string + labels: string[] + milestone?: { + id: number + iid: number + title: string + } + assignees?: Array<{ + id: number + name: string + username: string + }> + assignee?: { + id: number + name: string + username: string + } + author: { + id: number + name: string + username: string + } + web_url: string + due_date?: string + confidential: boolean +} + +export interface GitLabMergeRequest { + id: number + iid: number + project_id: number + title: string + description?: string + state: string + created_at: string + updated_at: string + merged_at?: string + closed_at?: string + source_branch: string + target_branch: string + source_project_id: number + target_project_id: number + labels: string[] + milestone?: { + id: number + iid: number + title: string + } + assignee?: { + id: number + name: string + username: string + } + assignees?: Array<{ + id: number + name: string + username: string + }> + author: { + id: number + name: string + username: string + } + merge_status: string + web_url: string + draft: boolean + work_in_progress: boolean + has_conflicts: boolean + merge_when_pipeline_succeeds: boolean +} + +export interface GitLabPipeline { + id: number + iid: number + project_id: number + sha: string + ref: string + status: string + source: string + created_at: string + updated_at: string + web_url: string + user?: { + id: number + name: string + username: string + } +} + +export interface GitLabBranch { + name: string + merged: boolean + protected: boolean + default: boolean + developers_can_push: boolean + developers_can_merge: boolean + can_push: boolean + web_url: string + commit?: { + id: string + short_id: string + title: string + author_name: string + authored_date: string + } +} + +export interface GitLabNote { + id: number + body: string + author: { + id: number + name: string + username: string + } + created_at: string + updated_at: string + system: boolean + noteable_id: number + noteable_type: string + noteable_iid?: number +} + +export interface GitLabUser { + id: number + name: string + username: string + email?: string + state: string + avatar_url: string + web_url: string +} + +export interface GitLabLabel { + id: number + name: string + color: string + description?: string + text_color: string +} + +export interface GitLabMilestone { + id: number + iid: number + project_id: number + title: string + description?: string + state: string + created_at: string + updated_at: string + due_date?: string + start_date?: string + web_url: string +} + +// ===== Common Parameters ===== + +interface GitLabBaseParams { + accessToken?: string +} + +// ===== Project Parameters ===== + +export interface GitLabListProjectsParams extends GitLabBaseParams { + owned?: boolean + membership?: boolean + search?: string + visibility?: 'public' | 'internal' | 'private' + orderBy?: 'id' | 'name' | 'path' | 'created_at' | 'updated_at' | 'last_activity_at' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabGetProjectParams extends GitLabBaseParams { + projectId: string | number +} + +// ===== Issue Parameters ===== + +export interface GitLabListIssuesParams extends GitLabBaseParams { + projectId: string | number + state?: 'opened' | 'closed' | 'all' + labels?: string + assigneeId?: number + milestoneTitle?: string + search?: string + orderBy?: 'created_at' | 'updated_at' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabGetIssueParams extends GitLabBaseParams { + projectId: string | number + issueIid: number +} + +export interface GitLabCreateIssueParams extends GitLabBaseParams { + projectId: string | number + title: string + description?: string + labels?: string + assigneeIds?: number[] + milestoneId?: number + dueDate?: string + confidential?: boolean +} + +export interface GitLabUpdateIssueParams extends GitLabBaseParams { + projectId: string | number + issueIid: number + title?: string + description?: string + stateEvent?: 'close' | 'reopen' + labels?: string + assigneeIds?: number[] + milestoneId?: number + dueDate?: string + confidential?: boolean +} + +export interface GitLabDeleteIssueParams extends GitLabBaseParams { + projectId: string | number + issueIid: number +} + +// ===== Merge Request Parameters ===== + +export interface GitLabListMergeRequestsParams extends GitLabBaseParams { + projectId: string | number + state?: 'opened' | 'closed' | 'merged' | 'all' + labels?: string + sourceBranch?: string + targetBranch?: string + orderBy?: 'created_at' | 'updated_at' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabGetMergeRequestParams extends GitLabBaseParams { + projectId: string | number + mergeRequestIid: number +} + +export interface GitLabCreateMergeRequestParams extends GitLabBaseParams { + projectId: string | number + sourceBranch: string + targetBranch: string + title: string + description?: string + labels?: string + assigneeIds?: number[] + milestoneId?: number + removeSourceBranch?: boolean + squash?: boolean + draft?: boolean +} + +export interface GitLabUpdateMergeRequestParams extends GitLabBaseParams { + projectId: string | number + mergeRequestIid: number + title?: string + description?: string + stateEvent?: 'close' | 'reopen' + labels?: string + assigneeIds?: number[] + milestoneId?: number + targetBranch?: string + removeSourceBranch?: boolean + squash?: boolean + draft?: boolean +} + +export interface GitLabMergeMergeRequestParams extends GitLabBaseParams { + projectId: string | number + mergeRequestIid: number + mergeCommitMessage?: string + squashCommitMessage?: string + squash?: boolean + shouldRemoveSourceBranch?: boolean + mergeWhenPipelineSucceeds?: boolean +} + +// ===== Pipeline Parameters ===== + +export interface GitLabListPipelinesParams extends GitLabBaseParams { + projectId: string | number + ref?: string + status?: + | 'created' + | 'waiting_for_resource' + | 'preparing' + | 'pending' + | 'running' + | 'success' + | 'failed' + | 'canceled' + | 'skipped' + | 'manual' + | 'scheduled' + orderBy?: 'id' | 'status' | 'ref' | 'updated_at' | 'user_id' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabGetPipelineParams extends GitLabBaseParams { + projectId: string | number + pipelineId: number +} + +export interface GitLabCreatePipelineParams extends GitLabBaseParams { + projectId: string | number + ref: string + variables?: Array<{ key: string; value: string; variable_type?: 'env_var' | 'file' }> +} + +export interface GitLabRetryPipelineParams extends GitLabBaseParams { + projectId: string | number + pipelineId: number +} + +export interface GitLabCancelPipelineParams extends GitLabBaseParams { + projectId: string | number + pipelineId: number +} + +// ===== Branch Parameters ===== + +export interface GitLabListBranchesParams extends GitLabBaseParams { + projectId: string | number + search?: string + perPage?: number + page?: number +} + +export interface GitLabGetBranchParams extends GitLabBaseParams { + projectId: string | number + branch: string +} + +export interface GitLabCreateBranchParams extends GitLabBaseParams { + projectId: string | number + branch: string + ref: string +} + +export interface GitLabDeleteBranchParams extends GitLabBaseParams { + projectId: string | number + branch: string +} + +// ===== Note/Comment Parameters ===== + +export interface GitLabListIssueNotesParams extends GitLabBaseParams { + projectId: string | number + issueIid: number + orderBy?: 'created_at' | 'updated_at' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabCreateIssueNoteParams extends GitLabBaseParams { + projectId: string | number + issueIid: number + body: string +} + +export interface GitLabListMergeRequestNotesParams extends GitLabBaseParams { + projectId: string | number + mergeRequestIid: number + orderBy?: 'created_at' | 'updated_at' + sort?: 'asc' | 'desc' + perPage?: number + page?: number +} + +export interface GitLabCreateMergeRequestNoteParams extends GitLabBaseParams { + projectId: string | number + mergeRequestIid: number + body: string +} + +// ===== Label Parameters ===== + +export interface GitLabListLabelsParams extends GitLabBaseParams { + projectId: string | number + search?: string + perPage?: number + page?: number +} + +export interface GitLabCreateLabelParams extends GitLabBaseParams { + projectId: string | number + name: string + color: string + description?: string +} + +// ===== User Parameters ===== + +export interface GitLabGetCurrentUserParams extends GitLabBaseParams {} + +export interface GitLabListUsersParams extends GitLabBaseParams { + search?: string + perPage?: number + page?: number +} + +// ===== Response Types ===== + +export interface GitLabListProjectsResponse extends ToolResponse { + output: { + projects?: GitLabProject[] + total?: number + } +} + +export interface GitLabGetProjectResponse extends ToolResponse { + output: { + project?: GitLabProject + } +} + +export interface GitLabListIssuesResponse extends ToolResponse { + output: { + issues?: GitLabIssue[] + total?: number + } +} + +export interface GitLabGetIssueResponse extends ToolResponse { + output: { + issue?: GitLabIssue + } +} + +export interface GitLabCreateIssueResponse extends ToolResponse { + output: { + issue?: GitLabIssue + } +} + +export interface GitLabUpdateIssueResponse extends ToolResponse { + output: { + issue?: GitLabIssue + } +} + +export interface GitLabDeleteIssueResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface GitLabListMergeRequestsResponse extends ToolResponse { + output: { + mergeRequests?: GitLabMergeRequest[] + total?: number + } +} + +export interface GitLabGetMergeRequestResponse extends ToolResponse { + output: { + mergeRequest?: GitLabMergeRequest + } +} + +export interface GitLabCreateMergeRequestResponse extends ToolResponse { + output: { + mergeRequest?: GitLabMergeRequest + } +} + +export interface GitLabUpdateMergeRequestResponse extends ToolResponse { + output: { + mergeRequest?: GitLabMergeRequest + } +} + +export interface GitLabMergeMergeRequestResponse extends ToolResponse { + output: { + mergeRequest?: GitLabMergeRequest + } +} + +export interface GitLabListPipelinesResponse extends ToolResponse { + output: { + pipelines?: GitLabPipeline[] + total?: number + } +} + +export interface GitLabGetPipelineResponse extends ToolResponse { + output: { + pipeline?: GitLabPipeline + } +} + +export interface GitLabCreatePipelineResponse extends ToolResponse { + output: { + pipeline?: GitLabPipeline + } +} + +export interface GitLabRetryPipelineResponse extends ToolResponse { + output: { + pipeline?: GitLabPipeline + } +} + +export interface GitLabCancelPipelineResponse extends ToolResponse { + output: { + pipeline?: GitLabPipeline + } +} + +export interface GitLabListBranchesResponse extends ToolResponse { + output: { + branches?: GitLabBranch[] + total?: number + } +} + +export interface GitLabGetBranchResponse extends ToolResponse { + output: { + branch?: GitLabBranch + } +} + +export interface GitLabCreateBranchResponse extends ToolResponse { + output: { + branch?: GitLabBranch + } +} + +export interface GitLabDeleteBranchResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface GitLabListNotesResponse extends ToolResponse { + output: { + notes?: GitLabNote[] + total?: number + } +} + +export interface GitLabCreateNoteResponse extends ToolResponse { + output: { + note?: GitLabNote + } +} + +export interface GitLabListLabelsResponse extends ToolResponse { + output: { + labels?: GitLabLabel[] + total?: number + } +} + +export interface GitLabCreateLabelResponse extends ToolResponse { + output: { + label?: GitLabLabel + } +} + +export interface GitLabGetCurrentUserResponse extends ToolResponse { + output: { + user?: GitLabUser + } +} + +export interface GitLabListUsersResponse extends ToolResponse { + output: { + users?: GitLabUser[] + total?: number + } +} + +// ===== Union Response Type ===== + +export type GitLabResponse = + | GitLabListProjectsResponse + | GitLabGetProjectResponse + | GitLabListIssuesResponse + | GitLabGetIssueResponse + | GitLabCreateIssueResponse + | GitLabUpdateIssueResponse + | GitLabDeleteIssueResponse + | GitLabListMergeRequestsResponse + | GitLabGetMergeRequestResponse + | GitLabCreateMergeRequestResponse + | GitLabUpdateMergeRequestResponse + | GitLabMergeMergeRequestResponse + | GitLabListPipelinesResponse + | GitLabGetPipelineResponse + | GitLabCreatePipelineResponse + | GitLabRetryPipelineResponse + | GitLabCancelPipelineResponse + | GitLabListBranchesResponse + | GitLabGetBranchResponse + | GitLabCreateBranchResponse + | GitLabDeleteBranchResponse + | GitLabListNotesResponse + | GitLabCreateNoteResponse + | GitLabListLabelsResponse + | GitLabCreateLabelResponse + | GitLabGetCurrentUserResponse + | GitLabListUsersResponse diff --git a/apps/sim/tools/gitlab/update_issue.ts b/apps/sim/tools/gitlab/update_issue.ts new file mode 100644 index 00000000000..9f81f10251d --- /dev/null +++ b/apps/sim/tools/gitlab/update_issue.ts @@ -0,0 +1,126 @@ +import type { GitLabUpdateIssueParams, GitLabUpdateIssueResponse } from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabUpdateIssueTool: ToolConfig = + { + id: 'gitlab_update_issue', + name: 'GitLab Update Issue', + description: 'Update an existing issue in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + issueIid: { + type: 'number', + required: true, + description: 'Issue internal ID (IID)', + }, + title: { + type: 'string', + required: false, + description: 'New issue title', + }, + description: { + type: 'string', + required: false, + description: 'New issue description (Markdown supported)', + }, + stateEvent: { + type: 'string', + required: false, + description: 'State event (close or reopen)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + assigneeIds: { + type: 'array', + required: false, + description: 'Array of user IDs to assign', + }, + milestoneId: { + type: 'number', + required: false, + description: 'Milestone ID to assign', + }, + dueDate: { + type: 'string', + required: false, + description: 'Due date in YYYY-MM-DD format', + }, + confidential: { + type: 'boolean', + required: false, + description: 'Whether the issue is confidential', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}` + }, + method: 'PUT', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.description !== undefined) body.description = params.description + if (params.stateEvent) body.state_event = params.stateEvent + if (params.labels !== undefined) body.labels = params.labels + if (params.assigneeIds) body.assignee_ids = params.assigneeIds + if (params.milestoneId !== undefined) body.milestone_id = params.milestoneId + if (params.dueDate !== undefined) body.due_date = params.dueDate + if (params.confidential !== undefined) body.confidential = params.confidential + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const issue = await response.json() + + return { + success: true, + output: { + issue, + }, + } + }, + + outputs: { + issue: { + type: 'object', + description: 'The updated GitLab issue', + }, + }, + } diff --git a/apps/sim/tools/gitlab/update_merge_request.ts b/apps/sim/tools/gitlab/update_merge_request.ts new file mode 100644 index 00000000000..a646f7f4a0d --- /dev/null +++ b/apps/sim/tools/gitlab/update_merge_request.ts @@ -0,0 +1,144 @@ +import type { + GitLabUpdateMergeRequestParams, + GitLabUpdateMergeRequestResponse, +} from '@/tools/gitlab/types' +import type { ToolConfig } from '@/tools/types' + +export const gitlabUpdateMergeRequestTool: ToolConfig< + GitLabUpdateMergeRequestParams, + GitLabUpdateMergeRequestResponse +> = { + id: 'gitlab_update_merge_request', + name: 'GitLab Update Merge Request', + description: 'Update an existing merge request in a GitLab project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'gitlab', + }, + + params: { + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + mergeRequestIid: { + type: 'number', + required: true, + description: 'Merge request internal ID (IID)', + }, + title: { + type: 'string', + required: false, + description: 'New merge request title', + }, + description: { + type: 'string', + required: false, + description: 'New merge request description', + }, + stateEvent: { + type: 'string', + required: false, + description: 'State event (close or reopen)', + }, + labels: { + type: 'string', + required: false, + description: 'Comma-separated list of label names', + }, + assigneeIds: { + type: 'array', + required: false, + description: 'Array of user IDs to assign', + }, + milestoneId: { + type: 'number', + required: false, + description: 'Milestone ID to assign', + }, + targetBranch: { + type: 'string', + required: false, + description: 'New target branch', + }, + removeSourceBranch: { + type: 'boolean', + required: false, + description: 'Delete source branch after merge', + }, + squash: { + type: 'boolean', + required: false, + description: 'Squash commits on merge', + }, + draft: { + type: 'boolean', + required: false, + description: 'Mark as draft (work in progress)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}` + }, + method: 'PUT', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for GitLab API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.description !== undefined) body.description = params.description + if (params.stateEvent) body.state_event = params.stateEvent + if (params.labels !== undefined) body.labels = params.labels + if (params.assigneeIds !== undefined) body.assignee_ids = params.assigneeIds + if (params.milestoneId !== undefined) body.milestone_id = params.milestoneId + if (params.targetBranch) body.target_branch = params.targetBranch + if (params.removeSourceBranch !== undefined) + body.remove_source_branch = params.removeSourceBranch + if (params.squash !== undefined) body.squash = params.squash + if (params.draft !== undefined) body.draft = params.draft + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const mergeRequest = await response.json() + + return { + success: true, + output: { + mergeRequest, + }, + } + }, + + outputs: { + mergeRequest: { + type: 'object', + description: 'The updated GitLab merge request', + }, + }, +} diff --git a/apps/sim/tools/grafana/alerts.ts b/apps/sim/tools/grafana/alerts.ts new file mode 100644 index 00000000000..eff8579b365 --- /dev/null +++ b/apps/sim/tools/grafana/alerts.ts @@ -0,0 +1,768 @@ +import type { + GrafanaCreateAlertRuleParams, + GrafanaCreateAlertRuleResponse, + GrafanaDeleteAlertRuleParams, + GrafanaDeleteAlertRuleResponse, + GrafanaGetAlertRuleParams, + GrafanaGetAlertRuleResponse, + GrafanaListAlertRulesParams, + GrafanaListAlertRulesResponse, + GrafanaListContactPointsParams, + GrafanaListContactPointsResponse, + GrafanaUpdateAlertRuleParams, + GrafanaUpdateAlertRuleResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listAlertRulesTool: ToolConfig< + GrafanaListAlertRulesParams, + GrafanaListAlertRulesResponse +> = { + id: 'grafana_list_alert_rules', + name: 'Grafana List Alert Rules', + description: 'List all alert rules in the Grafana instance', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + rules: Array.isArray(data) + ? data.map((rule: any) => ({ + uid: rule.uid, + title: rule.title, + condition: rule.condition, + data: rule.data, + updated: rule.updated, + noDataState: rule.noDataState, + execErrState: rule.execErrState, + for: rule.for, + annotations: rule.annotations || {}, + labels: rule.labels || {}, + isPaused: rule.isPaused || false, + folderUID: rule.folderUID, + ruleGroup: rule.ruleGroup, + orgId: rule.orgId, + namespace_uid: rule.namespace_uid, + namespace_id: rule.namespace_id, + provenance: rule.provenance || '', + })) + : [], + }, + } + }, + + outputs: { + rules: { + type: 'array', + description: 'List of alert rules', + items: { + type: 'object', + properties: { + uid: { type: 'string', description: 'Alert rule UID' }, + title: { type: 'string', description: 'Alert rule title' }, + condition: { type: 'string', description: 'Alert condition' }, + folderUID: { type: 'string', description: 'Parent folder UID' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + noDataState: { type: 'string', description: 'State when no data is returned' }, + execErrState: { type: 'string', description: 'State on execution error' }, + }, + }, + }, + }, +} + +export const getAlertRuleTool: ToolConfig = + { + id: 'grafana_get_alert_rule', + name: 'Grafana Get Alert Rule', + description: 'Get a specific alert rule by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to retrieve', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'Alert rule UID', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + condition: { + type: 'string', + description: 'Alert condition', + }, + data: { + type: 'json', + description: 'Alert rule query data', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + noDataState: { + type: 'string', + description: 'State when no data is returned', + }, + execErrState: { + type: 'string', + description: 'State on execution error', + }, + annotations: { + type: 'json', + description: 'Alert annotations', + }, + labels: { + type: 'json', + description: 'Alert labels', + }, + }, + } + +export const createAlertRuleTool: ToolConfig< + GrafanaCreateAlertRuleParams, + GrafanaCreateAlertRuleResponse +> = { + id: 'grafana_create_alert_rule', + name: 'Grafana Create Alert Rule', + description: 'Create a new alert rule', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the alert rule', + }, + folderUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the folder to create the alert in', + }, + ruleGroup: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the rule group', + }, + condition: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The refId of the query or expression to use as the alert condition', + }, + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'JSON array of query/expression data objects', + }, + forDuration: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Duration to wait before firing (e.g., 5m, 1h)', + }, + noDataState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State when no data is returned (NoData, Alerting, OK)', + }, + execErrState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State on execution error (Alerting, OK)', + }, + annotations: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of annotations', + }, + labels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of labels', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + let dataArray: any[] = [] + try { + dataArray = JSON.parse(params.data) + } catch { + throw new Error('Invalid JSON for data parameter') + } + + const body: Record = { + title: params.title, + folderUID: params.folderUid, + ruleGroup: params.ruleGroup, + condition: params.condition, + data: dataArray, + for: params.forDuration || '5m', + noDataState: params.noDataState || 'NoData', + execErrState: params.execErrState || 'Alerting', + } + + if (params.annotations) { + try { + body.annotations = JSON.parse(params.annotations) + } catch { + body.annotations = {} + } + } + + if (params.labels) { + try { + body.labels = JSON.parse(params.labels) + } catch { + body.labels = {} + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'The UID of the created alert rule', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + }, +} + +export const updateAlertRuleTool: ToolConfig< + GrafanaUpdateAlertRuleParams, + GrafanaUpdateAlertRuleResponse +> = { + id: 'grafana_update_alert_rule', + name: 'Grafana Update Alert Rule', + description: 'Update an existing alert rule', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the alert rule', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New folder UID to move the alert to', + }, + ruleGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New rule group name', + }, + condition: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New condition refId', + }, + data: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New JSON array of query/expression data objects', + }, + forDuration: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Duration to wait before firing (e.g., 5m, 1h)', + }, + noDataState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State when no data is returned (NoData, Alerting, OK)', + }, + execErrState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State on execution error (Alerting, OK)', + }, + annotations: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of annotations', + }, + labels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of labels', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'PUT', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.folderUid) body.folderUID = params.folderUid + if (params.ruleGroup) body.ruleGroup = params.ruleGroup + if (params.condition) body.condition = params.condition + if (params.forDuration) body.for = params.forDuration + if (params.noDataState) body.noDataState = params.noDataState + if (params.execErrState) body.execErrState = params.execErrState + + if (params.data) { + try { + body.data = JSON.parse(params.data) + } catch { + // Skip if invalid JSON + } + } + + if (params.annotations) { + try { + body.annotations = JSON.parse(params.annotations) + } catch { + // Skip if invalid JSON + } + } + + if (params.labels) { + try { + body.labels = JSON.parse(params.labels) + } catch { + // Skip if invalid JSON + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'The UID of the updated alert rule', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + }, +} + +export const deleteAlertRuleTool: ToolConfig< + GrafanaDeleteAlertRuleParams, + GrafanaDeleteAlertRuleResponse +> = { + id: 'grafana_delete_alert_rule', + name: 'Grafana Delete Alert Rule', + description: 'Delete an alert rule by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to delete', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async () => { + return { + success: true, + output: { + message: 'Alert rule deleted successfully', + }, + } + }, + + outputs: { + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} + +export const listContactPointsTool: ToolConfig< + GrafanaListContactPointsParams, + GrafanaListContactPointsResponse +> = { + id: 'grafana_list_contact_points', + name: 'Grafana List Contact Points', + description: 'List all alert notification contact points', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + contactPoints: Array.isArray(data) + ? data.map((cp: any) => ({ + uid: cp.uid, + name: cp.name, + type: cp.type, + settings: cp.settings || {}, + disableResolveMessage: cp.disableResolveMessage || false, + provenance: cp.provenance || '', + })) + : [], + }, + } + }, + + outputs: { + contactPoints: { + type: 'array', + description: 'List of contact points', + items: { + type: 'object', + properties: { + uid: { type: 'string', description: 'Contact point UID' }, + name: { type: 'string', description: 'Contact point name' }, + type: { type: 'string', description: 'Notification type (email, slack, etc.)' }, + settings: { type: 'object', description: 'Type-specific settings' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/annotations.ts b/apps/sim/tools/grafana/annotations.ts new file mode 100644 index 00000000000..3c7688107c6 --- /dev/null +++ b/apps/sim/tools/grafana/annotations.ts @@ -0,0 +1,491 @@ +import type { + GrafanaCreateAnnotationParams, + GrafanaCreateAnnotationResponse, + GrafanaDeleteAnnotationParams, + GrafanaDeleteAnnotationResponse, + GrafanaListAnnotationsParams, + GrafanaListAnnotationsResponse, + GrafanaUpdateAnnotationParams, + GrafanaUpdateAnnotationResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createAnnotationTool: ToolConfig< + GrafanaCreateAnnotationParams, + GrafanaCreateAnnotationResponse +> = { + id: 'grafana_create_annotation', + name: 'Grafana Create Annotation', + description: 'Create an annotation on a dashboard or as a global annotation', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text content of the annotation', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags', + }, + dashboardUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'UID of the dashboard to add the annotation to (optional for global annotations)', + }, + panelId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'ID of the panel to add the annotation to', + }, + time: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time in epoch milliseconds (defaults to now)', + }, + timeEnd: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time in epoch milliseconds (for range annotations)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + text: params.text, + time: params.time || Date.now(), + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + + if (params.dashboardUid) { + body.dashboardUID = params.dashboardUid + } + + if (params.panelId) { + body.panelId = params.panelId + } + + if (params.timeEnd) { + body.timeEnd = params.timeEnd + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + message: data.message || 'Annotation created successfully', + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The ID of the created annotation', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} + +export const listAnnotationsTool: ToolConfig< + GrafanaListAnnotationsParams, + GrafanaListAnnotationsResponse +> = { + id: 'grafana_list_annotations', + name: 'Grafana List Annotations', + description: 'Query annotations by time range, dashboard, or tags', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + from: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time in epoch milliseconds', + }, + to: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time in epoch milliseconds', + }, + dashboardUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by dashboard UID', + }, + panelId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Filter by panel ID', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags to filter by', + }, + type: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by type (alert or annotation)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of annotations to return', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + + if (params.from) searchParams.set('from', String(params.from)) + if (params.to) searchParams.set('to', String(params.to)) + if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid) + if (params.panelId) searchParams.set('panelId', String(params.panelId)) + if (params.tags) { + params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim())) + } + if (params.type) searchParams.set('type', params.type) + if (params.limit) searchParams.set('limit', String(params.limit)) + + const queryString = searchParams.toString() + return `${baseUrl}/api/annotations${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + annotations: Array.isArray(data) + ? data.map((a: any) => ({ + id: a.id, + alertId: a.alertId, + alertName: a.alertName, + dashboardId: a.dashboardId, + dashboardUID: a.dashboardUID, + panelId: a.panelId, + userId: a.userId, + newState: a.newState, + prevState: a.prevState, + created: a.created, + updated: a.updated, + time: a.time, + timeEnd: a.timeEnd, + text: a.text, + tags: a.tags || [], + login: a.login, + email: a.email, + avatarUrl: a.avatarUrl, + data: a.data, + })) + : [], + }, + } + }, + + outputs: { + annotations: { + type: 'array', + description: 'List of annotations', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Annotation ID' }, + text: { type: 'string', description: 'Annotation text' }, + tags: { type: 'array', description: 'Annotation tags' }, + time: { type: 'number', description: 'Start time in epoch ms' }, + timeEnd: { type: 'number', description: 'End time in epoch ms' }, + dashboardUID: { type: 'string', description: 'Dashboard UID' }, + panelId: { type: 'number', description: 'Panel ID' }, + }, + }, + }, + }, +} + +export const updateAnnotationTool: ToolConfig< + GrafanaUpdateAnnotationParams, + GrafanaUpdateAnnotationResponse +> = { + id: 'grafana_update_annotation', + name: 'Grafana Update Annotation', + description: 'Update an existing annotation', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + annotationId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the annotation to update', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New text content for the annotation', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of new tags', + }, + time: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New start time in epoch milliseconds', + }, + timeEnd: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New end time in epoch milliseconds', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, + method: 'PATCH', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + text: params.text, + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + + if (params.time) { + body.time = params.time + } + + if (params.timeEnd) { + body.timeEnd = params.timeEnd + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id || 0, + message: data.message || 'Annotation updated successfully', + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The ID of the updated annotation', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} + +export const deleteAnnotationTool: ToolConfig< + GrafanaDeleteAnnotationParams, + GrafanaDeleteAnnotationResponse +> = { + id: 'grafana_delete_annotation', + name: 'Grafana Delete Annotation', + description: 'Delete an annotation by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + annotationId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the annotation to delete', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + message: data.message || 'Annotation deleted successfully', + }, + } + }, + + outputs: { + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/grafana/dashboards.ts b/apps/sim/tools/grafana/dashboards.ts new file mode 100644 index 00000000000..dcb98cf8614 --- /dev/null +++ b/apps/sim/tools/grafana/dashboards.ts @@ -0,0 +1,663 @@ +import type { + GrafanaCreateDashboardParams, + GrafanaCreateDashboardResponse, + GrafanaDeleteDashboardParams, + GrafanaDeleteDashboardResponse, + GrafanaGetDashboardParams, + GrafanaGetDashboardResponse, + GrafanaListDashboardsParams, + GrafanaListDashboardsResponse, + GrafanaUpdateDashboardParams, + GrafanaUpdateDashboardResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const getDashboardTool: ToolConfig = + { + id: 'grafana_get_dashboard', + name: 'Grafana Get Dashboard', + description: 'Get a dashboard by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to retrieve', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dashboard: data.dashboard, + meta: data.meta, + }, + } + }, + + outputs: { + dashboard: { + type: 'json', + description: 'The full dashboard JSON object', + }, + meta: { + type: 'json', + description: 'Dashboard metadata (version, permissions, etc.)', + }, + }, + } + +export const listDashboardsTool: ToolConfig< + GrafanaListDashboardsParams, + GrafanaListDashboardsResponse +> = { + id: 'grafana_list_dashboards', + name: 'Grafana List Dashboards', + description: 'Search and list all dashboards', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter dashboards by title', + }, + tag: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by tag (comma-separated for multiple tags)', + }, + folderIds: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by folder IDs (comma-separated)', + }, + starred: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Only return starred dashboards', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of dashboards to return', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + searchParams.set('type', 'dash-db') + + if (params.query) searchParams.set('query', params.query) + if (params.tag) { + params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim())) + } + if (params.folderIds) { + params.folderIds.split(',').forEach((id) => searchParams.append('folderIds', id.trim())) + } + if (params.starred) searchParams.set('starred', 'true') + if (params.limit) searchParams.set('limit', String(params.limit)) + + return `${baseUrl}/api/search?${searchParams.toString()}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dashboards: Array.isArray(data) + ? data.map((d: any) => ({ + id: d.id, + uid: d.uid, + title: d.title, + uri: d.uri, + url: d.url, + slug: d.slug, + type: d.type, + tags: d.tags || [], + isStarred: d.isStarred || false, + folderId: d.folderId, + folderUid: d.folderUid, + folderTitle: d.folderTitle, + folderUrl: d.folderUrl, + sortMeta: d.sortMeta, + })) + : [], + }, + } + }, + + outputs: { + dashboards: { + type: 'array', + description: 'List of dashboard search results', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Dashboard ID' }, + uid: { type: 'string', description: 'Dashboard UID' }, + title: { type: 'string', description: 'Dashboard title' }, + url: { type: 'string', description: 'Dashboard URL path' }, + tags: { type: 'array', description: 'Dashboard tags' }, + folderTitle: { type: 'string', description: 'Parent folder title' }, + }, + }, + }, + }, +} + +export const createDashboardTool: ToolConfig< + GrafanaCreateDashboardParams, + GrafanaCreateDashboardResponse +> = { + id: 'grafana_create_dashboard', + name: 'Grafana Create Dashboard', + description: 'Create a new dashboard', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new dashboard', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The UID of the folder to create the dashboard in', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Dashboard timezone (e.g., browser, utc)', + }, + refresh: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', + }, + panels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of panel configurations', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Overwrite existing dashboard with same title', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Commit message for the dashboard version', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const dashboard: Record = { + title: params.title, + tags: params.tags + ? params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + : [], + timezone: params.timezone || 'browser', + schemaVersion: 39, + version: 0, + refresh: params.refresh || '', + } + + if (params.panels) { + try { + dashboard.panels = JSON.parse(params.panels) + } catch { + dashboard.panels = [] + } + } else { + dashboard.panels = [] + } + + const body: Record = { + dashboard, + overwrite: params.overwrite || false, + } + + if (params.folderUid) { + body.folderUid = params.folderUid + } + + if (params.message) { + body.message = params.message + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + url: data.url, + status: data.status, + version: data.version, + slug: data.slug, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the created dashboard', + }, + uid: { + type: 'string', + description: 'The UID of the created dashboard', + }, + url: { + type: 'string', + description: 'The URL path to the dashboard', + }, + status: { + type: 'string', + description: 'Status of the operation (success)', + }, + version: { + type: 'number', + description: 'The version number of the dashboard', + }, + slug: { + type: 'string', + description: 'URL-friendly slug of the dashboard', + }, + }, +} + +export const updateDashboardTool: ToolConfig< + GrafanaUpdateDashboardParams, + GrafanaUpdateDashboardResponse +> = { + id: 'grafana_update_dashboard', + name: 'Grafana Update Dashboard', + description: 'Update an existing dashboard', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the dashboard', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New folder UID to move the dashboard to', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of new tags', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Dashboard timezone (e.g., browser, utc)', + }, + refresh: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', + }, + panels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of panel configurations', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Overwrite even if there is a version conflict', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Commit message for this version', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const dashboard: Record = { + uid: params.dashboardUid, + } + + if (params.title) dashboard.title = params.title + if (params.tags) { + dashboard.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + if (params.timezone) dashboard.timezone = params.timezone + if (params.refresh) dashboard.refresh = params.refresh + if (params.panels) { + try { + dashboard.panels = JSON.parse(params.panels) + } catch { + // Keep existing panels if parse fails + } + } + + const body: Record = { + dashboard, + overwrite: params.overwrite !== false, + } + + if (params.folderUid) { + body.folderUid = params.folderUid + } + + if (params.message) { + body.message = params.message + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + url: data.url, + status: data.status, + version: data.version, + slug: data.slug, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the updated dashboard', + }, + uid: { + type: 'string', + description: 'The UID of the updated dashboard', + }, + url: { + type: 'string', + description: 'The URL path to the dashboard', + }, + status: { + type: 'string', + description: 'Status of the operation (success)', + }, + version: { + type: 'number', + description: 'The new version number of the dashboard', + }, + slug: { + type: 'string', + description: 'URL-friendly slug of the dashboard', + }, + }, +} + +export const deleteDashboardTool: ToolConfig< + GrafanaDeleteDashboardParams, + GrafanaDeleteDashboardResponse +> = { + id: 'grafana_delete_dashboard', + name: 'Grafana Delete Dashboard', + description: 'Delete a dashboard by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to delete', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + title: data.title || '', + message: data.message || 'Dashboard deleted', + id: data.id || 0, + }, + } + }, + + outputs: { + title: { + type: 'string', + description: 'The title of the deleted dashboard', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + id: { + type: 'number', + description: 'The ID of the deleted dashboard', + }, + }, +} diff --git a/apps/sim/tools/grafana/data_sources.ts b/apps/sim/tools/grafana/data_sources.ts new file mode 100644 index 00000000000..30a9ac91b3b --- /dev/null +++ b/apps/sim/tools/grafana/data_sources.ts @@ -0,0 +1,220 @@ +import type { + GrafanaGetDataSourceParams, + GrafanaGetDataSourceResponse, + GrafanaListDataSourcesParams, + GrafanaListDataSourcesResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listDataSourcesTool: ToolConfig< + GrafanaListDataSourcesParams, + GrafanaListDataSourcesResponse +> = { + id: 'grafana_list_data_sources', + name: 'Grafana List Data Sources', + description: 'List all data sources configured in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/datasources`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dataSources: Array.isArray(data) + ? data.map((ds: any) => ({ + id: ds.id, + uid: ds.uid, + orgId: ds.orgId, + name: ds.name, + type: ds.type, + typeName: ds.typeName, + typeLogoUrl: ds.typeLogoUrl, + access: ds.access, + url: ds.url, + user: ds.user, + database: ds.database, + basicAuth: ds.basicAuth || false, + isDefault: ds.isDefault || false, + jsonData: ds.jsonData || {}, + readOnly: ds.readOnly || false, + })) + : [], + }, + } + }, + + outputs: { + dataSources: { + type: 'array', + description: 'List of data sources', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Data source ID' }, + uid: { type: 'string', description: 'Data source UID' }, + name: { type: 'string', description: 'Data source name' }, + type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' }, + url: { type: 'string', description: 'Data source URL' }, + isDefault: { type: 'boolean', description: 'Whether this is the default data source' }, + }, + }, + }, + }, +} + +export const getDataSourceTool: ToolConfig< + GrafanaGetDataSourceParams, + GrafanaGetDataSourceResponse +> = { + id: 'grafana_get_data_source', + name: 'Grafana Get Data Source', + description: 'Get a data source by its ID or UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dataSourceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID or UID of the data source to retrieve', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + // Check if it looks like a UID (contains non-numeric characters) or ID + const isUid = /[^0-9]/.test(params.dataSourceId) + if (isUid) { + return `${baseUrl}/api/datasources/uid/${params.dataSourceId}` + } + return `${baseUrl}/api/datasources/${params.dataSourceId}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + orgId: data.orgId, + name: data.name, + type: data.type, + typeName: data.typeName, + typeLogoUrl: data.typeLogoUrl, + access: data.access, + url: data.url, + user: data.user, + database: data.database, + basicAuth: data.basicAuth || false, + isDefault: data.isDefault || false, + jsonData: data.jsonData || {}, + readOnly: data.readOnly || false, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'Data source ID', + }, + uid: { + type: 'string', + description: 'Data source UID', + }, + name: { + type: 'string', + description: 'Data source name', + }, + type: { + type: 'string', + description: 'Data source type', + }, + url: { + type: 'string', + description: 'Data source connection URL', + }, + database: { + type: 'string', + description: 'Database name (if applicable)', + }, + isDefault: { + type: 'boolean', + description: 'Whether this is the default data source', + }, + jsonData: { + type: 'json', + description: 'Additional data source configuration', + }, + }, +} diff --git a/apps/sim/tools/grafana/folders.ts b/apps/sim/tools/grafana/folders.ts new file mode 100644 index 00000000000..9232c2a1303 --- /dev/null +++ b/apps/sim/tools/grafana/folders.ts @@ -0,0 +1,225 @@ +import type { + GrafanaCreateFolderParams, + GrafanaCreateFolderResponse, + GrafanaListFoldersParams, + GrafanaListFoldersResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listFoldersTool: ToolConfig = { + id: 'grafana_list_folders', + name: 'Grafana List Folders', + description: 'List all folders in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of folders to return', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + + if (params.limit) searchParams.set('limit', String(params.limit)) + if (params.page) searchParams.set('page', String(params.page)) + + const queryString = searchParams.toString() + return `${baseUrl}/api/folders${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + folders: Array.isArray(data) + ? data.map((f: any) => ({ + id: f.id, + uid: f.uid, + title: f.title, + url: f.url, + hasAcl: f.hasAcl || false, + canSave: f.canSave || false, + canEdit: f.canEdit || false, + canAdmin: f.canAdmin || false, + canDelete: f.canDelete || false, + createdBy: f.createdBy || '', + created: f.created || '', + updatedBy: f.updatedBy || '', + updated: f.updated || '', + version: f.version || 0, + })) + : [], + }, + } + }, + + outputs: { + folders: { + type: 'array', + description: 'List of folders', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Folder ID' }, + uid: { type: 'string', description: 'Folder UID' }, + title: { type: 'string', description: 'Folder title' }, + url: { type: 'string', description: 'Folder URL path' }, + }, + }, + }, + }, +} + +export const createFolderTool: ToolConfig = + { + id: 'grafana_create_folder', + name: 'Grafana Create Folder', + description: 'Create a new folder in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new folder', + }, + uid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional UID for the folder (auto-generated if not provided)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + title: params.title, + } + + if (params.uid) { + body.uid = params.uid + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + title: data.title, + url: data.url, + hasAcl: data.hasAcl || false, + canSave: data.canSave || false, + canEdit: data.canEdit || false, + canAdmin: data.canAdmin || false, + canDelete: data.canDelete || false, + createdBy: data.createdBy || '', + created: data.created || '', + updatedBy: data.updatedBy || '', + updated: data.updated || '', + version: data.version || 0, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the created folder', + }, + uid: { + type: 'string', + description: 'The UID of the created folder', + }, + title: { + type: 'string', + description: 'The title of the created folder', + }, + url: { + type: 'string', + description: 'The URL path to the folder', + }, + }, + } diff --git a/apps/sim/tools/grafana/health.ts b/apps/sim/tools/grafana/health.ts new file mode 100644 index 00000000000..4769aa3276b --- /dev/null +++ b/apps/sim/tools/grafana/health.ts @@ -0,0 +1,155 @@ +import type { + GrafanaDataSourceHealthParams, + GrafanaDataSourceHealthResponse, + GrafanaHealthCheckParams, + GrafanaHealthCheckResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const healthCheckTool: ToolConfig = { + id: 'grafana_health_check', + name: 'Grafana Health Check', + description: 'Check the health status of a Grafana instance', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + commit: data.commit || '', + database: data.database || 'ok', + version: data.version || '', + }, + } + }, + + outputs: { + commit: { + type: 'string', + description: 'Git commit hash of the Grafana build', + }, + database: { + type: 'string', + description: 'Database connection status (ok or failing)', + }, + version: { + type: 'string', + description: 'Grafana version number', + }, + }, +} + +export const dataSourceHealthTool: ToolConfig< + GrafanaDataSourceHealthParams, + GrafanaDataSourceHealthResponse +> = { + id: 'grafana_data_source_health', + name: 'Grafana Data Source Health', + description: + 'Test the health of a specific data source connection. Note: This endpoint is deprecated since Grafana v9.0.0 and may not work on newer versions.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dataSourceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID or UID of the data source to check', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/datasources/${params.dataSourceId}/health`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: data.status || 'Unknown', + message: data.message || '', + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Health status (OK or ERROR)', + }, + message: { + type: 'string', + description: 'Status message or error description', + }, + }, +} diff --git a/apps/sim/tools/grafana/index.ts b/apps/sim/tools/grafana/index.ts new file mode 100644 index 00000000000..2247c15416b --- /dev/null +++ b/apps/sim/tools/grafana/index.ts @@ -0,0 +1,57 @@ +import { + createAlertRuleTool, + deleteAlertRuleTool, + getAlertRuleTool, + listAlertRulesTool, + listContactPointsTool, + updateAlertRuleTool, +} from '@/tools/grafana/alerts' +import { + createAnnotationTool, + deleteAnnotationTool, + listAnnotationsTool, + updateAnnotationTool, +} from '@/tools/grafana/annotations' +import { + createDashboardTool, + deleteDashboardTool, + getDashboardTool, + listDashboardsTool, + updateDashboardTool, +} from '@/tools/grafana/dashboards' +import { getDataSourceTool, listDataSourcesTool } from '@/tools/grafana/data_sources' +import { createFolderTool, listFoldersTool } from '@/tools/grafana/folders' +import { dataSourceHealthTool, healthCheckTool } from '@/tools/grafana/health' + +// Health tools +export const grafanaHealthCheckTool = healthCheckTool +export const grafanaDataSourceHealthTool = dataSourceHealthTool + +// Dashboard tools +export const grafanaGetDashboardTool = getDashboardTool +export const grafanaListDashboardsTool = listDashboardsTool +export const grafanaCreateDashboardTool = createDashboardTool +export const grafanaUpdateDashboardTool = updateDashboardTool +export const grafanaDeleteDashboardTool = deleteDashboardTool + +// Alert tools +export const grafanaListAlertRulesTool = listAlertRulesTool +export const grafanaGetAlertRuleTool = getAlertRuleTool +export const grafanaCreateAlertRuleTool = createAlertRuleTool +export const grafanaUpdateAlertRuleTool = updateAlertRuleTool +export const grafanaDeleteAlertRuleTool = deleteAlertRuleTool +export const grafanaListContactPointsTool = listContactPointsTool + +// Annotation tools +export const grafanaCreateAnnotationTool = createAnnotationTool +export const grafanaListAnnotationsTool = listAnnotationsTool +export const grafanaUpdateAnnotationTool = updateAnnotationTool +export const grafanaDeleteAnnotationTool = deleteAnnotationTool + +// Data Source tools +export const grafanaListDataSourcesTool = listDataSourcesTool +export const grafanaGetDataSourceTool = getDataSourceTool + +// Folder tools +export const grafanaListFoldersTool = listFoldersTool +export const grafanaCreateFolderTool = createFolderTool diff --git a/apps/sim/tools/grafana/types.ts b/apps/sim/tools/grafana/types.ts new file mode 100644 index 00000000000..1d0568ce74b --- /dev/null +++ b/apps/sim/tools/grafana/types.ts @@ -0,0 +1,451 @@ +// Common types for Grafana API tools +import type { ToolResponse } from '@/tools/types' + +// Common parameters for all Grafana tools +export interface GrafanaBaseParams { + apiKey: string + baseUrl: string + organizationId?: string +} + +// Health Check types +export interface GrafanaHealthCheckParams extends GrafanaBaseParams {} + +export interface GrafanaHealthCheckResponse extends ToolResponse { + output: { + commit: string + database: string + version: string + } +} + +export interface GrafanaDataSourceHealthParams extends GrafanaBaseParams { + dataSourceId: string +} + +export interface GrafanaDataSourceHealthResponse extends ToolResponse { + output: { + status: string + message: string + } +} + +// Dashboard types +export interface GrafanaGetDashboardParams extends GrafanaBaseParams { + dashboardUid: string +} + +export interface GrafanaDashboardMeta { + type: string + canSave: boolean + canEdit: boolean + canAdmin: boolean + canStar: boolean + canDelete: boolean + slug: string + url: string + expires: string + created: string + updated: string + updatedBy: string + createdBy: string + version: number + hasAcl: boolean + isFolder: boolean + folderId: number + folderUid: string + folderTitle: string + folderUrl: string + provisioned: boolean + provisionedExternalId: string +} + +export interface GrafanaDashboard { + id: number + uid: string + title: string + tags: string[] + timezone: string + schemaVersion: number + version: number + refresh: string + panels: any[] + templating: any + annotations: any + time: { + from: string + to: string + } +} + +export interface GrafanaGetDashboardResponse extends ToolResponse { + output: { + dashboard: GrafanaDashboard + meta: GrafanaDashboardMeta + } +} + +export interface GrafanaListDashboardsParams extends GrafanaBaseParams { + query?: string + tag?: string + folderIds?: string + starred?: boolean + limit?: number +} + +export interface GrafanaDashboardSearchResult { + id: number + uid: string + title: string + uri: string + url: string + slug: string + type: string + tags: string[] + isStarred: boolean + folderId: number + folderUid: string + folderTitle: string + folderUrl: string + sortMeta: number +} + +export interface GrafanaListDashboardsResponse extends ToolResponse { + output: { + dashboards: GrafanaDashboardSearchResult[] + } +} + +export interface GrafanaCreateDashboardParams extends GrafanaBaseParams { + title: string + folderUid?: string + tags?: string + timezone?: string + refresh?: string + panels?: string // JSON string of panels array + overwrite?: boolean + message?: string +} + +export interface GrafanaCreateDashboardResponse extends ToolResponse { + output: { + id: number + uid: string + url: string + status: string + version: number + slug: string + } +} + +export interface GrafanaUpdateDashboardParams extends GrafanaBaseParams { + dashboardUid: string + title?: string + folderUid?: string + tags?: string + timezone?: string + refresh?: string + panels?: string // JSON string of panels array + overwrite?: boolean + message?: string +} + +export interface GrafanaUpdateDashboardResponse extends ToolResponse { + output: { + id: number + uid: string + url: string + status: string + version: number + slug: string + } +} + +export interface GrafanaDeleteDashboardParams extends GrafanaBaseParams { + dashboardUid: string +} + +export interface GrafanaDeleteDashboardResponse extends ToolResponse { + output: { + title: string + message: string + id: number + } +} + +// Alert Rule types +export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {} + +export interface GrafanaAlertRule { + uid: string + title: string + condition: string + data: any[] + updated: string + noDataState: string + execErrState: string + for: string + annotations: Record + labels: Record + isPaused: boolean + folderUID: string + ruleGroup: string + orgId: number + namespace_uid: string + namespace_id: number + provenance: string +} + +export interface GrafanaListAlertRulesResponse extends ToolResponse { + output: { + rules: GrafanaAlertRule[] + } +} + +export interface GrafanaGetAlertRuleParams extends GrafanaBaseParams { + alertRuleUid: string +} + +export interface GrafanaGetAlertRuleResponse extends ToolResponse { + output: GrafanaAlertRule +} + +export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams { + title: string + folderUid: string + ruleGroup: string + condition: string + data: string // JSON string of data array + forDuration?: string + noDataState?: string + execErrState?: string + annotations?: string // JSON string + labels?: string // JSON string +} + +export interface GrafanaCreateAlertRuleResponse extends ToolResponse { + output: GrafanaAlertRule +} + +export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams { + alertRuleUid: string + title?: string + folderUid?: string + ruleGroup?: string + condition?: string + data?: string // JSON string of data array + forDuration?: string + noDataState?: string + execErrState?: string + annotations?: string // JSON string + labels?: string // JSON string +} + +export interface GrafanaUpdateAlertRuleResponse extends ToolResponse { + output: GrafanaAlertRule +} + +export interface GrafanaDeleteAlertRuleParams extends GrafanaBaseParams { + alertRuleUid: string +} + +export interface GrafanaDeleteAlertRuleResponse extends ToolResponse { + output: { + message: string + } +} + +// Annotation types +export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams { + text: string + tags?: string // comma-separated + dashboardUid?: string + panelId?: number + time?: number // epoch ms + timeEnd?: number // epoch ms +} + +export interface GrafanaAnnotation { + id: number + alertId: number + alertName: string + dashboardId: number + dashboardUID: string + panelId: number + userId: number + newState: string + prevState: string + created: number + updated: number + time: number + timeEnd: number + text: string + tags: string[] + login: string + email: string + avatarUrl: string + data: any +} + +export interface GrafanaCreateAnnotationResponse extends ToolResponse { + output: { + id: number + message: string + } +} + +export interface GrafanaListAnnotationsParams extends GrafanaBaseParams { + from?: number + to?: number + dashboardUid?: string + panelId?: number + tags?: string // comma-separated + type?: string + limit?: number +} + +export interface GrafanaListAnnotationsResponse extends ToolResponse { + output: { + annotations: GrafanaAnnotation[] + } +} + +export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams { + annotationId: number + text: string + tags?: string // comma-separated + time?: number + timeEnd?: number +} + +export interface GrafanaUpdateAnnotationResponse extends ToolResponse { + output: { + id: number + message: string + } +} + +export interface GrafanaDeleteAnnotationParams extends GrafanaBaseParams { + annotationId: number +} + +export interface GrafanaDeleteAnnotationResponse extends ToolResponse { + output: { + message: string + } +} + +// Data Source types +export interface GrafanaListDataSourcesParams extends GrafanaBaseParams {} + +export interface GrafanaDataSource { + id: number + uid: string + orgId: number + name: string + type: string + typeName: string + typeLogoUrl: string + access: string + url: string + user: string + database: string + basicAuth: boolean + isDefault: boolean + jsonData: any + readOnly: boolean +} + +export interface GrafanaListDataSourcesResponse extends ToolResponse { + output: { + dataSources: GrafanaDataSource[] + } +} + +export interface GrafanaGetDataSourceParams extends GrafanaBaseParams { + dataSourceId: string +} + +export interface GrafanaGetDataSourceResponse extends ToolResponse { + output: GrafanaDataSource +} + +// Folder types +export interface GrafanaListFoldersParams extends GrafanaBaseParams { + limit?: number + page?: number +} + +export interface GrafanaFolder { + id: number + uid: string + title: string + url: string + hasAcl: boolean + canSave: boolean + canEdit: boolean + canAdmin: boolean + canDelete: boolean + createdBy: string + created: string + updatedBy: string + updated: string + version: number +} + +export interface GrafanaListFoldersResponse extends ToolResponse { + output: { + folders: GrafanaFolder[] + } +} + +export interface GrafanaCreateFolderParams extends GrafanaBaseParams { + title: string + uid?: string +} + +export interface GrafanaCreateFolderResponse extends ToolResponse { + output: GrafanaFolder +} + +// Contact Points types +export interface GrafanaListContactPointsParams extends GrafanaBaseParams {} + +export interface GrafanaContactPoint { + uid: string + name: string + type: string + settings: Record + disableResolveMessage: boolean + provenance: string +} + +export interface GrafanaListContactPointsResponse extends ToolResponse { + output: { + contactPoints: GrafanaContactPoint[] + } +} + +// Union type for all Grafana responses +export type GrafanaResponse = + | GrafanaHealthCheckResponse + | GrafanaDataSourceHealthResponse + | GrafanaGetDashboardResponse + | GrafanaListDashboardsResponse + | GrafanaCreateDashboardResponse + | GrafanaUpdateDashboardResponse + | GrafanaDeleteDashboardResponse + | GrafanaListAlertRulesResponse + | GrafanaGetAlertRuleResponse + | GrafanaCreateAlertRuleResponse + | GrafanaUpdateAlertRuleResponse + | GrafanaDeleteAlertRuleResponse + | GrafanaCreateAnnotationResponse + | GrafanaListAnnotationsResponse + | GrafanaUpdateAnnotationResponse + | GrafanaDeleteAnnotationResponse + | GrafanaListDataSourcesResponse + | GrafanaGetDataSourceResponse + | GrafanaListFoldersResponse + | GrafanaCreateFolderResponse + | GrafanaListContactPointsResponse diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 7624c338543..ff9d4e1b2e3 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -474,9 +474,21 @@ async function handleInternalRequest( if (toolId.startsWith('custom_') && tool.request.body) { const requestBody = tool.request.body(params) - if (requestBody.schema && requestBody.params) { + if ( + typeof requestBody === 'object' && + requestBody !== null && + 'schema' in requestBody && + 'params' in requestBody + ) { try { - validateClientSideParams(requestBody.params, requestBody.schema) + validateClientSideParams( + requestBody.params as Record, + requestBody.schema as { + type: string + properties: Record + required?: string[] + } + ) } catch (validationError) { logger.error(`[${requestId}] Custom tool validation failed for ${toolId}:`, { error: diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d9b94cd2c7e..0658f845538 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1,3 +1,13 @@ +import { + ahrefsBacklinksStatsTool, + ahrefsBacklinksTool, + ahrefsBrokenBacklinksTool, + ahrefsDomainRatingTool, + ahrefsKeywordOverviewTool, + ahrefsOrganicKeywordsTool, + ahrefsReferringDomainsTool, + ahrefsTopPagesTool, +} from '@/tools/ahrefs' import { airtableCreateRecordsTool, airtableGetRecordTool, @@ -41,6 +51,20 @@ import { asanaSearchTasksTool, asanaUpdateTaskTool, } from '@/tools/asana' +import { + boxCopyFileTool, + boxCreateCollaborationTool, + boxCreateFolderTool, + boxCreateSharedLinkTool, + boxDeleteFileTool, + boxDeleteFolderTool, + boxDownloadFileTool, + boxGetCurrentUserTool, + boxGetFileTool, + boxListFolderTool, + boxSearchTool, + boxUpdateFileTool, +} from '@/tools/box' import { browserUseRunTaskTool } from '@/tools/browser_use' import { calendlyCancelEventTool, @@ -71,6 +95,20 @@ import { confluenceUpdateCommentTool, confluenceUpdateTool, } from '@/tools/confluence' +import { + datadogCancelDowntimeTool, + datadogCreateDowntimeTool, + datadogCreateEventTool, + datadogCreateMonitorTool, + datadogGetMonitorTool, + datadogListDowntimesTool, + datadogListMonitorsTool, + datadogMuteMonitorTool, + datadogQueryLogsTool, + datadogQueryTimeseriesTool, + datadogSendLogsTool, + datadogSubmitMetricsTool, +} from '@/tools/datadog' import { discordAddReactionTool, discordArchiveThreadTool, @@ -108,6 +146,18 @@ import { discordUpdateMemberTool, discordUpdateRoleTool, } from '@/tools/discord' +import { + dropboxCopyTool, + dropboxCreateFolderTool, + dropboxCreateSharedLinkTool, + dropboxDeleteTool, + dropboxDownloadTool, + dropboxGetMetadataTool, + dropboxListFolderTool, + dropboxMoveTool, + dropboxSearchTool, + dropboxUploadTool, +} from '@/tools/dropbox' import { deleteTool as dynamodbDeleteTool, getTool as dynamodbGetTool, @@ -116,6 +166,20 @@ import { scanTool as dynamodbScanTool, updateTool as dynamodbUpdateTool, } from '@/tools/dynamodb' +import { + elasticsearchBulkTool, + elasticsearchClusterHealthTool, + elasticsearchClusterStatsTool, + elasticsearchCountTool, + elasticsearchCreateIndexTool, + elasticsearchDeleteDocumentTool, + elasticsearchDeleteIndexTool, + elasticsearchGetDocumentTool, + elasticsearchGetIndexTool, + elasticsearchIndexDocumentTool, + elasticsearchSearchTool, + elasticsearchUpdateDocumentTool, +} from '@/tools/elasticsearch' import { elevenLabsTtsTool } from '@/tools/elevenlabs' import { exaAnswerTool, @@ -187,6 +251,27 @@ import { githubUpdateProjectTool, githubUpdateReleaseTool, } from '@/tools/github' +import { + gitlabCancelPipelineTool, + gitlabCreateIssueNoteTool, + gitlabCreateIssueTool, + gitlabCreateMergeRequestNoteTool, + gitlabCreateMergeRequestTool, + gitlabCreatePipelineTool, + gitlabDeleteIssueTool, + gitlabGetIssueTool, + gitlabGetMergeRequestTool, + gitlabGetPipelineTool, + gitlabGetProjectTool, + gitlabListIssuesTool, + gitlabListMergeRequestsTool, + gitlabListPipelinesTool, + gitlabListProjectsTool, + gitlabMergeMergeRequestTool, + gitlabRetryPipelineTool, + gitlabUpdateIssueTool, + gitlabUpdateMergeRequestTool, +} from '@/tools/gitlab' import { gmailAddLabelTool, gmailArchiveTool, @@ -233,6 +318,29 @@ import { listMattersHoldsTool, listMattersTool, } from '@/tools/google_vault' +import { + grafanaCreateAlertRuleTool, + grafanaCreateAnnotationTool, + grafanaCreateDashboardTool, + grafanaCreateFolderTool, + grafanaDataSourceHealthTool, + grafanaDeleteAlertRuleTool, + grafanaDeleteAnnotationTool, + grafanaDeleteDashboardTool, + grafanaGetAlertRuleTool, + grafanaGetDashboardTool, + grafanaGetDataSourceTool, + grafanaHealthCheckTool, + grafanaListAlertRulesTool, + grafanaListAnnotationsTool, + grafanaListContactPointsTool, + grafanaListDashboardsTool, + grafanaListDataSourcesTool, + grafanaListFoldersTool, + grafanaUpdateAlertRuleTool, + grafanaUpdateAnnotationTool, + grafanaUpdateDashboardTool, +} from '@/tools/grafana' import { guardrailsValidateTool } from '@/tools/guardrails' import { requestTool as httpRequest } from '@/tools/http' import { @@ -833,6 +941,24 @@ import { sharepointUpdateListItemTool, sharepointUploadFileTool, } from '@/tools/sharepoint' +import { + shopifyAdjustInventoryTool, + shopifyCancelOrderTool, + shopifyCreateCustomerTool, + shopifyCreateProductTool, + shopifyDeleteCustomerTool, + shopifyDeleteProductTool, + shopifyGetCustomerTool, + shopifyGetInventoryLevelTool, + shopifyGetOrderTool, + shopifyGetProductTool, + shopifyListCustomersTool, + shopifyListOrdersTool, + shopifyListProductsTool, + shopifyUpdateCustomerTool, + shopifyUpdateOrderTool, + shopifyUpdateProductTool, +} from '@/tools/shopify' import { slackAddReactionTool, slackCanvasTool, @@ -844,6 +970,21 @@ import { } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' import { smtpSendMailTool } from '@/tools/smtp' +import { + checkCommandExistsTool as sshCheckCommandExistsTool, + checkFileExistsTool as sshCheckFileExistsTool, + createDirectoryTool as sshCreateDirectoryTool, + deleteFileTool as sshDeleteFileTool, + downloadFileTool as sshDownloadFileTool, + executeCommandTool as sshExecuteCommandTool, + executeScriptTool as sshExecuteScriptTool, + getSystemInfoTool as sshGetSystemInfoTool, + listDirectoryTool as sshListDirectoryTool, + moveRenameTool as sshMoveRenameTool, + readFileContentTool as sshReadFileContentTool, + uploadFileTool as sshUploadFileTool, + writeFileContentTool as sshWriteFileContentTool, +} from '@/tools/ssh' import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand' import { stripeCancelPaymentIntentTool, @@ -998,6 +1139,34 @@ import { wikipediaRandomPageTool, wikipediaSearchTool, } from '@/tools/wikipedia' +import { + wordpressCreateCategoryTool, + wordpressCreateCommentTool, + wordpressCreatePageTool, + wordpressCreatePostTool, + wordpressCreateTagTool, + wordpressDeleteCommentTool, + wordpressDeleteMediaTool, + wordpressDeletePageTool, + wordpressDeletePostTool, + wordpressGetCurrentUserTool, + wordpressGetMediaTool, + wordpressGetPageTool, + wordpressGetPostTool, + wordpressGetUserTool, + wordpressListCategoriesTool, + wordpressListCommentsTool, + wordpressListMediaTool, + wordpressListPagesTool, + wordpressListPostsTool, + wordpressListTagsTool, + wordpressListUsersTool, + wordpressSearchContentTool, + wordpressUpdateCommentTool, + wordpressUpdatePageTool, + wordpressUpdatePostTool, + wordpressUploadMediaTool, +} from '@/tools/wordpress' import { workflowExecutorTool } from '@/tools/workflow' import { xReadTool, xSearchTool, xUserTool, xWriteTool } from '@/tools/x' import { @@ -1049,6 +1218,14 @@ import { zepGetUserThreadsTool, zepGetUserTool, } from '@/tools/zep' +import { + zoomCreateMeetingTool, + zoomDeleteMeetingTool, + zoomGetMeetingInvitationTool, + zoomGetMeetingTool, + zoomListMeetingsTool, + zoomUpdateMeetingTool, +} from '@/tools/zoom' // Registry of all available tools export const tools: Record = { @@ -1062,6 +1239,18 @@ export const tools: Record = { asana_search_tasks: asanaSearchTasksTool, asana_add_comment: asanaAddCommentTool, browser_use_run_task: browserUseRunTaskTool, + box_get_file: boxGetFileTool, + box_list_folder: boxListFolderTool, + box_create_folder: boxCreateFolderTool, + box_delete_file: boxDeleteFileTool, + box_delete_folder: boxDeleteFolderTool, + box_copy_file: boxCopyFileTool, + box_update_file: boxUpdateFileTool, + box_search: boxSearchTool, + box_download_file: boxDownloadFileTool, + box_get_current_user: boxGetCurrentUserTool, + box_create_shared_link: boxCreateSharedLinkTool, + box_create_collaboration: boxCreateCollaborationTool, openai_embeddings: openAIEmbeddings, http_request: httpRequest, huggingface_chat: huggingfaceChatTool, @@ -1073,6 +1262,27 @@ export const tools: Record = { firecrawl_crawl: crawlTool, firecrawl_map: mapTool, firecrawl_extract: extractTool, + grafana_health_check: grafanaHealthCheckTool, + grafana_data_source_health: grafanaDataSourceHealthTool, + grafana_get_dashboard: grafanaGetDashboardTool, + grafana_list_dashboards: grafanaListDashboardsTool, + grafana_create_dashboard: grafanaCreateDashboardTool, + grafana_update_dashboard: grafanaUpdateDashboardTool, + grafana_delete_dashboard: grafanaDeleteDashboardTool, + grafana_list_alert_rules: grafanaListAlertRulesTool, + grafana_get_alert_rule: grafanaGetAlertRuleTool, + grafana_create_alert_rule: grafanaCreateAlertRuleTool, + grafana_update_alert_rule: grafanaUpdateAlertRuleTool, + grafana_delete_alert_rule: grafanaDeleteAlertRuleTool, + grafana_list_contact_points: grafanaListContactPointsTool, + grafana_create_annotation: grafanaCreateAnnotationTool, + grafana_list_annotations: grafanaListAnnotationsTool, + grafana_update_annotation: grafanaUpdateAnnotationTool, + grafana_delete_annotation: grafanaDeleteAnnotationTool, + grafana_list_data_sources: grafanaListDataSourcesTool, + grafana_get_data_source: grafanaGetDataSourceTool, + grafana_list_folders: grafanaListFoldersTool, + grafana_create_folder: grafanaCreateFolderTool, google_search: googleSearchTool, guardrails_validate: guardrailsValidateTool, jina_read_url: readUrlTool, @@ -1098,6 +1308,19 @@ export const tools: Record = { sendgrid_delete_template: sendGridDeleteTemplateTool, sendgrid_create_template_version: sendGridCreateTemplateVersionTool, smtp_send_mail: smtpSendMailTool, + ssh_execute_command: sshExecuteCommandTool, + ssh_execute_script: sshExecuteScriptTool, + ssh_check_command_exists: sshCheckCommandExistsTool, + ssh_upload_file: sshUploadFileTool, + ssh_download_file: sshDownloadFileTool, + ssh_list_directory: sshListDirectoryTool, + ssh_check_file_exists: sshCheckFileExistsTool, + ssh_create_directory: sshCreateDirectoryTool, + ssh_delete_file: sshDeleteFileTool, + ssh_move_rename: sshMoveRenameTool, + ssh_get_system_info: sshGetSystemInfoTool, + ssh_read_file_content: sshReadFileContentTool, + ssh_write_file_content: sshWriteFileContentTool, mailgun_send_message: mailgunSendMessageTool, mailgun_get_message: mailgunGetMessageTool, mailgun_list_messages: mailgunListMessagesTool, @@ -1253,6 +1476,16 @@ export const tools: Record = { dynamodb_scan: dynamodbScanTool, dynamodb_update: dynamodbUpdateTool, dynamodb_delete: dynamodbDeleteTool, + dropbox_upload: dropboxUploadTool, + dropbox_download: dropboxDownloadTool, + dropbox_list_folder: dropboxListFolderTool, + dropbox_create_folder: dropboxCreateFolderTool, + dropbox_delete: dropboxDeleteTool, + dropbox_copy: dropboxCopyTool, + dropbox_move: dropboxMoveTool, + dropbox_get_metadata: dropboxGetMetadataTool, + dropbox_create_shared_link: dropboxCreateSharedLinkTool, + dropbox_search: dropboxSearchTool, mongodb_query: mongodbQueryTool, mongodb_insert: mongodbInsertTool, mongodb_update: mongodbUpdateTool, @@ -1319,6 +1552,37 @@ export const tools: Record = { github_create_project: githubCreateProjectTool, github_update_project: githubUpdateProjectTool, github_delete_project: githubDeleteProjectTool, + gitlab_list_projects: gitlabListProjectsTool, + gitlab_get_project: gitlabGetProjectTool, + gitlab_list_issues: gitlabListIssuesTool, + gitlab_get_issue: gitlabGetIssueTool, + gitlab_create_issue: gitlabCreateIssueTool, + gitlab_update_issue: gitlabUpdateIssueTool, + gitlab_delete_issue: gitlabDeleteIssueTool, + gitlab_create_issue_note: gitlabCreateIssueNoteTool, + gitlab_list_merge_requests: gitlabListMergeRequestsTool, + gitlab_get_merge_request: gitlabGetMergeRequestTool, + gitlab_create_merge_request: gitlabCreateMergeRequestTool, + gitlab_update_merge_request: gitlabUpdateMergeRequestTool, + gitlab_merge_merge_request: gitlabMergeMergeRequestTool, + gitlab_create_merge_request_note: gitlabCreateMergeRequestNoteTool, + gitlab_list_pipelines: gitlabListPipelinesTool, + gitlab_get_pipeline: gitlabGetPipelineTool, + gitlab_create_pipeline: gitlabCreatePipelineTool, + gitlab_retry_pipeline: gitlabRetryPipelineTool, + gitlab_cancel_pipeline: gitlabCancelPipelineTool, + elasticsearch_search: elasticsearchSearchTool, + elasticsearch_index_document: elasticsearchIndexDocumentTool, + elasticsearch_get_document: elasticsearchGetDocumentTool, + elasticsearch_update_document: elasticsearchUpdateDocumentTool, + elasticsearch_delete_document: elasticsearchDeleteDocumentTool, + elasticsearch_bulk: elasticsearchBulkTool, + elasticsearch_count: elasticsearchCountTool, + elasticsearch_create_index: elasticsearchCreateIndexTool, + elasticsearch_delete_index: elasticsearchDeleteIndexTool, + elasticsearch_get_index: elasticsearchGetIndexTool, + elasticsearch_cluster_health: elasticsearchClusterHealthTool, + elasticsearch_cluster_stats: elasticsearchClusterStatsTool, exa_search: exaSearchTool, exa_get_contents: exaGetContentsTool, exa_find_similar_links: exaFindSimilarLinksTool, @@ -1438,6 +1702,14 @@ export const tools: Record = { airtable_get_record: airtableGetRecordTool, airtable_list_records: airtableListRecordsTool, airtable_update_record: airtableUpdateRecordTool, + ahrefs_domain_rating: ahrefsDomainRatingTool, + ahrefs_backlinks: ahrefsBacklinksTool, + ahrefs_backlinks_stats: ahrefsBacklinksStatsTool, + ahrefs_referring_domains: ahrefsReferringDomainsTool, + ahrefs_organic_keywords: ahrefsOrganicKeywordsTool, + ahrefs_top_pages: ahrefsTopPagesTool, + ahrefs_keyword_overview: ahrefsKeywordOverviewTool, + ahrefs_broken_backlinks: ahrefsBrokenBacklinksTool, apify_run_actor_sync: apifyRunActorSyncTool, apify_run_actor_async: apifyRunActorAsyncTool, apollo_people_search: apolloPeopleSearchTool, @@ -1555,6 +1827,18 @@ export const tools: Record = { discord_execute_webhook: discordExecuteWebhookTool, discord_get_webhook: discordGetWebhookTool, discord_delete_webhook: discordDeleteWebhookTool, + datadog_submit_metrics: datadogSubmitMetricsTool, + datadog_query_timeseries: datadogQueryTimeseriesTool, + datadog_create_event: datadogCreateEventTool, + datadog_create_monitor: datadogCreateMonitorTool, + datadog_get_monitor: datadogGetMonitorTool, + datadog_list_monitors: datadogListMonitorsTool, + datadog_mute_monitor: datadogMuteMonitorTool, + datadog_query_logs: datadogQueryLogsTool, + datadog_send_logs: datadogSendLogsTool, + datadog_create_downtime: datadogCreateDowntimeTool, + datadog_list_downtimes: datadogListDowntimesTool, + datadog_cancel_downtime: datadogCancelDowntimeTool, openai_image: imageTool, microsoft_teams_read_chat: microsoftTeamsReadChatTool, microsoft_teams_write_chat: microsoftTeamsWriteChatTool, @@ -1658,6 +1942,22 @@ export const tools: Record = { linear_update_project_status: linearUpdateProjectStatusTool, linear_delete_project_status: linearDeleteProjectStatusTool, linear_list_project_statuses: linearListProjectStatusesTool, + shopify_create_product: shopifyCreateProductTool, + shopify_get_product: shopifyGetProductTool, + shopify_list_products: shopifyListProductsTool, + shopify_update_product: shopifyUpdateProductTool, + shopify_delete_product: shopifyDeleteProductTool, + shopify_get_order: shopifyGetOrderTool, + shopify_list_orders: shopifyListOrdersTool, + shopify_update_order: shopifyUpdateOrderTool, + shopify_cancel_order: shopifyCancelOrderTool, + shopify_create_customer: shopifyCreateCustomerTool, + shopify_get_customer: shopifyGetCustomerTool, + shopify_list_customers: shopifyListCustomersTool, + shopify_update_customer: shopifyUpdateCustomerTool, + shopify_delete_customer: shopifyDeleteCustomerTool, + shopify_get_inventory_level: shopifyGetInventoryLevelTool, + shopify_adjust_inventory: shopifyAdjustInventoryTool, onedrive_create_folder: onedriveCreateFolderTool, onedrive_delete: onedriveDeleteTool, onedrive_download: onedriveDownloadTool, @@ -1702,6 +2002,32 @@ export const tools: Record = { wikipedia_search: wikipediaSearchTool, wikipedia_content: wikipediaPageContentTool, wikipedia_random: wikipediaRandomPageTool, + wordpress_create_post: wordpressCreatePostTool, + wordpress_update_post: wordpressUpdatePostTool, + wordpress_delete_post: wordpressDeletePostTool, + wordpress_get_post: wordpressGetPostTool, + wordpress_list_posts: wordpressListPostsTool, + wordpress_create_page: wordpressCreatePageTool, + wordpress_update_page: wordpressUpdatePageTool, + wordpress_delete_page: wordpressDeletePageTool, + wordpress_get_page: wordpressGetPageTool, + wordpress_list_pages: wordpressListPagesTool, + wordpress_upload_media: wordpressUploadMediaTool, + wordpress_get_media: wordpressGetMediaTool, + wordpress_list_media: wordpressListMediaTool, + wordpress_delete_media: wordpressDeleteMediaTool, + wordpress_create_comment: wordpressCreateCommentTool, + wordpress_list_comments: wordpressListCommentsTool, + wordpress_update_comment: wordpressUpdateCommentTool, + wordpress_delete_comment: wordpressDeleteCommentTool, + wordpress_create_category: wordpressCreateCategoryTool, + wordpress_list_categories: wordpressListCategoriesTool, + wordpress_create_tag: wordpressCreateTagTool, + wordpress_list_tags: wordpressListTagsTool, + wordpress_get_current_user: wordpressGetCurrentUserTool, + wordpress_list_users: wordpressListUsersTool, + wordpress_get_user: wordpressGetUserTool, + wordpress_search_content: wordpressSearchContentTool, google_vault_create_matters_export: createMattersExportTool, google_vault_list_matters_export: listMattersExportTool, google_vault_create_matters_holds: createMattersHoldsTool, @@ -2009,4 +2335,10 @@ export const tools: Record = { sentry_releases_list: listReleasesTool, sentry_releases_create: createReleaseTool, sentry_releases_deploy: createDeployTool, + zoom_create_meeting: zoomCreateMeetingTool, + zoom_list_meetings: zoomListMeetingsTool, + zoom_get_meeting: zoomGetMeetingTool, + zoom_update_meeting: zoomUpdateMeetingTool, + zoom_delete_meeting: zoomDeleteMeetingTool, + zoom_get_meeting_invitation: zoomGetMeetingInvitationTool, } diff --git a/apps/sim/tools/shopify/adjust_inventory.ts b/apps/sim/tools/shopify/adjust_inventory.ts new file mode 100644 index 00000000000..27b40c7b662 --- /dev/null +++ b/apps/sim/tools/shopify/adjust_inventory.ts @@ -0,0 +1,159 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyAdjustInventoryParams, ShopifyInventoryResponse } from './types' + +export const shopifyAdjustInventoryTool: ToolConfig< + ShopifyAdjustInventoryParams, + ShopifyInventoryResponse +> = { + id: 'shopify_adjust_inventory', + name: 'Shopify Adjust Inventory', + description: 'Adjust inventory quantity for a product variant at a specific location', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + inventoryItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inventory item ID (gid://shopify/InventoryItem/123456789)', + }, + locationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Location ID (gid://shopify/Location/123456789)', + }, + delta: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Amount to adjust (positive to increase, negative to decrease)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.inventoryItemId) { + throw new Error('Inventory item ID is required') + } + if (!params.locationId) { + throw new Error('Location ID is required') + } + if (params.delta === undefined || params.delta === null) { + throw new Error('Delta is required') + } + + return { + query: ` + mutation inventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) { + inventoryAdjustQuantities(input: $input) { + inventoryAdjustmentGroup { + createdAt + reason + changes { + name + delta + quantityAfterChange + item { + id + sku + } + location { + id + name + } + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input: { + reason: 'correction', + name: 'available', + changes: [ + { + inventoryItemId: params.inventoryItemId, + locationId: params.locationId, + delta: params.delta, + }, + ], + }, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to adjust inventory', + output: {}, + } + } + + const result = data.data?.inventoryAdjustQuantities + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const adjustmentGroup = result?.inventoryAdjustmentGroup + if (!adjustmentGroup) { + return { + success: false, + error: 'Inventory adjustment was not successful', + output: {}, + } + } + + return { + success: true, + output: { + inventoryLevel: { + adjustmentGroup, + changes: adjustmentGroup.changes, + }, + }, + } + }, + + outputs: { + inventoryLevel: { + type: 'object', + description: 'The inventory adjustment result', + }, + }, +} diff --git a/apps/sim/tools/shopify/cancel_order.ts b/apps/sim/tools/shopify/cancel_order.ts new file mode 100644 index 00000000000..b83156d8fb3 --- /dev/null +++ b/apps/sim/tools/shopify/cancel_order.ts @@ -0,0 +1,146 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCancelOrderParams, ShopifyOrderResponse } from './types' + +export const shopifyCancelOrderTool: ToolConfig = { + id: 'shopify_cancel_order', + name: 'Shopify Cancel Order', + description: 'Cancel an order in your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + orderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Order ID to cancel (gid://shopify/Order/123456789)', + }, + reason: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cancellation reason (CUSTOMER, FRAUD, INVENTORY, DECLINED, OTHER)', + }, + notifyCustomer: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to notify the customer about the cancellation', + }, + refund: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to refund the order', + }, + restock: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to restock the inventory', + }, + staffNote: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'A note about the cancellation for staff reference', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.orderId) { + throw new Error('Order ID is required to cancel an order') + } + if (!params.reason) { + throw new Error('Cancellation reason is required') + } + + return { + query: ` + mutation orderCancel($orderId: ID!, $reason: OrderCancelReason!, $notifyCustomer: Boolean, $refund: Boolean, $restock: Boolean, $staffNote: String) { + orderCancel(orderId: $orderId, reason: $reason, notifyCustomer: $notifyCustomer, refund: $refund, restock: $restock, staffNote: $staffNote) { + job { + id + done + } + orderCancelUserErrors { + field + message + code + } + } + } + `, + variables: { + orderId: params.orderId, + reason: params.reason, + notifyCustomer: params.notifyCustomer ?? false, + refund: params.refund ?? false, + restock: params.restock ?? false, + staffNote: params.staffNote, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to cancel order', + output: {}, + } + } + + const result = data.data?.orderCancel + if (result?.orderCancelUserErrors?.length > 0) { + return { + success: false, + error: result.orderCancelUserErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + return { + success: true, + output: { + order: { + id: result?.job?.id, + cancelled: result?.job?.done ?? true, + message: 'Order cancellation initiated', + }, + }, + } + }, + + outputs: { + order: { + type: 'object', + description: 'The cancellation result', + }, + }, +} diff --git a/apps/sim/tools/shopify/create_customer.ts b/apps/sim/tools/shopify/create_customer.ts new file mode 100644 index 00000000000..a3e94db9169 --- /dev/null +++ b/apps/sim/tools/shopify/create_customer.ts @@ -0,0 +1,209 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCreateCustomerParams, ShopifyCustomerResponse } from './types' + +export const shopifyCreateCustomerTool: ToolConfig< + ShopifyCreateCustomerParams, + ShopifyCustomerResponse +> = { + id: 'shopify_create_customer', + name: 'Shopify Create Customer', + description: 'Create a new customer in your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer email address', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer first name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer last name', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer phone number', + }, + note: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Note about the customer', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Customer tags', + }, + addresses: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Customer addresses', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + // Shopify requires at least one of: email, phone, firstName, or lastName + const hasEmail = params.email?.trim() + const hasPhone = params.phone?.trim() + const hasFirstName = params.firstName?.trim() + const hasLastName = params.lastName?.trim() + + if (!hasEmail && !hasPhone && !hasFirstName && !hasLastName) { + throw new Error('Customer must have at least one of: email, phone, firstName, or lastName') + } + + const input: Record = {} + + if (hasEmail) { + input.email = params.email + } + if (hasFirstName) { + input.firstName = params.firstName + } + if (hasLastName) { + input.lastName = params.lastName + } + if (hasPhone) { + input.phone = params.phone + } + if (params.note) { + input.note = params.note + } + if (params.tags && Array.isArray(params.tags)) { + input.tags = params.tags + } + if (params.addresses && Array.isArray(params.addresses)) { + input.addresses = params.addresses + } + + return { + query: ` + mutation customerCreate($input: CustomerInput!) { + customerCreate(input: $input) { + customer { + id + email + firstName + lastName + phone + createdAt + updatedAt + note + tags + ordersCount + totalSpentV2 { + amount + currencyCode + } + addresses { + address1 + address2 + city + province + country + zip + phone + } + defaultAddress { + address1 + city + province + country + zip + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create customer', + output: {}, + } + } + + const result = data.data?.customerCreate + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const customer = result?.customer + if (!customer) { + return { + success: false, + error: 'Customer creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + customer, + }, + } + }, + + outputs: { + customer: { + type: 'object', + description: 'The created customer', + }, + }, +} diff --git a/apps/sim/tools/shopify/create_product.ts b/apps/sim/tools/shopify/create_product.ts new file mode 100644 index 00000000000..c39e1b570c8 --- /dev/null +++ b/apps/sim/tools/shopify/create_product.ts @@ -0,0 +1,208 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCreateProductParams, ShopifyProductResponse } from './types' + +export const shopifyCreateProductTool: ToolConfig< + ShopifyCreateProductParams, + ShopifyProductResponse +> = { + id: 'shopify_create_product', + name: 'Shopify Create Product', + description: 'Create a new product in your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product title', + }, + descriptionHtml: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product description (HTML)', + }, + vendor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product vendor/brand', + }, + productType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product type/category', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Product tags', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product status (ACTIVE, DRAFT, ARCHIVED)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.title || !params.title.trim()) { + throw new Error('Title is required to create a Shopify product') + } + + const input: Record = { + title: params.title, + } + + if (params.descriptionHtml) { + input.descriptionHtml = params.descriptionHtml + } + if (params.vendor) { + input.vendor = params.vendor + } + if (params.productType) { + input.productType = params.productType + } + if (params.tags && Array.isArray(params.tags)) { + input.tags = params.tags + } + if (params.status) { + input.status = params.status + } + + return { + query: ` + mutation productCreate($input: ProductInput!) { + productCreate(input: $input) { + product { + id + title + handle + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + variants(first: 10) { + edges { + node { + id + title + price + compareAtPrice + sku + barcode + inventoryQuantity + } + } + } + images(first: 10) { + edges { + node { + id + url + altText + } + } + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create product', + output: {}, + } + } + + const result = data.data?.productCreate + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const product = result?.product + if (!product) { + return { + success: false, + error: 'Product creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + product: { + id: product.id, + title: product.title, + handle: product.handle, + descriptionHtml: product.descriptionHtml, + vendor: product.vendor, + productType: product.productType, + tags: product.tags, + status: product.status, + createdAt: product.createdAt, + updatedAt: product.updatedAt, + variants: product.variants, + images: product.images, + }, + }, + } + }, + + outputs: { + product: { + type: 'object', + description: 'The created product', + }, + }, +} diff --git a/apps/sim/tools/shopify/delete_customer.ts b/apps/sim/tools/shopify/delete_customer.ts new file mode 100644 index 00000000000..657bd37eeef --- /dev/null +++ b/apps/sim/tools/shopify/delete_customer.ts @@ -0,0 +1,113 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyDeleteCustomerParams, ShopifyDeleteResponse } from './types' + +export const shopifyDeleteCustomerTool: ToolConfig< + ShopifyDeleteCustomerParams, + ShopifyDeleteResponse +> = { + id: 'shopify_delete_customer', + name: 'Shopify Delete Customer', + description: 'Delete a customer from your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID to delete (gid://shopify/Customer/123456789)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.customerId) { + throw new Error('Customer ID is required to delete a customer') + } + + return { + query: ` + mutation customerDelete($input: CustomerDeleteInput!) { + customerDelete(input: $input) { + deletedCustomerId + userErrors { + field + message + } + } + } + `, + variables: { + input: { + id: params.customerId, + }, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete customer', + output: {}, + } + } + + const result = data.data?.customerDelete + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + if (!result?.deletedCustomerId) { + return { + success: false, + error: 'Customer deletion was not successful', + output: {}, + } + } + + return { + success: true, + output: { + deletedId: result.deletedCustomerId, + }, + } + }, + + outputs: { + deletedId: { + type: 'string', + description: 'The ID of the deleted customer', + }, + }, +} diff --git a/apps/sim/tools/shopify/delete_product.ts b/apps/sim/tools/shopify/delete_product.ts new file mode 100644 index 00000000000..751b57c6e02 --- /dev/null +++ b/apps/sim/tools/shopify/delete_product.ts @@ -0,0 +1,113 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyDeleteProductParams, ShopifyDeleteResponse } from './types' + +export const shopifyDeleteProductTool: ToolConfig< + ShopifyDeleteProductParams, + ShopifyDeleteResponse +> = { + id: 'shopify_delete_product', + name: 'Shopify Delete Product', + description: 'Delete a product from your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + productId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID to delete (gid://shopify/Product/123456789)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.productId) { + throw new Error('Product ID is required to delete a product') + } + + return { + query: ` + mutation productDelete($input: ProductDeleteInput!) { + productDelete(input: $input) { + deletedProductId + userErrors { + field + message + } + } + } + `, + variables: { + input: { + id: params.productId, + }, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete product', + output: {}, + } + } + + const result = data.data?.productDelete + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + if (!result?.deletedProductId) { + return { + success: false, + error: 'Product deletion was not successful', + output: {}, + } + } + + return { + success: true, + output: { + deletedId: result.deletedProductId, + }, + } + }, + + outputs: { + deletedId: { + type: 'string', + description: 'The ID of the deleted product', + }, + }, +} diff --git a/apps/sim/tools/shopify/get_customer.ts b/apps/sim/tools/shopify/get_customer.ts new file mode 100644 index 00000000000..3c1fc5f6c82 --- /dev/null +++ b/apps/sim/tools/shopify/get_customer.ts @@ -0,0 +1,133 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCustomerResponse, ShopifyGetCustomerParams } from './types' + +export const shopifyGetCustomerTool: ToolConfig = + { + id: 'shopify_get_customer', + name: 'Shopify Get Customer', + description: 'Get a single customer by ID from your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID (gid://shopify/Customer/123456789)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.customerId) { + throw new Error('Customer ID is required') + } + + return { + query: ` + query getCustomer($id: ID!) { + customer(id: $id) { + id + email + firstName + lastName + phone + createdAt + updatedAt + note + tags + ordersCount + totalSpentV2 { + amount + currencyCode + } + addresses { + firstName + lastName + address1 + address2 + city + province + provinceCode + country + countryCode + zip + phone + } + defaultAddress { + firstName + lastName + address1 + address2 + city + province + country + zip + } + } + } + `, + variables: { + id: params.customerId, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get customer', + output: {}, + } + } + + const customer = data.data?.customer + if (!customer) { + return { + success: false, + error: 'Customer not found', + output: {}, + } + } + + return { + success: true, + output: { + customer, + }, + } + }, + + outputs: { + customer: { + type: 'object', + description: 'The customer details', + }, + }, + } diff --git a/apps/sim/tools/shopify/get_inventory_level.ts b/apps/sim/tools/shopify/get_inventory_level.ts new file mode 100644 index 00000000000..ff9da7560cc --- /dev/null +++ b/apps/sim/tools/shopify/get_inventory_level.ts @@ -0,0 +1,128 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyGetInventoryLevelParams, ShopifyInventoryResponse } from './types' + +export const shopifyGetInventoryLevelTool: ToolConfig< + ShopifyGetInventoryLevelParams, + ShopifyInventoryResponse +> = { + id: 'shopify_get_inventory_level', + name: 'Shopify Get Inventory Level', + description: 'Get inventory level for a product variant at a specific location', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + inventoryItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inventory item ID (gid://shopify/InventoryItem/123456789)', + }, + locationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Location ID to filter by (optional)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.inventoryItemId) { + throw new Error('Inventory item ID is required') + } + + return { + query: ` + query getInventoryItem($id: ID!) { + inventoryItem(id: $id) { + id + sku + tracked + inventoryLevels(first: 50) { + edges { + node { + id + available + location { + id + name + } + } + } + } + } + } + `, + variables: { + id: params.inventoryItemId, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get inventory level', + output: {}, + } + } + + const inventoryItem = data.data?.inventoryItem + if (!inventoryItem) { + return { + success: false, + error: 'Inventory item not found', + output: {}, + } + } + + const inventoryLevels = inventoryItem.inventoryLevels.edges.map( + (edge: { node: unknown }) => edge.node + ) + + return { + success: true, + output: { + inventoryLevel: { + id: inventoryItem.id, + sku: inventoryItem.sku, + tracked: inventoryItem.tracked, + levels: inventoryLevels, + }, + }, + } + }, + + outputs: { + inventoryLevel: { + type: 'object', + description: 'The inventory level details', + }, + }, +} diff --git a/apps/sim/tools/shopify/get_order.ts b/apps/sim/tools/shopify/get_order.ts new file mode 100644 index 00000000000..52d1ac04141 --- /dev/null +++ b/apps/sim/tools/shopify/get_order.ts @@ -0,0 +1,202 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyGetOrderParams, ShopifyOrderResponse } from './types' + +export const shopifyGetOrderTool: ToolConfig = { + id: 'shopify_get_order', + name: 'Shopify Get Order', + description: 'Get a single order by ID from your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + orderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Order ID (gid://shopify/Order/123456789)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.orderId) { + throw new Error('Order ID is required') + } + + return { + query: ` + query getOrder($id: ID!) { + order(id: $id) { + id + name + email + phone + createdAt + updatedAt + cancelledAt + closedAt + displayFinancialStatus + displayFulfillmentStatus + totalPriceSet { + shopMoney { + amount + currencyCode + } + } + subtotalPriceSet { + shopMoney { + amount + currencyCode + } + } + totalTaxSet { + shopMoney { + amount + currencyCode + } + } + totalShippingPriceSet { + shopMoney { + amount + currencyCode + } + } + note + tags + customer { + id + email + firstName + lastName + phone + } + lineItems(first: 50) { + edges { + node { + id + title + quantity + variant { + id + title + price + sku + } + originalTotalSet { + shopMoney { + amount + currencyCode + } + } + discountedTotalSet { + shopMoney { + amount + currencyCode + } + } + } + } + } + shippingAddress { + firstName + lastName + address1 + address2 + city + province + provinceCode + country + countryCode + zip + phone + } + billingAddress { + firstName + lastName + address1 + address2 + city + province + provinceCode + country + countryCode + zip + phone + } + fulfillments { + id + status + createdAt + updatedAt + trackingInfo { + company + number + url + } + } + } + } + `, + variables: { + id: params.orderId, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get order', + output: {}, + } + } + + const order = data.data?.order + if (!order) { + return { + success: false, + error: 'Order not found', + output: {}, + } + } + + return { + success: true, + output: { + order, + }, + } + }, + + outputs: { + order: { + type: 'object', + description: 'The order details', + }, + }, +} diff --git a/apps/sim/tools/shopify/get_product.ts b/apps/sim/tools/shopify/get_product.ts new file mode 100644 index 00000000000..50782f0d3d0 --- /dev/null +++ b/apps/sim/tools/shopify/get_product.ts @@ -0,0 +1,130 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyGetProductParams, ShopifyProductResponse } from './types' + +export const shopifyGetProductTool: ToolConfig = { + id: 'shopify_get_product', + name: 'Shopify Get Product', + description: 'Get a single product by ID from your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + productId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID (gid://shopify/Product/123456789)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.productId) { + throw new Error('Product ID is required') + } + + return { + query: ` + query getProduct($id: ID!) { + product(id: $id) { + id + title + handle + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + variants(first: 50) { + edges { + node { + id + title + price + compareAtPrice + sku + barcode + inventoryQuantity + weight + weightUnit + availableForSale + } + } + } + images(first: 20) { + edges { + node { + id + url + altText + } + } + } + } + } + `, + variables: { + id: params.productId, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get product', + output: {}, + } + } + + const product = data.data?.product + if (!product) { + return { + success: false, + error: 'Product not found', + output: {}, + } + } + + return { + success: true, + output: { + product, + }, + } + }, + + outputs: { + product: { + type: 'object', + description: 'The product details', + }, + }, +} diff --git a/apps/sim/tools/shopify/index.ts b/apps/sim/tools/shopify/index.ts new file mode 100644 index 00000000000..4e30b05b81e --- /dev/null +++ b/apps/sim/tools/shopify/index.ts @@ -0,0 +1,21 @@ +// Product Tools + +export { shopifyAdjustInventoryTool } from './adjust_inventory' +export { shopifyCancelOrderTool } from './cancel_order' +// Customer Tools +export { shopifyCreateCustomerTool } from './create_customer' +export { shopifyCreateProductTool } from './create_product' +export { shopifyDeleteCustomerTool } from './delete_customer' +export { shopifyDeleteProductTool } from './delete_product' +export { shopifyGetCustomerTool } from './get_customer' +// Inventory Tools +export { shopifyGetInventoryLevelTool } from './get_inventory_level' +// Order Tools +export { shopifyGetOrderTool } from './get_order' +export { shopifyGetProductTool } from './get_product' +export { shopifyListCustomersTool } from './list_customers' +export { shopifyListOrdersTool } from './list_orders' +export { shopifyListProductsTool } from './list_products' +export { shopifyUpdateCustomerTool } from './update_customer' +export { shopifyUpdateOrderTool } from './update_order' +export { shopifyUpdateProductTool } from './update_product' diff --git a/apps/sim/tools/shopify/list_customers.ts b/apps/sim/tools/shopify/list_customers.ts new file mode 100644 index 00000000000..03481872442 --- /dev/null +++ b/apps/sim/tools/shopify/list_customers.ts @@ -0,0 +1,140 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCustomersResponse, ShopifyListCustomersParams } from './types' + +export const shopifyListCustomersTool: ToolConfig< + ShopifyListCustomersParams, + ShopifyCustomersResponse +> = { + id: 'shopify_list_customers', + name: 'Shopify List Customers', + description: 'List customers from your Shopify store with optional filtering', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of customers to return (default: 50, max: 250)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search query to filter customers (e.g., "email:*@example.com" or "tag:vip" or "orders_count:>5")', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listCustomers($first: Int!, $query: String) { + customers(first: $first, query: $query) { + edges { + node { + id + email + firstName + lastName + phone + createdAt + updatedAt + note + tags + ordersCount + totalSpentV2 { + amount + currencyCode + } + defaultAddress { + address1 + city + province + country + zip + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + query: params.query || null, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list customers', + output: {}, + } + } + + const customersData = data.data?.customers + if (!customersData) { + return { + success: false, + error: 'Failed to retrieve customers', + output: {}, + } + } + + const customers = customersData.edges.map((edge: { node: unknown }) => edge.node) + + return { + success: true, + output: { + customers, + pageInfo: customersData.pageInfo, + }, + } + }, + + outputs: { + customers: { + type: 'array', + description: 'List of customers', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/list_orders.ts b/apps/sim/tools/shopify/list_orders.ts new file mode 100644 index 00000000000..6643489f2a7 --- /dev/null +++ b/apps/sim/tools/shopify/list_orders.ts @@ -0,0 +1,169 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyListOrdersParams, ShopifyOrdersResponse } from './types' + +export const shopifyListOrdersTool: ToolConfig = { + id: 'shopify_list_orders', + name: 'Shopify List Orders', + description: 'List orders from your Shopify store with optional filtering', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of orders to return (default: 50, max: 250)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search query to filter orders (e.g., "financial_status:paid" or "fulfillment_status:unfulfilled" or "email:customer@example.com")', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listOrders($first: Int!, $query: String) { + orders(first: $first, query: $query) { + edges { + node { + id + name + email + phone + createdAt + updatedAt + cancelledAt + closedAt + displayFinancialStatus + displayFulfillmentStatus + totalPriceSet { + shopMoney { + amount + currencyCode + } + } + subtotalPriceSet { + shopMoney { + amount + currencyCode + } + } + note + tags + customer { + id + email + firstName + lastName + } + lineItems(first: 10) { + edges { + node { + id + title + quantity + variant { + id + title + price + sku + } + } + } + } + shippingAddress { + firstName + lastName + city + province + country + zip + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + query: params.query || null, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list orders', + output: {}, + } + } + + const ordersData = data.data?.orders + if (!ordersData) { + return { + success: false, + error: 'Failed to retrieve orders', + output: {}, + } + } + + const orders = ordersData.edges.map((edge: { node: unknown }) => edge.node) + + return { + success: true, + output: { + orders, + pageInfo: ordersData.pageInfo, + }, + } + }, + + outputs: { + orders: { + type: 'array', + description: 'List of orders', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/list_products.ts b/apps/sim/tools/shopify/list_products.ts new file mode 100644 index 00000000000..48ac9baae04 --- /dev/null +++ b/apps/sim/tools/shopify/list_products.ts @@ -0,0 +1,150 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyListProductsParams, ShopifyProductsResponse } from './types' + +export const shopifyListProductsTool: ToolConfig< + ShopifyListProductsParams, + ShopifyProductsResponse +> = { + id: 'shopify_list_products', + name: 'Shopify List Products', + description: 'List products from your Shopify store with optional filtering', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of products to return (default: 50, max: 250)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search query to filter products (e.g., "title:shirt" or "vendor:Nike" or "status:active")', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listProducts($first: Int!, $query: String) { + products(first: $first, query: $query) { + edges { + node { + id + title + handle + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + variants(first: 10) { + edges { + node { + id + title + price + compareAtPrice + sku + inventoryQuantity + } + } + } + images(first: 5) { + edges { + node { + id + url + altText + } + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + query: params.query || null, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list products', + output: {}, + } + } + + const productsData = data.data?.products + if (!productsData) { + return { + success: false, + error: 'Failed to retrieve products', + output: {}, + } + } + + const products = productsData.edges.map((edge: { node: unknown }) => edge.node) + + return { + success: true, + output: { + products, + pageInfo: productsData.pageInfo, + }, + } + }, + + outputs: { + products: { + type: 'array', + description: 'List of products', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/types.ts b/apps/sim/tools/shopify/types.ts new file mode 100644 index 00000000000..678ebbd35c9 --- /dev/null +++ b/apps/sim/tools/shopify/types.ts @@ -0,0 +1,377 @@ +// Shopify GraphQL API Types +import type { ToolResponse } from '@/tools/types' + +// Common GraphQL Response Types +export interface ShopifyGraphQLError { + message: string + locations?: { line: number; column: number }[] + path?: string[] + extensions?: Record +} + +export interface ShopifyUserError { + field: string[] + message: string +} + +// Product Types +export interface ShopifyProduct { + id: string + title: string + handle: string + descriptionHtml: string + vendor: string + productType: string + tags: string[] + status: 'ACTIVE' | 'DRAFT' | 'ARCHIVED' + createdAt: string + updatedAt: string + variants: { + edges: Array<{ + node: ShopifyVariant + }> + } + images: { + edges: Array<{ + node: ShopifyImage + }> + } +} + +export interface ShopifyVariant { + id: string + title: string + price: string + compareAtPrice: string | null + sku: string | null + barcode: string | null + inventoryQuantity: number + weight: number | null + weightUnit: string + availableForSale: boolean +} + +export interface ShopifyImage { + id: string + url: string + altText: string | null +} + +// Order Types +export interface ShopifyOrder { + id: string + name: string + email: string | null + phone: string | null + createdAt: string + updatedAt: string + cancelledAt: string | null + closedAt: string | null + displayFinancialStatus: string + displayFulfillmentStatus: string + totalPriceSet: ShopifyMoneyBag + subtotalPriceSet: ShopifyMoneyBag + totalTaxSet: ShopifyMoneyBag + totalShippingPriceSet: ShopifyMoneyBag + note: string | null + tags: string[] + customer: ShopifyCustomer | null + lineItems: { + edges: Array<{ + node: ShopifyLineItem + }> + } + shippingAddress: ShopifyAddress | null + billingAddress: ShopifyAddress | null + fulfillments: ShopifyFulfillment[] +} + +export interface ShopifyMoneyBag { + shopMoney: { + amount: string + currencyCode: string + } + presentmentMoney: { + amount: string + currencyCode: string + } +} + +export interface ShopifyLineItem { + id: string + title: string + quantity: number + variant: ShopifyVariant | null + originalTotalSet: ShopifyMoneyBag + discountedTotalSet: ShopifyMoneyBag +} + +export interface ShopifyAddress { + firstName: string | null + lastName: string | null + address1: string | null + address2: string | null + city: string | null + province: string | null + provinceCode: string | null + country: string | null + countryCode: string | null + zip: string | null + phone: string | null +} + +// Customer Types +export interface ShopifyCustomer { + id: string + email: string | null + firstName: string | null + lastName: string | null + phone: string | null + createdAt: string + updatedAt: string + note: string | null + tags: string[] + ordersCount: string + totalSpentV2: { + amount: string + currencyCode: string + } + addresses: ShopifyAddress[] + defaultAddress: ShopifyAddress | null +} + +// Fulfillment Types +export interface ShopifyFulfillment { + id: string + status: string + createdAt: string + updatedAt: string + trackingInfo: Array<{ + company: string | null + number: string | null + url: string | null + }> +} + +// Inventory Types +export interface ShopifyInventoryLevel { + id: string + available: number + location: { + id: string + name: string + } +} + +export interface ShopifyInventoryItem { + id: string + sku: string | null + tracked: boolean + inventoryLevels: { + edges: Array<{ + node: ShopifyInventoryLevel + }> + } +} + +// Tool Parameter Types +export interface ShopifyBaseParams { + accessToken: string + shopDomain: string +} + +// Product Tool Params +export interface ShopifyCreateProductParams extends ShopifyBaseParams { + title: string + descriptionHtml?: string + vendor?: string + productType?: string + tags?: string[] + status?: 'ACTIVE' | 'DRAFT' | 'ARCHIVED' +} + +export interface ShopifyGetProductParams extends ShopifyBaseParams { + productId: string +} + +export interface ShopifyListProductsParams extends ShopifyBaseParams { + first?: number + query?: string +} + +export interface ShopifyUpdateProductParams extends ShopifyBaseParams { + productId: string + title?: string + descriptionHtml?: string + vendor?: string + productType?: string + tags?: string[] + status?: 'ACTIVE' | 'DRAFT' | 'ARCHIVED' +} + +export interface ShopifyDeleteProductParams extends ShopifyBaseParams { + productId: string +} + +// Order Tool Params +export interface ShopifyGetOrderParams extends ShopifyBaseParams { + orderId: string +} + +export interface ShopifyListOrdersParams extends ShopifyBaseParams { + first?: number + query?: string +} + +export interface ShopifyUpdateOrderParams extends ShopifyBaseParams { + orderId: string + note?: string + tags?: string[] + email?: string +} + +export interface ShopifyCancelOrderParams extends ShopifyBaseParams { + orderId: string + reason: 'CUSTOMER' | 'FRAUD' | 'INVENTORY' | 'DECLINED' | 'OTHER' + notifyCustomer?: boolean + refund?: boolean + restock?: boolean + staffNote?: string +} + +// Customer Tool Params +export interface ShopifyCreateCustomerParams extends ShopifyBaseParams { + email?: string + firstName?: string + lastName?: string + phone?: string + note?: string + tags?: string[] + addresses?: Array<{ + address1?: string + address2?: string + city?: string + province?: string + country?: string + zip?: string + phone?: string + }> +} + +export interface ShopifyGetCustomerParams extends ShopifyBaseParams { + customerId: string +} + +export interface ShopifyListCustomersParams extends ShopifyBaseParams { + first?: number + query?: string +} + +export interface ShopifyUpdateCustomerParams extends ShopifyBaseParams { + customerId: string + email?: string + firstName?: string + lastName?: string + phone?: string + note?: string + tags?: string[] +} + +export interface ShopifyDeleteCustomerParams extends ShopifyBaseParams { + customerId: string +} + +// Inventory Tool Params +export interface ShopifyGetInventoryLevelParams extends ShopifyBaseParams { + inventoryItemId: string + locationId?: string +} + +export interface ShopifyAdjustInventoryParams extends ShopifyBaseParams { + inventoryItemId: string + locationId: string + delta: number +} + +export interface ShopifySetInventoryParams extends ShopifyBaseParams { + inventoryItemId: string + locationId: string + quantity: number +} + +// Fulfillment Tool Params +export interface ShopifyCreateFulfillmentParams extends ShopifyBaseParams { + orderId: string + lineItemIds?: string[] + trackingNumber?: string + trackingCompany?: string + trackingUrl?: string + notifyCustomer?: boolean +} + +// Tool Response Types +export interface ShopifyProductResponse extends ToolResponse { + output: { + product?: ShopifyProduct + } +} + +export interface ShopifyProductsResponse extends ToolResponse { + output: { + products?: ShopifyProduct[] + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export interface ShopifyOrderResponse extends ToolResponse { + output: { + order?: ShopifyOrder | Record + } +} + +export interface ShopifyOrdersResponse extends ToolResponse { + output: { + orders?: ShopifyOrder[] + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export interface ShopifyCustomerResponse extends ToolResponse { + output: { + customer?: ShopifyCustomer + } +} + +export interface ShopifyCustomersResponse extends ToolResponse { + output: { + customers?: ShopifyCustomer[] + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export interface ShopifyInventoryResponse extends ToolResponse { + output: { + inventoryLevel?: ShopifyInventoryLevel | Record + } +} + +export interface ShopifyFulfillmentResponse extends ToolResponse { + output: { + fulfillment?: ShopifyFulfillment + } +} + +export interface ShopifyDeleteResponse extends ToolResponse { + output: { + deletedId?: string + } +} diff --git a/apps/sim/tools/shopify/update_customer.ts b/apps/sim/tools/shopify/update_customer.ts new file mode 100644 index 00000000000..622ca98129a --- /dev/null +++ b/apps/sim/tools/shopify/update_customer.ts @@ -0,0 +1,200 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyCustomerResponse, ShopifyUpdateCustomerParams } from './types' + +export const shopifyUpdateCustomerTool: ToolConfig< + ShopifyUpdateCustomerParams, + ShopifyCustomerResponse +> = { + id: 'shopify_update_customer', + name: 'Shopify Update Customer', + description: 'Update an existing customer in your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID to update (gid://shopify/Customer/123456789)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New customer email address', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New customer first name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New customer last name', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New customer phone number', + }, + note: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New note about the customer', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New customer tags', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.customerId) { + throw new Error('Customer ID is required to update a customer') + } + + const input: Record = { + id: params.customerId, + } + + if (params.email !== undefined) { + input.email = params.email + } + if (params.firstName !== undefined) { + input.firstName = params.firstName + } + if (params.lastName !== undefined) { + input.lastName = params.lastName + } + if (params.phone !== undefined) { + input.phone = params.phone + } + if (params.note !== undefined) { + input.note = params.note + } + if (params.tags !== undefined) { + input.tags = params.tags + } + + return { + query: ` + mutation customerUpdate($input: CustomerInput!) { + customerUpdate(input: $input) { + customer { + id + email + firstName + lastName + phone + createdAt + updatedAt + note + tags + ordersCount + totalSpentV2 { + amount + currencyCode + } + addresses { + address1 + city + province + country + zip + } + defaultAddress { + address1 + city + province + country + zip + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update customer', + output: {}, + } + } + + const result = data.data?.customerUpdate + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const customer = result?.customer + if (!customer) { + return { + success: false, + error: 'Customer update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + customer, + }, + } + }, + + outputs: { + customer: { + type: 'object', + description: 'The updated customer', + }, + }, +} diff --git a/apps/sim/tools/shopify/update_order.ts b/apps/sim/tools/shopify/update_order.ts new file mode 100644 index 00000000000..904c95f2af3 --- /dev/null +++ b/apps/sim/tools/shopify/update_order.ts @@ -0,0 +1,164 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyOrderResponse, ShopifyUpdateOrderParams } from './types' + +export const shopifyUpdateOrderTool: ToolConfig = { + id: 'shopify_update_order', + name: 'Shopify Update Order', + description: 'Update an existing order in your Shopify store (note, tags, email)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + orderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Order ID to update (gid://shopify/Order/123456789)', + }, + note: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New order note', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New order tags', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New customer email for the order', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.orderId) { + throw new Error('Order ID is required to update an order') + } + + const input: Record = { + id: params.orderId, + } + + if (params.note !== undefined) { + input.note = params.note + } + if (params.tags !== undefined) { + input.tags = params.tags + } + if (params.email !== undefined) { + input.email = params.email + } + + return { + query: ` + mutation orderUpdate($input: OrderInput!) { + orderUpdate(input: $input) { + order { + id + name + email + phone + createdAt + updatedAt + note + tags + displayFinancialStatus + displayFulfillmentStatus + totalPriceSet { + shopMoney { + amount + currencyCode + } + } + customer { + id + email + firstName + lastName + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update order', + output: {}, + } + } + + const result = data.data?.orderUpdate + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const order = result?.order + if (!order) { + return { + success: false, + error: 'Order update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + order, + }, + } + }, + + outputs: { + order: { + type: 'object', + description: 'The updated order', + }, + }, +} diff --git a/apps/sim/tools/shopify/update_product.ts b/apps/sim/tools/shopify/update_product.ts new file mode 100644 index 00000000000..d744da14d53 --- /dev/null +++ b/apps/sim/tools/shopify/update_product.ts @@ -0,0 +1,203 @@ +import type { ToolConfig } from '@/tools/types' +import type { ShopifyProductResponse, ShopifyUpdateProductParams } from './types' + +export const shopifyUpdateProductTool: ToolConfig< + ShopifyUpdateProductParams, + ShopifyProductResponse +> = { + id: 'shopify_update_product', + name: 'Shopify Update Product', + description: 'Update an existing product in your Shopify store', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + productId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID to update (gid://shopify/Product/123456789)', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New product title', + }, + descriptionHtml: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New product description (HTML)', + }, + vendor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New product vendor/brand', + }, + productType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New product type/category', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New product tags', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New product status (ACTIVE, DRAFT, ARCHIVED)', + }, + }, + + request: { + url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + if (!params.productId) { + throw new Error('Product ID is required to update a product') + } + + const input: Record = { + id: params.productId, + } + + if (params.title !== undefined) { + input.title = params.title + } + if (params.descriptionHtml !== undefined) { + input.descriptionHtml = params.descriptionHtml + } + if (params.vendor !== undefined) { + input.vendor = params.vendor + } + if (params.productType !== undefined) { + input.productType = params.productType + } + if (params.tags !== undefined) { + input.tags = params.tags + } + if (params.status !== undefined) { + input.status = params.status + } + + return { + query: ` + mutation productUpdate($input: ProductInput!) { + productUpdate(input: $input) { + product { + id + title + handle + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + variants(first: 10) { + edges { + node { + id + title + price + compareAtPrice + sku + inventoryQuantity + } + } + } + images(first: 10) { + edges { + node { + id + url + altText + } + } + } + } + userErrors { + field + message + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update product', + output: {}, + } + } + + const result = data.data?.productUpdate + if (result?.userErrors?.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const product = result?.product + if (!product) { + return { + success: false, + error: 'Product update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + product, + }, + } + }, + + outputs: { + product: { + type: 'object', + description: 'The updated product', + }, + }, +} diff --git a/apps/sim/tools/ssh/check_command_exists.ts b/apps/sim/tools/ssh/check_command_exists.ts new file mode 100644 index 00000000000..dc27f4190ba --- /dev/null +++ b/apps/sim/tools/ssh/check_command_exists.ts @@ -0,0 +1,96 @@ +import type { SSHCheckCommandExistsParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const checkCommandExistsTool: ToolConfig = { + id: 'ssh_check_command_exists', + name: 'SSH Check Command Exists', + description: 'Check if a command/program exists on the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + commandName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Command name to check (e.g., docker, git, python3)', + }, + }, + + request: { + url: '/api/tools/ssh/check-command-exists', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + commandName: params.commandName, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH check command exists failed') + } + + return { + success: true, + output: { + commandExists: data.exists ?? false, + commandPath: data.path, + version: data.version, + message: data.message, + }, + } + }, + + outputs: { + commandExists: { type: 'boolean', description: 'Whether the command exists' }, + commandPath: { type: 'string', description: 'Full path to the command (if found)' }, + version: { type: 'string', description: 'Command version output (if applicable)' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/check_file_exists.ts b/apps/sim/tools/ssh/check_file_exists.ts new file mode 100644 index 00000000000..b1a7b4efd1f --- /dev/null +++ b/apps/sim/tools/ssh/check_file_exists.ts @@ -0,0 +1,107 @@ +import type { SSHCheckFileExistsParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const checkFileExistsTool: ToolConfig = { + id: 'ssh_check_file_exists', + name: 'SSH Check File Exists', + description: 'Check if a file or directory exists on the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Remote file or directory path to check', + }, + type: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Expected type: file, directory, or any (default: any)', + }, + }, + + request: { + url: '/api/tools/ssh/check-file-exists', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + type: params.type || 'any', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH check file exists failed') + } + + return { + success: true, + output: { + exists: data.exists ?? false, + type: data.type || 'not_found', + size: data.size, + permissions: data.permissions, + modified: data.modified, + message: data.message, + }, + } + }, + + outputs: { + exists: { type: 'boolean', description: 'Whether the path exists' }, + type: { type: 'string', description: 'Type of path (file, directory, symlink, not_found)' }, + size: { type: 'number', description: 'File size if it is a file' }, + permissions: { type: 'string', description: 'File permissions (e.g., 0755)' }, + modified: { type: 'string', description: 'Last modified timestamp' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/create_directory.ts b/apps/sim/tools/ssh/create_directory.ts new file mode 100644 index 00000000000..3c26685ab07 --- /dev/null +++ b/apps/sim/tools/ssh/create_directory.ts @@ -0,0 +1,110 @@ +import type { SSHCreateDirectoryParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const createDirectoryTool: ToolConfig = { + id: 'ssh_create_directory', + name: 'SSH Create Directory', + description: 'Create a directory on the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Directory path to create', + }, + recursive: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Create parent directories if they do not exist (default: true)', + }, + permissions: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Directory permissions (default: 0755)', + }, + }, + + request: { + url: '/api/tools/ssh/create-directory', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + recursive: params.recursive !== false, + permissions: params.permissions || '0755', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH create directory failed') + } + + return { + success: true, + output: { + created: data.created ?? true, + remotePath: data.path, + alreadyExists: data.alreadyExists ?? false, + message: data.message, + }, + } + }, + + outputs: { + created: { type: 'boolean', description: 'Whether the directory was created successfully' }, + remotePath: { type: 'string', description: 'Created directory path' }, + alreadyExists: { type: 'boolean', description: 'Whether the directory already existed' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/delete_file.ts b/apps/sim/tools/ssh/delete_file.ts new file mode 100644 index 00000000000..8527ad9fff7 --- /dev/null +++ b/apps/sim/tools/ssh/delete_file.ts @@ -0,0 +1,108 @@ +import type { SSHDeleteFileParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteFileTool: ToolConfig = { + id: 'ssh_delete_file', + name: 'SSH Delete File', + description: 'Delete a file or directory from the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Path to delete', + }, + recursive: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Recursively delete directories (default: false)', + }, + force: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Force deletion without confirmation (default: false)', + }, + }, + + request: { + url: '/api/tools/ssh/delete-file', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + recursive: params.recursive === true, + force: params.force === true, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH delete file failed') + } + + return { + success: true, + output: { + deleted: data.deleted ?? true, + remotePath: data.path, + message: data.message, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the path was deleted successfully' }, + remotePath: { type: 'string', description: 'Deleted path' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/download_file.ts b/apps/sim/tools/ssh/download_file.ts new file mode 100644 index 00000000000..e7ed207f024 --- /dev/null +++ b/apps/sim/tools/ssh/download_file.ts @@ -0,0 +1,100 @@ +import type { SSHDownloadFileParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const downloadFileTool: ToolConfig = { + id: 'ssh_download_file', + name: 'SSH Download File', + description: 'Download a file from a remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + remotePath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Path of the file on the remote server', + }, + }, + + request: { + url: '/api/tools/ssh/download-file', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + remotePath: params.remotePath, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH file download failed') + } + + return { + success: true, + output: { + downloaded: true, + fileContent: data.content, + fileName: data.fileName, + remotePath: data.remotePath, + size: data.size, + message: data.message, + }, + } + }, + + outputs: { + downloaded: { type: 'boolean', description: 'Whether the file was downloaded successfully' }, + fileContent: { type: 'string', description: 'File content (base64 encoded for binary files)' }, + fileName: { type: 'string', description: 'Name of the downloaded file' }, + remotePath: { type: 'string', description: 'Source path on the remote server' }, + size: { type: 'number', description: 'File size in bytes' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/execute_command.ts b/apps/sim/tools/ssh/execute_command.ts new file mode 100644 index 00000000000..f6fb5af7717 --- /dev/null +++ b/apps/sim/tools/ssh/execute_command.ts @@ -0,0 +1,105 @@ +import type { SSHExecuteCommandParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const executeCommandTool: ToolConfig = { + id: 'ssh_execute_command', + name: 'SSH Execute Command', + description: 'Execute a shell command on a remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + command: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Shell command to execute on the remote server', + }, + workingDirectory: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Working directory for command execution', + }, + }, + + request: { + url: '/api/tools/ssh/execute-command', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + command: params.command, + workingDirectory: params.workingDirectory, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH command execution failed') + } + + return { + success: true, + output: { + stdout: data.stdout || '', + stderr: data.stderr || '', + exitCode: data.exitCode ?? 0, + success: data.exitCode === 0, + message: data.message, + }, + } + }, + + outputs: { + stdout: { type: 'string', description: 'Standard output from command' }, + stderr: { type: 'string', description: 'Standard error output' }, + exitCode: { type: 'number', description: 'Command exit code' }, + success: { type: 'boolean', description: 'Whether command succeeded (exit code 0)' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/execute_script.ts b/apps/sim/tools/ssh/execute_script.ts new file mode 100644 index 00000000000..87694da5c8a --- /dev/null +++ b/apps/sim/tools/ssh/execute_script.ts @@ -0,0 +1,114 @@ +import type { SSHExecuteScriptParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const executeScriptTool: ToolConfig = { + id: 'ssh_execute_script', + name: 'SSH Execute Script', + description: 'Upload and execute a multi-line script on a remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + script: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Script content to execute (bash, python, etc.)', + }, + interpreter: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Script interpreter (default: /bin/bash)', + }, + workingDirectory: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Working directory for script execution', + }, + }, + + request: { + url: '/api/tools/ssh/execute-script', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + script: params.script, + interpreter: params.interpreter || '/bin/bash', + workingDirectory: params.workingDirectory, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH script execution failed') + } + + return { + success: true, + output: { + stdout: data.stdout || '', + stderr: data.stderr || '', + exitCode: data.exitCode ?? 0, + success: data.exitCode === 0, + scriptPath: data.scriptPath, + message: data.message, + }, + } + }, + + outputs: { + stdout: { type: 'string', description: 'Standard output from script' }, + stderr: { type: 'string', description: 'Standard error output' }, + exitCode: { type: 'number', description: 'Script exit code' }, + success: { type: 'boolean', description: 'Whether script succeeded (exit code 0)' }, + scriptPath: { type: 'string', description: 'Temporary path where script was uploaded' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/get_system_info.ts b/apps/sim/tools/ssh/get_system_info.ts new file mode 100644 index 00000000000..b786211a057 --- /dev/null +++ b/apps/sim/tools/ssh/get_system_info.ts @@ -0,0 +1,101 @@ +import type { SSHGetSystemInfoParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const getSystemInfoTool: ToolConfig = { + id: 'ssh_get_system_info', + name: 'SSH Get System Info', + description: 'Retrieve system information from the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + }, + + request: { + url: '/api/tools/ssh/get-system-info', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH get system info failed') + } + + return { + success: true, + output: { + hostname: data.hostname, + os: data.os, + architecture: data.architecture, + uptime: data.uptime, + memory: data.memory, + diskSpace: data.diskSpace, + message: data.message, + }, + } + }, + + outputs: { + hostname: { type: 'string', description: 'Server hostname' }, + os: { type: 'string', description: 'Operating system (e.g., Linux, Darwin)' }, + architecture: { type: 'string', description: 'CPU architecture (e.g., x64, arm64)' }, + uptime: { type: 'number', description: 'System uptime in seconds' }, + memory: { + type: 'json', + description: 'Memory information (total, free, used)', + }, + diskSpace: { + type: 'json', + description: 'Disk space information (total, free, used)', + }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/index.ts b/apps/sim/tools/ssh/index.ts new file mode 100644 index 00000000000..be79a5e0eda --- /dev/null +++ b/apps/sim/tools/ssh/index.ts @@ -0,0 +1,14 @@ +export { checkCommandExistsTool } from './check_command_exists' +export { checkFileExistsTool } from './check_file_exists' +export { createDirectoryTool } from './create_directory' +export { deleteFileTool } from './delete_file' +export { downloadFileTool } from './download_file' +export { executeCommandTool } from './execute_command' +export { executeScriptTool } from './execute_script' +export { getSystemInfoTool } from './get_system_info' +export { listDirectoryTool } from './list_directory' +export { moveRenameTool } from './move_rename' +export { readFileContentTool } from './read_file_content' +export * from './types' +export { uploadFileTool } from './upload_file' +export { writeFileContentTool } from './write_file_content' diff --git a/apps/sim/tools/ssh/list_directory.ts b/apps/sim/tools/ssh/list_directory.ts new file mode 100644 index 00000000000..dd6a0daecae --- /dev/null +++ b/apps/sim/tools/ssh/list_directory.ts @@ -0,0 +1,123 @@ +import type { SSHListDirectoryParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const listDirectoryTool: ToolConfig = { + id: 'ssh_list_directory', + name: 'SSH List Directory', + description: 'List files and directories in a remote directory', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Remote directory path to list', + }, + detailed: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include file details (size, permissions, modified date)', + }, + recursive: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'List subdirectories recursively (default: false)', + }, + }, + + request: { + url: '/api/tools/ssh/list-directory', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + detailed: params.detailed !== false, + recursive: params.recursive === true, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH list directory failed') + } + + return { + success: true, + output: { + entries: data.entries || [], + totalFiles: data.totalFiles || 0, + totalDirectories: data.totalDirectories || 0, + message: data.message, + }, + } + }, + + outputs: { + entries: { + type: 'array', + description: 'Array of file and directory entries', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'File or directory name' }, + type: { type: 'string', description: 'Entry type (file, directory, symlink)' }, + size: { type: 'number', description: 'File size in bytes' }, + permissions: { type: 'string', description: 'File permissions' }, + modified: { type: 'string', description: 'Last modified timestamp' }, + }, + }, + }, + totalFiles: { type: 'number', description: 'Total number of files' }, + totalDirectories: { type: 'number', description: 'Total number of directories' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/move_rename.ts b/apps/sim/tools/ssh/move_rename.ts new file mode 100644 index 00000000000..8ec95be2f3d --- /dev/null +++ b/apps/sim/tools/ssh/move_rename.ts @@ -0,0 +1,110 @@ +import type { SSHMoveRenameParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const moveRenameTool: ToolConfig = { + id: 'ssh_move_rename', + name: 'SSH Move/Rename', + description: 'Move or rename a file or directory on the remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + sourcePath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Current path of the file or directory', + }, + destinationPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New path for the file or directory', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Overwrite destination if it exists (default: false)', + }, + }, + + request: { + url: '/api/tools/ssh/move-rename', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + sourcePath: params.sourcePath, + destinationPath: params.destinationPath, + overwrite: params.overwrite === true, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH move/rename failed') + } + + return { + success: true, + output: { + moved: data.success ?? true, + sourcePath: data.sourcePath, + destinationPath: data.destinationPath, + message: data.message, + }, + } + }, + + outputs: { + moved: { type: 'boolean', description: 'Whether the operation was successful' }, + sourcePath: { type: 'string', description: 'Original path' }, + destinationPath: { type: 'string', description: 'New path' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/read_file_content.ts b/apps/sim/tools/ssh/read_file_content.ts new file mode 100644 index 00000000000..c7e72d997cc --- /dev/null +++ b/apps/sim/tools/ssh/read_file_content.ts @@ -0,0 +1,112 @@ +import type { SSHReadFileContentParams, SSHResponse } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const readFileContentTool: ToolConfig = { + id: 'ssh_read_file_content', + name: 'SSH Read File Content', + description: 'Read the contents of a remote file', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Remote file path to read', + }, + encoding: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'File encoding (default: utf-8)', + }, + maxSize: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum file size to read in MB (default: 10)', + }, + }, + + request: { + url: '/api/tools/ssh/read-file-content', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + encoding: params.encoding || 'utf-8', + maxSize: params.maxSize || 10, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH read file content failed') + } + + return { + success: true, + output: { + content: data.content, + size: data.size, + lines: data.lines, + remotePath: data.path, + message: data.message, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'File content as string' }, + size: { type: 'number', description: 'File size in bytes' }, + lines: { type: 'number', description: 'Number of lines in file' }, + remotePath: { type: 'string', description: 'Remote file path' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/types.ts b/apps/sim/tools/ssh/types.ts new file mode 100644 index 00000000000..ed7274edea3 --- /dev/null +++ b/apps/sim/tools/ssh/types.ts @@ -0,0 +1,229 @@ +import type { ToolResponse } from '@/tools/types' + +// Base SSH connection configuration +export interface SSHConnectionConfig { + host: string + port: number + username: string + password?: string + privateKey?: string + passphrase?: string + timeout?: number + keepaliveInterval?: number + readyTimeout?: number +} + +// Execute Command parameters +export interface SSHExecuteCommandParams extends SSHConnectionConfig { + command: string + workingDirectory?: string +} + +// Execute Script parameters +export interface SSHExecuteScriptParams extends SSHConnectionConfig { + script: string + interpreter?: string + workingDirectory?: string +} + +// Check Command Exists parameters +export interface SSHCheckCommandExistsParams extends SSHConnectionConfig { + commandName: string +} + +// Upload File parameters +export interface SSHUploadFileParams extends SSHConnectionConfig { + fileContent: string + fileName: string + remotePath: string + permissions?: string + overwrite?: boolean +} + +// Upload Directory parameters +export interface SSHUploadDirectoryParams extends SSHConnectionConfig { + localDirectory: string + remoteDirectory: string + recursive?: boolean + concurrency?: number +} + +// Download File parameters +export interface SSHDownloadFileParams extends SSHConnectionConfig { + remotePath: string +} + +// Download Directory parameters +export interface SSHDownloadDirectoryParams extends SSHConnectionConfig { + remotePath: string + recursive?: boolean +} + +// List Directory parameters +export interface SSHListDirectoryParams extends SSHConnectionConfig { + path: string + detailed?: boolean + recursive?: boolean +} + +// Check File Exists parameters +export interface SSHCheckFileExistsParams extends SSHConnectionConfig { + path: string + type?: 'file' | 'directory' | 'any' +} + +// Create Directory parameters +export interface SSHCreateDirectoryParams extends SSHConnectionConfig { + path: string + recursive?: boolean + permissions?: string +} + +// Delete File parameters +export interface SSHDeleteFileParams extends SSHConnectionConfig { + path: string + recursive?: boolean + force?: boolean +} + +// Move/Rename parameters +export interface SSHMoveRenameParams extends SSHConnectionConfig { + sourcePath: string + destinationPath: string + overwrite?: boolean +} + +// Get System Info parameters +export interface SSHGetSystemInfoParams extends SSHConnectionConfig {} + +// Read File Content parameters +export interface SSHReadFileContentParams extends SSHConnectionConfig { + path: string + encoding?: string + maxSize?: number +} + +// Write File Content parameters +export interface SSHWriteFileContentParams extends SSHConnectionConfig { + path: string + content: string + mode?: 'overwrite' | 'append' | 'create' + permissions?: string +} + +// File info interface +export interface SSHFileInfo { + name: string + type: 'file' | 'directory' | 'symlink' | 'other' + size: number + permissions: string + modified: string + owner?: string + group?: string +} + +// System info interface +export interface SSHSystemInfo { + hostname: string + os: string + architecture: string + uptime: number + memory: { + total: number + free: number + used: number + } + diskSpace: { + total: number + free: number + used: number + } +} + +// Response types +export interface SSHResponse extends ToolResponse { + output: { + // Command execution outputs + stdout?: string + stderr?: string + exitCode?: number + success?: boolean + + // File operation outputs + uploaded?: boolean + downloaded?: boolean + fileContent?: string + fileName?: string + remotePath?: string + localPath?: string + size?: number + + // Directory listing outputs + entries?: SSHFileInfo[] + totalFiles?: number + totalDirectories?: number + + // File existence check outputs + exists?: boolean + type?: 'file' | 'directory' | 'symlink' | 'not_found' + permissions?: string + modified?: string + + // System info outputs + hostname?: string + os?: string + architecture?: string + uptime?: number + memory?: { + total: number + free: number + used: number + } + diskSpace?: { + total: number + free: number + used: number + } + + // File content outputs + content?: string + lines?: number + + // Operation status + created?: boolean + deleted?: boolean + written?: boolean + moved?: boolean + alreadyExists?: boolean + + // Command check outputs + commandExists?: boolean + commandPath?: string + version?: string + + // Script execution outputs + scriptPath?: string + + // Metadata + message?: string + metadata?: Record + } +} + +// Union type for all SSH parameters +export type SSHParams = + | SSHExecuteCommandParams + | SSHExecuteScriptParams + | SSHCheckCommandExistsParams + | SSHUploadFileParams + | SSHUploadDirectoryParams + | SSHDownloadFileParams + | SSHDownloadDirectoryParams + | SSHListDirectoryParams + | SSHCheckFileExistsParams + | SSHCreateDirectoryParams + | SSHDeleteFileParams + | SSHMoveRenameParams + | SSHGetSystemInfoParams + | SSHReadFileContentParams + | SSHWriteFileContentParams diff --git a/apps/sim/tools/ssh/upload_file.ts b/apps/sim/tools/ssh/upload_file.ts new file mode 100644 index 00000000000..10cd389cbf5 --- /dev/null +++ b/apps/sim/tools/ssh/upload_file.ts @@ -0,0 +1,124 @@ +import type { SSHResponse, SSHUploadFileParams } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const uploadFileTool: ToolConfig = { + id: 'ssh_upload_file', + name: 'SSH Upload File', + description: 'Upload a file to a remote SSH server', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + fileContent: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'File content to upload (base64 encoded for binary files)', + }, + fileName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the file being uploaded', + }, + remotePath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Destination path on the remote server', + }, + permissions: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'File permissions (e.g., 0644)', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Whether to overwrite existing files (default: true)', + }, + }, + + request: { + url: '/api/tools/ssh/upload-file', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + fileContent: params.fileContent, + fileName: params.fileName, + remotePath: params.remotePath, + permissions: params.permissions, + overwrite: params.overwrite !== false, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH file upload failed') + } + + return { + success: true, + output: { + uploaded: true, + remotePath: data.remotePath, + size: data.size, + message: data.message, + }, + } + }, + + outputs: { + uploaded: { type: 'boolean', description: 'Whether the file was uploaded successfully' }, + remotePath: { type: 'string', description: 'Final path on the remote server' }, + size: { type: 'number', description: 'File size in bytes' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/ssh/write_file_content.ts b/apps/sim/tools/ssh/write_file_content.ts new file mode 100644 index 00000000000..90b0e125087 --- /dev/null +++ b/apps/sim/tools/ssh/write_file_content.ts @@ -0,0 +1,117 @@ +import type { SSHResponse, SSHWriteFileContentParams } from '@/tools/ssh/types' +import type { ToolConfig } from '@/tools/types' + +export const writeFileContentTool: ToolConfig = { + id: 'ssh_write_file_content', + name: 'SSH Write File Content', + description: 'Write or append content to a remote file', + version: '1.0.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'SSH server port (default: 22)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'SSH username', + }, + password: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Password for authentication (if not using private key)', + }, + privateKey: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Private key for authentication (OpenSSH format)', + }, + passphrase: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Passphrase for encrypted private key', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Remote file path to write to', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Content to write to the file', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Write mode: overwrite, append, or create (default: overwrite)', + }, + permissions: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'File permissions (e.g., 0644)', + }, + }, + + request: { + url: '/api/tools/ssh/write-file-content', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: Number(params.port) || 22, + username: params.username, + password: params.password, + privateKey: params.privateKey, + passphrase: params.passphrase, + path: params.path, + content: params.content, + mode: params.mode || 'overwrite', + permissions: params.permissions, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'SSH write file content failed') + } + + return { + success: true, + output: { + written: data.written ?? true, + remotePath: data.path, + size: data.size, + message: data.message, + }, + } + }, + + outputs: { + written: { type: 'boolean', description: 'Whether the file was written successfully' }, + remotePath: { type: 'string', description: 'File path' }, + size: { type: 'number', description: 'Final file size in bytes' }, + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index 1a3bfd10f83..e64c33e4d56 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -92,7 +92,7 @@ export interface ToolConfig

{ url: string | ((params: P) => string) method: HttpMethod | ((params: P) => HttpMethod) headers: (params: P) => Record - body?: (params: P) => Record + body?: (params: P) => Record | string } // Post-processing (optional) - allows additional processing after the initial request diff --git a/apps/sim/tools/wordpress/create_category.ts b/apps/sim/tools/wordpress/create_category.ts new file mode 100644 index 00000000000..bdaf9ec010c --- /dev/null +++ b/apps/sim/tools/wordpress/create_category.ts @@ -0,0 +1,127 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressCreateCategoryParams, WordPressCreateCategoryResponse } from './types' + +export const createCategoryTool: ToolConfig< + WordPressCreateCategoryParams, + WordPressCreateCategoryResponse +> = { + id: 'wordpress_create_category', + name: 'WordPress Create Category', + description: 'Create a new category in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Category name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Category description', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Parent category ID for hierarchical categories', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the category', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/categories` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = { + name: params.name, + } + + if (params.description) body.description = params.description + if (params.parent) body.parent = params.parent + if (params.slug) body.slug = params.slug + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + category: { + id: data.id, + count: data.count, + description: data.description, + link: data.link, + name: data.name, + slug: data.slug, + taxonomy: data.taxonomy, + parent: data.parent, + }, + }, + } + }, + + outputs: { + category: { + type: 'object', + description: 'The created category', + properties: { + id: { type: 'number', description: 'Category ID' }, + count: { type: 'number', description: 'Number of posts in this category' }, + description: { type: 'string', description: 'Category description' }, + link: { type: 'string', description: 'Category archive URL' }, + name: { type: 'string', description: 'Category name' }, + slug: { type: 'string', description: 'Category slug' }, + taxonomy: { type: 'string', description: 'Taxonomy name' }, + parent: { type: 'number', description: 'Parent category ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/create_comment.ts b/apps/sim/tools/wordpress/create_comment.ts new file mode 100644 index 00000000000..2513389365a --- /dev/null +++ b/apps/sim/tools/wordpress/create_comment.ts @@ -0,0 +1,147 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressCreateCommentParams, WordPressCreateCommentResponse } from './types' + +export const createCommentTool: ToolConfig< + WordPressCreateCommentParams, + WordPressCreateCommentResponse +> = { + id: 'wordpress_create_comment', + name: 'WordPress Create Comment', + description: 'Create a new comment on a WordPress post', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + postId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the post to comment on', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment content', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Parent comment ID for replies', + }, + authorName: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comment author display name', + }, + authorEmail: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comment author email', + }, + authorUrl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comment author URL', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/comments` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = { + post: params.postId, + content: params.content, + } + + if (params.parent) body.parent = params.parent + if (params.authorName) body.author_name = params.authorName + if (params.authorEmail) body.author_email = params.authorEmail + if (params.authorUrl) body.author_url = params.authorUrl + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + comment: { + id: data.id, + post: data.post, + parent: data.parent, + author: data.author, + author_name: data.author_name, + author_email: data.author_email, + author_url: data.author_url, + date: data.date, + content: data.content, + link: data.link, + status: data.status, + }, + }, + } + }, + + outputs: { + comment: { + type: 'object', + description: 'The created comment', + properties: { + id: { type: 'number', description: 'Comment ID' }, + post: { type: 'number', description: 'Post ID' }, + parent: { type: 'number', description: 'Parent comment ID' }, + author: { type: 'number', description: 'Author user ID' }, + author_name: { type: 'string', description: 'Author display name' }, + author_email: { type: 'string', description: 'Author email' }, + author_url: { type: 'string', description: 'Author URL' }, + date: { type: 'string', description: 'Comment date' }, + content: { type: 'object', description: 'Comment content object' }, + link: { type: 'string', description: 'Comment permalink' }, + status: { type: 'string', description: 'Comment status' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/create_page.ts b/apps/sim/tools/wordpress/create_page.ts new file mode 100644 index 00000000000..2d85905b6fd --- /dev/null +++ b/apps/sim/tools/wordpress/create_page.ts @@ -0,0 +1,164 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressCreatePageParams, WordPressCreatePageResponse } from './types' + +export const createPageTool: ToolConfig = { + id: 'wordpress_create_page', + name: 'WordPress Create Page', + description: 'Create a new page in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Page title', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page content (HTML or plain text)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Page status: publish, draft, pending, private', + }, + excerpt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page excerpt', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Parent page ID for hierarchical pages', + }, + menuOrder: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Order in page menu', + }, + featuredMedia: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Featured image media ID', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the page', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/pages` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = { + title: params.title, + } + + if (params.content) body.content = params.content + if (params.status) body.status = params.status + if (params.excerpt) body.excerpt = params.excerpt + if (params.slug) body.slug = params.slug + if (params.parent) body.parent = params.parent + if (params.menuOrder) body.menu_order = params.menuOrder + if (params.featuredMedia) body.featured_media = params.featuredMedia + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + page: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + parent: data.parent, + menu_order: data.menu_order, + }, + }, + } + }, + + outputs: { + page: { + type: 'object', + description: 'The created page', + properties: { + id: { type: 'number', description: 'Page ID' }, + date: { type: 'string', description: 'Page creation date' }, + modified: { type: 'string', description: 'Page modification date' }, + slug: { type: 'string', description: 'Page slug' }, + status: { type: 'string', description: 'Page status' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Page URL' }, + title: { type: 'object', description: 'Page title object' }, + content: { type: 'object', description: 'Page content object' }, + excerpt: { type: 'object', description: 'Page excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menu_order: { type: 'number', description: 'Menu order' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/create_post.ts b/apps/sim/tools/wordpress/create_post.ts new file mode 100644 index 00000000000..5473d6378c7 --- /dev/null +++ b/apps/sim/tools/wordpress/create_post.ts @@ -0,0 +1,176 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressCreatePostParams, WordPressCreatePostResponse } from './types' + +export const createPostTool: ToolConfig = { + id: 'wordpress_create_post', + name: 'WordPress Create Post', + description: 'Create a new blog post in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Post title', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post content (HTML or plain text)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Post status: publish, draft, pending, private, or future', + }, + excerpt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post excerpt', + }, + categories: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated category IDs', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated tag IDs', + }, + featuredMedia: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Featured image media ID', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the post', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/posts` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = { + title: params.title, + } + + if (params.content) body.content = params.content + if (params.status) body.status = params.status + if (params.excerpt) body.excerpt = params.excerpt + if (params.slug) body.slug = params.slug + if (params.featuredMedia) body.featured_media = params.featuredMedia + + if (params.categories) { + body.categories = params.categories + .split(',') + .map((id: string) => Number.parseInt(id.trim(), 10)) + .filter((id: number) => !Number.isNaN(id)) + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((id: string) => Number.parseInt(id.trim(), 10)) + .filter((id: number) => !Number.isNaN(id)) + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + post: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + categories: data.categories || [], + tags: data.tags || [], + }, + }, + } + }, + + outputs: { + post: { + type: 'object', + description: 'The created post', + properties: { + id: { type: 'number', description: 'Post ID' }, + date: { type: 'string', description: 'Post creation date' }, + modified: { type: 'string', description: 'Post modification date' }, + slug: { type: 'string', description: 'Post slug' }, + status: { type: 'string', description: 'Post status' }, + type: { type: 'string', description: 'Post type' }, + link: { type: 'string', description: 'Post URL' }, + title: { type: 'object', description: 'Post title object' }, + content: { type: 'object', description: 'Post content object' }, + excerpt: { type: 'object', description: 'Post excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + categories: { type: 'array', description: 'Category IDs' }, + tags: { type: 'array', description: 'Tag IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/create_tag.ts b/apps/sim/tools/wordpress/create_tag.ts new file mode 100644 index 00000000000..13a30280b30 --- /dev/null +++ b/apps/sim/tools/wordpress/create_tag.ts @@ -0,0 +1,115 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressCreateTagParams, WordPressCreateTagResponse } from './types' + +export const createTagTool: ToolConfig = { + id: 'wordpress_create_tag', + name: 'WordPress Create Tag', + description: 'Create a new tag in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Tag name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Tag description', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the tag', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/tags` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = { + name: params.name, + } + + if (params.description) body.description = params.description + if (params.slug) body.slug = params.slug + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + tag: { + id: data.id, + count: data.count, + description: data.description, + link: data.link, + name: data.name, + slug: data.slug, + taxonomy: data.taxonomy, + }, + }, + } + }, + + outputs: { + tag: { + type: 'object', + description: 'The created tag', + properties: { + id: { type: 'number', description: 'Tag ID' }, + count: { type: 'number', description: 'Number of posts with this tag' }, + description: { type: 'string', description: 'Tag description' }, + link: { type: 'string', description: 'Tag archive URL' }, + name: { type: 'string', description: 'Tag name' }, + slug: { type: 'string', description: 'Tag slug' }, + taxonomy: { type: 'string', description: 'Taxonomy name' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/delete_comment.ts b/apps/sim/tools/wordpress/delete_comment.ts new file mode 100644 index 00000000000..ee645161809 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_comment.ts @@ -0,0 +1,116 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressDeleteCommentParams, WordPressDeleteCommentResponse } from './types' + +export const deleteCommentTool: ToolConfig< + WordPressDeleteCommentParams, + WordPressDeleteCommentResponse +> = { + id: 'wordpress_delete_comment', + name: 'WordPress Delete Comment', + description: 'Delete a comment from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + commentId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the comment to delete', + }, + force: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Bypass trash and force delete permanently', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const forceParam = params.force ? '?force=true' : '' + return `${baseUrl}/wp-json/wp/v2/comments/${params.commentId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + deleted: data.deleted || true, + comment: { + id: data.id || data.previous?.id, + post: data.post || data.previous?.post, + parent: data.parent || data.previous?.parent, + author: data.author || data.previous?.author, + author_name: data.author_name || data.previous?.author_name, + author_email: data.author_email || data.previous?.author_email, + author_url: data.author_url || data.previous?.author_url, + date: data.date || data.previous?.date, + content: data.content || data.previous?.content, + link: data.link || data.previous?.link, + status: data.status || data.previous?.status || 'trash', + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the comment was deleted', + }, + comment: { + type: 'object', + description: 'The deleted comment', + properties: { + id: { type: 'number', description: 'Comment ID' }, + post: { type: 'number', description: 'Post ID' }, + parent: { type: 'number', description: 'Parent comment ID' }, + author: { type: 'number', description: 'Author user ID' }, + author_name: { type: 'string', description: 'Author display name' }, + author_email: { type: 'string', description: 'Author email' }, + author_url: { type: 'string', description: 'Author URL' }, + date: { type: 'string', description: 'Comment date' }, + content: { type: 'object', description: 'Comment content object' }, + link: { type: 'string', description: 'Comment permalink' }, + status: { type: 'string', description: 'Comment status' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/delete_media.ts b/apps/sim/tools/wordpress/delete_media.ts new file mode 100644 index 00000000000..19046bb61b8 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_media.ts @@ -0,0 +1,116 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressDeleteMediaParams, WordPressDeleteMediaResponse } from './types' + +export const deleteMediaTool: ToolConfig = + { + id: 'wordpress_delete_media', + name: 'WordPress Delete Media', + description: 'Delete a media item from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + mediaId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the media item to delete', + }, + force: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Force delete (media has no trash, so deletion is permanent)', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + // Media deletion requires force=true to actually delete + return `${baseUrl}/wp-json/wp/v2/media/${params.mediaId}?force=true` + }, + method: 'DELETE', + headers: (params) => { + const credentials = Buffer.from( + `${params.username}:${params.applicationPassword}` + ).toString('base64') + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + deleted: data.deleted || true, + media: { + id: data.id || data.previous?.id, + date: data.date || data.previous?.date, + slug: data.slug || data.previous?.slug, + type: data.type || data.previous?.type, + link: data.link || data.previous?.link, + title: data.title || data.previous?.title, + caption: data.caption || data.previous?.caption, + alt_text: data.alt_text || data.previous?.alt_text, + media_type: data.media_type || data.previous?.media_type, + mime_type: data.mime_type || data.previous?.mime_type, + source_url: data.source_url || data.previous?.source_url, + media_details: data.media_details || data.previous?.media_details, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the media was deleted', + }, + media: { + type: 'object', + description: 'The deleted media item', + properties: { + id: { type: 'number', description: 'Media ID' }, + date: { type: 'string', description: 'Upload date' }, + slug: { type: 'string', description: 'Media slug' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Media page URL' }, + title: { type: 'object', description: 'Media title object' }, + caption: { type: 'object', description: 'Media caption object' }, + alt_text: { type: 'string', description: 'Alt text' }, + media_type: { type: 'string', description: 'Media type (image, video, etc.)' }, + mime_type: { type: 'string', description: 'MIME type' }, + source_url: { type: 'string', description: 'Direct URL to the media file' }, + media_details: { type: 'object', description: 'Media details (dimensions, etc.)' }, + }, + }, + }, + } diff --git a/apps/sim/tools/wordpress/delete_page.ts b/apps/sim/tools/wordpress/delete_page.ts new file mode 100644 index 00000000000..b41b22f24c0 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_page.ts @@ -0,0 +1,119 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressDeletePageParams, WordPressDeletePageResponse } from './types' + +export const deletePageTool: ToolConfig = { + id: 'wordpress_delete_page', + name: 'WordPress Delete Page', + description: 'Delete a page from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + pageId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to delete', + }, + force: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Bypass trash and force delete permanently', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const forceParam = params.force ? '?force=true' : '' + return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + deleted: data.deleted || true, + page: { + id: data.id || data.previous?.id, + date: data.date || data.previous?.date, + modified: data.modified || data.previous?.modified, + slug: data.slug || data.previous?.slug, + status: data.status || data.previous?.status || 'trash', + type: data.type || data.previous?.type, + link: data.link || data.previous?.link, + title: data.title || data.previous?.title, + content: data.content || data.previous?.content, + excerpt: data.excerpt || data.previous?.excerpt, + author: data.author || data.previous?.author, + featured_media: data.featured_media || data.previous?.featured_media, + parent: data.parent || data.previous?.parent, + menu_order: data.menu_order || data.previous?.menu_order, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the page was deleted', + }, + page: { + type: 'object', + description: 'The deleted page', + properties: { + id: { type: 'number', description: 'Page ID' }, + date: { type: 'string', description: 'Page creation date' }, + modified: { type: 'string', description: 'Page modification date' }, + slug: { type: 'string', description: 'Page slug' }, + status: { type: 'string', description: 'Page status' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Page URL' }, + title: { type: 'object', description: 'Page title object' }, + content: { type: 'object', description: 'Page content object' }, + excerpt: { type: 'object', description: 'Page excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menu_order: { type: 'number', description: 'Menu order' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/delete_post.ts b/apps/sim/tools/wordpress/delete_post.ts new file mode 100644 index 00000000000..b679a478c4d --- /dev/null +++ b/apps/sim/tools/wordpress/delete_post.ts @@ -0,0 +1,119 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressDeletePostParams, WordPressDeletePostResponse } from './types' + +export const deletePostTool: ToolConfig = { + id: 'wordpress_delete_post', + name: 'WordPress Delete Post', + description: 'Delete a blog post from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + postId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the post to delete', + }, + force: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Bypass trash and force delete permanently', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const forceParam = params.force ? '?force=true' : '' + return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + deleted: data.deleted || true, + post: { + id: data.id || data.previous?.id, + date: data.date || data.previous?.date, + modified: data.modified || data.previous?.modified, + slug: data.slug || data.previous?.slug, + status: data.status || data.previous?.status || 'trash', + type: data.type || data.previous?.type, + link: data.link || data.previous?.link, + title: data.title || data.previous?.title, + content: data.content || data.previous?.content, + excerpt: data.excerpt || data.previous?.excerpt, + author: data.author || data.previous?.author, + featured_media: data.featured_media || data.previous?.featured_media, + categories: data.categories || data.previous?.categories || [], + tags: data.tags || data.previous?.tags || [], + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the post was deleted', + }, + post: { + type: 'object', + description: 'The deleted post', + properties: { + id: { type: 'number', description: 'Post ID' }, + date: { type: 'string', description: 'Post creation date' }, + modified: { type: 'string', description: 'Post modification date' }, + slug: { type: 'string', description: 'Post slug' }, + status: { type: 'string', description: 'Post status' }, + type: { type: 'string', description: 'Post type' }, + link: { type: 'string', description: 'Post URL' }, + title: { type: 'object', description: 'Post title object' }, + content: { type: 'object', description: 'Post content object' }, + excerpt: { type: 'object', description: 'Post excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + categories: { type: 'array', description: 'Category IDs' }, + tags: { type: 'array', description: 'Tag IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/get_current_user.ts b/apps/sim/tools/wordpress/get_current_user.ts new file mode 100644 index 00000000000..1ff83a439bd --- /dev/null +++ b/apps/sim/tools/wordpress/get_current_user.ts @@ -0,0 +1,100 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressGetCurrentUserParams, WordPressGetCurrentUserResponse } from './types' + +export const getCurrentUserTool: ToolConfig< + WordPressGetCurrentUserParams, + WordPressGetCurrentUserResponse +> = { + id: 'wordpress_get_current_user', + name: 'WordPress Get Current User', + description: 'Get information about the currently authenticated WordPress user', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/users/me` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + user: { + id: data.id, + username: data.username, + name: data.name, + first_name: data.first_name, + last_name: data.last_name, + email: data.email, + url: data.url, + description: data.description, + link: data.link, + slug: data.slug, + roles: data.roles || [], + avatar_urls: data.avatar_urls, + }, + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'The current user', + properties: { + id: { type: 'number', description: 'User ID' }, + username: { type: 'string', description: 'Username' }, + name: { type: 'string', description: 'Display name' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + url: { type: 'string', description: 'User website URL' }, + description: { type: 'string', description: 'User bio' }, + link: { type: 'string', description: 'Author archive URL' }, + slug: { type: 'string', description: 'User slug' }, + roles: { type: 'array', description: 'User roles' }, + avatar_urls: { type: 'object', description: 'Avatar URLs at different sizes' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/get_media.ts b/apps/sim/tools/wordpress/get_media.ts new file mode 100644 index 00000000000..5f442e23e0a --- /dev/null +++ b/apps/sim/tools/wordpress/get_media.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressGetMediaParams, WordPressGetMediaResponse } from './types' + +export const getMediaTool: ToolConfig = { + id: 'wordpress_get_media', + name: 'WordPress Get Media', + description: 'Get a single media item from WordPress by ID', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + mediaId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the media item to retrieve', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/media/${params.mediaId}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + media: { + id: data.id, + date: data.date, + slug: data.slug, + type: data.type, + link: data.link, + title: data.title, + caption: data.caption, + alt_text: data.alt_text, + media_type: data.media_type, + mime_type: data.mime_type, + source_url: data.source_url, + media_details: data.media_details, + }, + }, + } + }, + + outputs: { + media: { + type: 'object', + description: 'The retrieved media item', + properties: { + id: { type: 'number', description: 'Media ID' }, + date: { type: 'string', description: 'Upload date' }, + slug: { type: 'string', description: 'Media slug' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Media page URL' }, + title: { type: 'object', description: 'Media title object' }, + caption: { type: 'object', description: 'Media caption object' }, + alt_text: { type: 'string', description: 'Alt text' }, + media_type: { type: 'string', description: 'Media type (image, video, etc.)' }, + mime_type: { type: 'string', description: 'MIME type' }, + source_url: { type: 'string', description: 'Direct URL to the media file' }, + media_details: { type: 'object', description: 'Media details (dimensions, etc.)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/get_page.ts b/apps/sim/tools/wordpress/get_page.ts new file mode 100644 index 00000000000..ae660f5c0bb --- /dev/null +++ b/apps/sim/tools/wordpress/get_page.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressGetPageParams, WordPressGetPageResponse } from './types' + +export const getPageTool: ToolConfig = { + id: 'wordpress_get_page', + name: 'WordPress Get Page', + description: 'Get a single page from WordPress by ID', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + pageId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to retrieve', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + page: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + parent: data.parent, + menu_order: data.menu_order, + }, + }, + } + }, + + outputs: { + page: { + type: 'object', + description: 'The retrieved page', + properties: { + id: { type: 'number', description: 'Page ID' }, + date: { type: 'string', description: 'Page creation date' }, + modified: { type: 'string', description: 'Page modification date' }, + slug: { type: 'string', description: 'Page slug' }, + status: { type: 'string', description: 'Page status' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Page URL' }, + title: { type: 'object', description: 'Page title object' }, + content: { type: 'object', description: 'Page content object' }, + excerpt: { type: 'object', description: 'Page excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menu_order: { type: 'number', description: 'Menu order' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/get_post.ts b/apps/sim/tools/wordpress/get_post.ts new file mode 100644 index 00000000000..0ba20be687d --- /dev/null +++ b/apps/sim/tools/wordpress/get_post.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressGetPostParams, WordPressGetPostResponse } from './types' + +export const getPostTool: ToolConfig = { + id: 'wordpress_get_post', + name: 'WordPress Get Post', + description: 'Get a single blog post from WordPress by ID', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + postId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the post to retrieve', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + post: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + categories: data.categories || [], + tags: data.tags || [], + }, + }, + } + }, + + outputs: { + post: { + type: 'object', + description: 'The retrieved post', + properties: { + id: { type: 'number', description: 'Post ID' }, + date: { type: 'string', description: 'Post creation date' }, + modified: { type: 'string', description: 'Post modification date' }, + slug: { type: 'string', description: 'Post slug' }, + status: { type: 'string', description: 'Post status' }, + type: { type: 'string', description: 'Post type' }, + link: { type: 'string', description: 'Post URL' }, + title: { type: 'object', description: 'Post title object' }, + content: { type: 'object', description: 'Post content object' }, + excerpt: { type: 'object', description: 'Post excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + categories: { type: 'array', description: 'Category IDs' }, + tags: { type: 'array', description: 'Tag IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/get_user.ts b/apps/sim/tools/wordpress/get_user.ts new file mode 100644 index 00000000000..5b88ebd1c2d --- /dev/null +++ b/apps/sim/tools/wordpress/get_user.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressGetUserParams, WordPressGetUserResponse } from './types' + +export const getUserTool: ToolConfig = { + id: 'wordpress_get_user', + name: 'WordPress Get User', + description: 'Get a specific user from WordPress by ID', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + userId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the user to retrieve', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/users/${params.userId}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + user: { + id: data.id, + username: data.username, + name: data.name, + first_name: data.first_name, + last_name: data.last_name, + email: data.email, + url: data.url, + description: data.description, + link: data.link, + slug: data.slug, + roles: data.roles || [], + avatar_urls: data.avatar_urls, + }, + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'The retrieved user', + properties: { + id: { type: 'number', description: 'User ID' }, + username: { type: 'string', description: 'Username' }, + name: { type: 'string', description: 'Display name' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + url: { type: 'string', description: 'User website URL' }, + description: { type: 'string', description: 'User bio' }, + link: { type: 'string', description: 'Author archive URL' }, + slug: { type: 'string', description: 'User slug' }, + roles: { type: 'array', description: 'User roles' }, + avatar_urls: { type: 'object', description: 'Avatar URLs at different sizes' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/index.ts b/apps/sim/tools/wordpress/index.ts new file mode 100644 index 00000000000..3889208a544 --- /dev/null +++ b/apps/sim/tools/wordpress/index.ts @@ -0,0 +1,69 @@ +// WordPress tools exports +import { createCategoryTool } from '@/tools/wordpress/create_category' +import { createCommentTool } from '@/tools/wordpress/create_comment' +import { createPageTool } from '@/tools/wordpress/create_page' +import { createPostTool } from '@/tools/wordpress/create_post' +import { createTagTool } from '@/tools/wordpress/create_tag' +import { deleteCommentTool } from '@/tools/wordpress/delete_comment' +import { deleteMediaTool } from '@/tools/wordpress/delete_media' +import { deletePageTool } from '@/tools/wordpress/delete_page' +import { deletePostTool } from '@/tools/wordpress/delete_post' +import { getCurrentUserTool } from '@/tools/wordpress/get_current_user' +import { getMediaTool } from '@/tools/wordpress/get_media' +import { getPageTool } from '@/tools/wordpress/get_page' +import { getPostTool } from '@/tools/wordpress/get_post' +import { getUserTool } from '@/tools/wordpress/get_user' +import { listCategoriesTool } from '@/tools/wordpress/list_categories' +import { listCommentsTool } from '@/tools/wordpress/list_comments' +import { listMediaTool } from '@/tools/wordpress/list_media' +import { listPagesTool } from '@/tools/wordpress/list_pages' +import { listPostsTool } from '@/tools/wordpress/list_posts' +import { listTagsTool } from '@/tools/wordpress/list_tags' +import { listUsersTool } from '@/tools/wordpress/list_users' +import { searchContentTool } from '@/tools/wordpress/search_content' +import { updateCommentTool } from '@/tools/wordpress/update_comment' +import { updatePageTool } from '@/tools/wordpress/update_page' +import { updatePostTool } from '@/tools/wordpress/update_post' +import { uploadMediaTool } from '@/tools/wordpress/upload_media' + +// Post operations +export const wordpressCreatePostTool = createPostTool +export const wordpressUpdatePostTool = updatePostTool +export const wordpressDeletePostTool = deletePostTool +export const wordpressGetPostTool = getPostTool +export const wordpressListPostsTool = listPostsTool + +// Page operations +export const wordpressCreatePageTool = createPageTool +export const wordpressUpdatePageTool = updatePageTool +export const wordpressDeletePageTool = deletePageTool +export const wordpressGetPageTool = getPageTool +export const wordpressListPagesTool = listPagesTool + +// Media operations +export const wordpressUploadMediaTool = uploadMediaTool +export const wordpressGetMediaTool = getMediaTool +export const wordpressListMediaTool = listMediaTool +export const wordpressDeleteMediaTool = deleteMediaTool + +// Comment operations +export const wordpressCreateCommentTool = createCommentTool +export const wordpressListCommentsTool = listCommentsTool +export const wordpressUpdateCommentTool = updateCommentTool +export const wordpressDeleteCommentTool = deleteCommentTool + +// Category operations +export const wordpressCreateCategoryTool = createCategoryTool +export const wordpressListCategoriesTool = listCategoriesTool + +// Tag operations +export const wordpressCreateTagTool = createTagTool +export const wordpressListTagsTool = listTagsTool + +// User operations +export const wordpressGetCurrentUserTool = getCurrentUserTool +export const wordpressListUsersTool = listUsersTool +export const wordpressGetUserTool = getUserTool + +// Search operations +export const wordpressSearchContentTool = searchContentTool diff --git a/apps/sim/tools/wordpress/list_categories.ts b/apps/sim/tools/wordpress/list_categories.ts new file mode 100644 index 00000000000..4dbeb4277c5 --- /dev/null +++ b/apps/sim/tools/wordpress/list_categories.ts @@ -0,0 +1,160 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListCategoriesParams, WordPressListCategoriesResponse } from './types' + +export const listCategoriesTool: ToolConfig< + WordPressListCategoriesParams, + WordPressListCategoriesResponse +> = { + id: 'wordpress_list_categories', + name: 'WordPress List Categories', + description: 'List categories from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of categories per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter categories', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Filter by parent category ID', + }, + hideEmpty: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Hide categories with no posts', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: id, name, slug, count', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.search) queryParams.append('search', params.search) + if (params.parent !== undefined) queryParams.append('parent', String(params.parent)) + if (params.hideEmpty !== undefined) queryParams.append('hide_empty', String(params.hideEmpty)) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/categories${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + categories: data.map((cat: any) => ({ + id: cat.id, + count: cat.count, + description: cat.description, + link: cat.link, + name: cat.name, + slug: cat.slug, + taxonomy: cat.taxonomy, + parent: cat.parent, + })), + total, + totalPages, + }, + } + }, + + outputs: { + categories: { + type: 'array', + description: 'List of categories', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Category ID' }, + count: { type: 'number', description: 'Number of posts in this category' }, + description: { type: 'string', description: 'Category description' }, + link: { type: 'string', description: 'Category archive URL' }, + name: { type: 'string', description: 'Category name' }, + slug: { type: 'string', description: 'Category slug' }, + taxonomy: { type: 'string', description: 'Taxonomy name' }, + parent: { type: 'number', description: 'Parent category ID' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of categories', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_comments.ts b/apps/sim/tools/wordpress/list_comments.ts new file mode 100644 index 00000000000..bd5bed20283 --- /dev/null +++ b/apps/sim/tools/wordpress/list_comments.ts @@ -0,0 +1,166 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListCommentsParams, WordPressListCommentsResponse } from './types' + +export const listCommentsTool: ToolConfig< + WordPressListCommentsParams, + WordPressListCommentsResponse +> = { + id: 'wordpress_list_comments', + name: 'WordPress List Comments', + description: 'List comments from WordPress with optional filters', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of comments per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + postId: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Filter by post ID', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by comment status: approved, hold, spam, trash', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter comments', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: date, id, parent', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.postId) queryParams.append('post', String(params.postId)) + if (params.status) queryParams.append('status', params.status) + if (params.search) queryParams.append('search', params.search) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/comments${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + comments: data.map((comment: any) => ({ + id: comment.id, + post: comment.post, + parent: comment.parent, + author: comment.author, + author_name: comment.author_name, + author_email: comment.author_email, + author_url: comment.author_url, + date: comment.date, + content: comment.content, + link: comment.link, + status: comment.status, + })), + total, + totalPages, + }, + } + }, + + outputs: { + comments: { + type: 'array', + description: 'List of comments', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Comment ID' }, + post: { type: 'number', description: 'Post ID' }, + parent: { type: 'number', description: 'Parent comment ID' }, + author: { type: 'number', description: 'Author user ID' }, + author_name: { type: 'string', description: 'Author display name' }, + author_email: { type: 'string', description: 'Author email' }, + author_url: { type: 'string', description: 'Author URL' }, + date: { type: 'string', description: 'Comment date' }, + content: { type: 'object', description: 'Comment content object' }, + link: { type: 'string', description: 'Comment permalink' }, + status: { type: 'string', description: 'Comment status' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of comments', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_media.ts b/apps/sim/tools/wordpress/list_media.ts new file mode 100644 index 00000000000..8f67a511620 --- /dev/null +++ b/apps/sim/tools/wordpress/list_media.ts @@ -0,0 +1,165 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListMediaParams, WordPressListMediaResponse } from './types' + +export const listMediaTool: ToolConfig = { + id: 'wordpress_list_media', + name: 'WordPress List Media', + description: 'List media items from the WordPress media library', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of media items per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter media', + }, + mediaType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by media type: image, video, audio, application', + }, + mimeType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by specific MIME type (e.g., image/jpeg)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: date, id, title, slug', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.search) queryParams.append('search', params.search) + if (params.mediaType) queryParams.append('media_type', params.mediaType) + if (params.mimeType) queryParams.append('mime_type', params.mimeType) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/media${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + media: data.map((item: any) => ({ + id: item.id, + date: item.date, + slug: item.slug, + type: item.type, + link: item.link, + title: item.title, + caption: item.caption, + alt_text: item.alt_text, + media_type: item.media_type, + mime_type: item.mime_type, + source_url: item.source_url, + media_details: item.media_details, + })), + total, + totalPages, + }, + } + }, + + outputs: { + media: { + type: 'array', + description: 'List of media items', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Media ID' }, + date: { type: 'string', description: 'Upload date' }, + slug: { type: 'string', description: 'Media slug' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Media page URL' }, + title: { type: 'object', description: 'Media title object' }, + caption: { type: 'object', description: 'Media caption object' }, + alt_text: { type: 'string', description: 'Alt text' }, + media_type: { type: 'string', description: 'Media type (image, video, etc.)' }, + mime_type: { type: 'string', description: 'MIME type' }, + source_url: { type: 'string', description: 'Direct URL to the media file' }, + media_details: { type: 'object', description: 'Media details (dimensions, etc.)' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of media items', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_pages.ts b/apps/sim/tools/wordpress/list_pages.ts new file mode 100644 index 00000000000..8e14a2f1414 --- /dev/null +++ b/apps/sim/tools/wordpress/list_pages.ts @@ -0,0 +1,169 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListPagesParams, WordPressListPagesResponse } from './types' + +export const listPagesTool: ToolConfig = { + id: 'wordpress_list_pages', + name: 'WordPress List Pages', + description: 'List pages from WordPress with optional filters', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of pages per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Page status filter: publish, draft, pending, private', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Filter by parent page ID', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter pages', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: date, id, title, slug, modified, menu_order', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.status) queryParams.append('status', params.status) + if (params.parent !== undefined) queryParams.append('parent', String(params.parent)) + if (params.search) queryParams.append('search', params.search) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/pages${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + pages: data.map((page: any) => ({ + id: page.id, + date: page.date, + modified: page.modified, + slug: page.slug, + status: page.status, + type: page.type, + link: page.link, + title: page.title, + content: page.content, + excerpt: page.excerpt, + author: page.author, + featured_media: page.featured_media, + parent: page.parent, + menu_order: page.menu_order, + })), + total, + totalPages, + }, + } + }, + + outputs: { + pages: { + type: 'array', + description: 'List of pages', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Page ID' }, + date: { type: 'string', description: 'Page creation date' }, + modified: { type: 'string', description: 'Page modification date' }, + slug: { type: 'string', description: 'Page slug' }, + status: { type: 'string', description: 'Page status' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Page URL' }, + title: { type: 'object', description: 'Page title object' }, + content: { type: 'object', description: 'Page content object' }, + excerpt: { type: 'object', description: 'Page excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menu_order: { type: 'number', description: 'Menu order' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of pages', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_posts.ts b/apps/sim/tools/wordpress/list_posts.ts new file mode 100644 index 00000000000..2cf88e4ad11 --- /dev/null +++ b/apps/sim/tools/wordpress/list_posts.ts @@ -0,0 +1,197 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListPostsParams, WordPressListPostsResponse } from './types' + +export const listPostsTool: ToolConfig = { + id: 'wordpress_list_posts', + name: 'WordPress List Posts', + description: 'List blog posts from WordPress with optional filters', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of posts per page (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Post status filter: publish, draft, pending, private', + }, + author: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Filter by author ID', + }, + categories: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated category IDs to filter by', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated tag IDs to filter by', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter posts', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: date, id, title, slug, modified', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.status) queryParams.append('status', params.status) + if (params.author) queryParams.append('author', String(params.author)) + if (params.search) queryParams.append('search', params.search) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + if (params.categories) { + const catIds = params.categories + .split(',') + .map((id: string) => id.trim()) + .filter((id: string) => id.length > 0) + queryParams.append('categories', catIds.join(',')) + } + + if (params.tags) { + const tagIds = params.tags + .split(',') + .map((id: string) => id.trim()) + .filter((id: string) => id.length > 0) + queryParams.append('tags', tagIds.join(',')) + } + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/posts${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + posts: data.map((post: any) => ({ + id: post.id, + date: post.date, + modified: post.modified, + slug: post.slug, + status: post.status, + type: post.type, + link: post.link, + title: post.title, + content: post.content, + excerpt: post.excerpt, + author: post.author, + featured_media: post.featured_media, + categories: post.categories || [], + tags: post.tags || [], + })), + total, + totalPages, + }, + } + }, + + outputs: { + posts: { + type: 'array', + description: 'List of posts', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Post ID' }, + date: { type: 'string', description: 'Post creation date' }, + modified: { type: 'string', description: 'Post modification date' }, + slug: { type: 'string', description: 'Post slug' }, + status: { type: 'string', description: 'Post status' }, + type: { type: 'string', description: 'Post type' }, + link: { type: 'string', description: 'Post URL' }, + title: { type: 'object', description: 'Post title object' }, + content: { type: 'object', description: 'Post content object' }, + excerpt: { type: 'object', description: 'Post excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + categories: { type: 'array', description: 'Category IDs' }, + tags: { type: 'array', description: 'Tag IDs' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of posts', + }, + totalPages: { + type: 'number', + description: 'Total number of pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_tags.ts b/apps/sim/tools/wordpress/list_tags.ts new file mode 100644 index 00000000000..55657c152ad --- /dev/null +++ b/apps/sim/tools/wordpress/list_tags.ts @@ -0,0 +1,148 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListTagsParams, WordPressListTagsResponse } from './types' + +export const listTagsTool: ToolConfig = { + id: 'wordpress_list_tags', + name: 'WordPress List Tags', + description: 'List tags from WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of tags per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter tags', + }, + hideEmpty: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Hide tags with no posts', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: id, name, slug, count', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.search) queryParams.append('search', params.search) + if (params.hideEmpty !== undefined) queryParams.append('hide_empty', String(params.hideEmpty)) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/tags${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + tags: data.map((tag: any) => ({ + id: tag.id, + count: tag.count, + description: tag.description, + link: tag.link, + name: tag.name, + slug: tag.slug, + taxonomy: tag.taxonomy, + })), + total, + totalPages, + }, + } + }, + + outputs: { + tags: { + type: 'array', + description: 'List of tags', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Tag ID' }, + count: { type: 'number', description: 'Number of posts with this tag' }, + description: { type: 'string', description: 'Tag description' }, + link: { type: 'string', description: 'Tag archive URL' }, + name: { type: 'string', description: 'Tag name' }, + slug: { type: 'string', description: 'Tag slug' }, + taxonomy: { type: 'string', description: 'Taxonomy name' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of tags', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/list_users.ts b/apps/sim/tools/wordpress/list_users.ts new file mode 100644 index 00000000000..fa764a07386 --- /dev/null +++ b/apps/sim/tools/wordpress/list_users.ts @@ -0,0 +1,158 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressListUsersParams, WordPressListUsersResponse } from './types' + +export const listUsersTool: ToolConfig = { + id: 'wordpress_list_users', + name: 'WordPress List Users', + description: 'List users from WordPress (requires admin privileges)', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of users per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter users', + }, + roles: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated role names to filter by', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order by field: id, name, slug, email, registered_date', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.search) queryParams.append('search', params.search) + if (params.roles) queryParams.append('roles', params.roles) + if (params.orderBy) queryParams.append('orderby', params.orderBy) + if (params.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${baseUrl}/wp-json/wp/v2/users${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + users: data.map((user: any) => ({ + id: user.id, + username: user.username, + name: user.name, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + url: user.url, + description: user.description, + link: user.link, + slug: user.slug, + roles: user.roles || [], + avatar_urls: user.avatar_urls, + })), + total, + totalPages, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'List of users', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'User ID' }, + username: { type: 'string', description: 'Username' }, + name: { type: 'string', description: 'Display name' }, + first_name: { type: 'string', description: 'First name' }, + last_name: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + url: { type: 'string', description: 'User website URL' }, + description: { type: 'string', description: 'User bio' }, + link: { type: 'string', description: 'Author archive URL' }, + slug: { type: 'string', description: 'User slug' }, + roles: { type: 'array', description: 'User roles' }, + avatar_urls: { type: 'object', description: 'Avatar URLs at different sizes' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of users', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/search_content.ts b/apps/sim/tools/wordpress/search_content.ts new file mode 100644 index 00000000000..f60ce5b3323 --- /dev/null +++ b/apps/sim/tools/wordpress/search_content.ts @@ -0,0 +1,139 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressSearchContentParams, WordPressSearchContentResponse } from './types' + +export const searchContentTool: ToolConfig< + WordPressSearchContentParams, + WordPressSearchContentResponse +> = { + id: 'wordpress_search_content', + name: 'WordPress Search Content', + description: 'Search across all content types in WordPress (posts, pages, media)', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of results per request (default: 10, max: 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + type: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by content type: post, page, attachment', + }, + subtype: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by post type slug (e.g., post, page)', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + const queryParams = new URLSearchParams() + + queryParams.append('search', params.query) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + if (params.type) queryParams.append('type', params.type) + if (params.subtype) queryParams.append('subtype', params.subtype) + + return `${baseUrl}/wp-json/wp/v2/search?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + const total = Number.parseInt(response.headers.get('X-WP-Total') || '0', 10) + const totalPages = Number.parseInt(response.headers.get('X-WP-TotalPages') || '0', 10) + + return { + success: true, + output: { + results: data.map((result: any) => ({ + id: result.id, + title: result.title, + url: result.url, + type: result.type, + subtype: result.subtype, + })), + total, + totalPages, + }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Search results', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Content ID' }, + title: { type: 'string', description: 'Content title' }, + url: { type: 'string', description: 'Content URL' }, + type: { type: 'string', description: 'Content type (post, page, attachment)' }, + subtype: { type: 'string', description: 'Post type slug' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of results', + }, + totalPages: { + type: 'number', + description: 'Total number of result pages', + }, + }, +} diff --git a/apps/sim/tools/wordpress/types.ts b/apps/sim/tools/wordpress/types.ts new file mode 100644 index 00000000000..3758040fa1e --- /dev/null +++ b/apps/sim/tools/wordpress/types.ts @@ -0,0 +1,630 @@ +// Common types for WordPress REST API tools +import type { ToolResponse } from '@/tools/types' + +// Common parameters for all WordPress tools (Application Password auth) +export interface WordPressBaseParams { + siteUrl: string // WordPress site URL (e.g., https://example.com) + username: string // WordPress username + applicationPassword: string // Application Password +} + +// Post status types +export type PostStatus = 'publish' | 'draft' | 'pending' | 'private' | 'future' + +// Comment status types +export type CommentStatus = 'approved' | 'hold' | 'spam' | 'trash' + +// ============================================ +// POST OPERATIONS +// ============================================ + +// Create Post +export interface WordPressCreatePostParams extends WordPressBaseParams { + title: string + content?: string + status?: PostStatus + excerpt?: string + categories?: string // Comma-separated category IDs + tags?: string // Comma-separated tag IDs + featuredMedia?: number + slug?: string +} + +export interface WordPressPost { + id: number + date: string + modified: string + slug: string + status: PostStatus + type: string + link: string + title: { + rendered: string + } + content: { + rendered: string + } + excerpt: { + rendered: string + } + author: number + featured_media: number + categories: number[] + tags: number[] +} + +export interface WordPressCreatePostResponse extends ToolResponse { + output: { + post: WordPressPost + } +} + +// Update Post +export interface WordPressUpdatePostParams extends WordPressBaseParams { + postId: number + title?: string + content?: string + status?: PostStatus + excerpt?: string + categories?: string + tags?: string + featuredMedia?: number + slug?: string +} + +export interface WordPressUpdatePostResponse extends ToolResponse { + output: { + post: WordPressPost + } +} + +// Delete Post +export interface WordPressDeletePostParams extends WordPressBaseParams { + postId: number + force?: boolean // Bypass trash and force delete +} + +export interface WordPressDeletePostResponse extends ToolResponse { + output: { + deleted: boolean + post: WordPressPost + } +} + +// Get Post +export interface WordPressGetPostParams extends WordPressBaseParams { + postId: number +} + +export interface WordPressGetPostResponse extends ToolResponse { + output: { + post: WordPressPost + } +} + +// List Posts +export interface WordPressListPostsParams extends WordPressBaseParams { + perPage?: number + page?: number + status?: PostStatus + author?: number + categories?: string + tags?: string + search?: string + orderBy?: 'date' | 'id' | 'title' | 'slug' | 'modified' + order?: 'asc' | 'desc' +} + +export interface WordPressListPostsResponse extends ToolResponse { + output: { + posts: WordPressPost[] + total: number + totalPages: number + } +} + +// Search Posts +export interface WordPressSearchPostsParams extends WordPressBaseParams { + query: string + perPage?: number + page?: number +} + +export interface WordPressSearchPostsResponse extends ToolResponse { + output: { + posts: WordPressPost[] + total: number + totalPages: number + } +} + +// ============================================ +// PAGE OPERATIONS +// ============================================ + +// Create Page +export interface WordPressCreatePageParams extends WordPressBaseParams { + title: string + content?: string + status?: PostStatus + excerpt?: string + parent?: number + menuOrder?: number + featuredMedia?: number + slug?: string +} + +export interface WordPressPage { + id: number + date: string + modified: string + slug: string + status: PostStatus + type: string + link: string + title: { + rendered: string + } + content: { + rendered: string + } + excerpt: { + rendered: string + } + author: number + featured_media: number + parent: number + menu_order: number +} + +export interface WordPressCreatePageResponse extends ToolResponse { + output: { + page: WordPressPage + } +} + +// Update Page +export interface WordPressUpdatePageParams extends WordPressBaseParams { + pageId: number + title?: string + content?: string + status?: PostStatus + excerpt?: string + parent?: number + menuOrder?: number + featuredMedia?: number + slug?: string +} + +export interface WordPressUpdatePageResponse extends ToolResponse { + output: { + page: WordPressPage + } +} + +// Delete Page +export interface WordPressDeletePageParams extends WordPressBaseParams { + pageId: number + force?: boolean +} + +export interface WordPressDeletePageResponse extends ToolResponse { + output: { + deleted: boolean + page: WordPressPage + } +} + +// Get Page +export interface WordPressGetPageParams extends WordPressBaseParams { + pageId: number +} + +export interface WordPressGetPageResponse extends ToolResponse { + output: { + page: WordPressPage + } +} + +// List Pages +export interface WordPressListPagesParams extends WordPressBaseParams { + perPage?: number + page?: number + status?: PostStatus + parent?: number + search?: string + orderBy?: 'date' | 'id' | 'title' | 'slug' | 'modified' | 'menu_order' + order?: 'asc' | 'desc' +} + +export interface WordPressListPagesResponse extends ToolResponse { + output: { + pages: WordPressPage[] + total: number + totalPages: number + } +} + +// ============================================ +// MEDIA OPERATIONS +// ============================================ + +// Upload Media +export interface WordPressUploadMediaParams extends WordPressBaseParams { + file: string // Base64 encoded file data or URL + filename: string + title?: string + caption?: string + altText?: string + description?: string +} + +export interface WordPressMedia { + id: number + date: string + slug: string + type: string + link: string + title: { + rendered: string + } + caption: { + rendered: string + } + alt_text: string + media_type: string + mime_type: string + source_url: string + media_details?: { + width?: number + height?: number + file?: string + } +} + +export interface WordPressUploadMediaResponse extends ToolResponse { + output: { + media: WordPressMedia + } +} + +// Get Media +export interface WordPressGetMediaParams extends WordPressBaseParams { + mediaId: number +} + +export interface WordPressGetMediaResponse extends ToolResponse { + output: { + media: WordPressMedia + } +} + +// List Media +export interface WordPressListMediaParams extends WordPressBaseParams { + perPage?: number + page?: number + search?: string + mediaType?: 'image' | 'video' | 'audio' | 'application' + mimeType?: string + orderBy?: 'date' | 'id' | 'title' | 'slug' + order?: 'asc' | 'desc' +} + +export interface WordPressListMediaResponse extends ToolResponse { + output: { + media: WordPressMedia[] + total: number + totalPages: number + } +} + +// Delete Media +export interface WordPressDeleteMediaParams extends WordPressBaseParams { + mediaId: number + force?: boolean +} + +export interface WordPressDeleteMediaResponse extends ToolResponse { + output: { + deleted: boolean + media: WordPressMedia + } +} + +// ============================================ +// COMMENT OPERATIONS +// ============================================ + +// Create Comment +export interface WordPressCreateCommentParams extends WordPressBaseParams { + postId: number + content: string + parent?: number + authorName?: string + authorEmail?: string + authorUrl?: string +} + +export interface WordPressComment { + id: number + post: number + parent: number + author: number + author_name: string + author_email?: string + author_url: string + date: string + content: { + rendered: string + } + link: string + status: string +} + +export interface WordPressCreateCommentResponse extends ToolResponse { + output: { + comment: WordPressComment + } +} + +// Get Comment +export interface WordPressGetCommentParams extends WordPressBaseParams { + commentId: number +} + +export interface WordPressGetCommentResponse extends ToolResponse { + output: { + comment: WordPressComment + } +} + +// List Comments +export interface WordPressListCommentsParams extends WordPressBaseParams { + perPage?: number + page?: number + postId?: number + status?: CommentStatus + search?: string + orderBy?: 'date' | 'id' | 'parent' + order?: 'asc' | 'desc' +} + +export interface WordPressListCommentsResponse extends ToolResponse { + output: { + comments: WordPressComment[] + total: number + totalPages: number + } +} + +// Update Comment +export interface WordPressUpdateCommentParams extends WordPressBaseParams { + commentId: number + content?: string + status?: CommentStatus +} + +export interface WordPressUpdateCommentResponse extends ToolResponse { + output: { + comment: WordPressComment + } +} + +// Delete Comment +export interface WordPressDeleteCommentParams extends WordPressBaseParams { + commentId: number + force?: boolean +} + +export interface WordPressDeleteCommentResponse extends ToolResponse { + output: { + deleted: boolean + comment: WordPressComment + } +} + +// ============================================ +// TAXONOMY OPERATIONS (Categories & Tags) +// ============================================ + +// Create Category +export interface WordPressCreateCategoryParams extends WordPressBaseParams { + name: string + description?: string + parent?: number + slug?: string +} + +export interface WordPressCategory { + id: number + count: number + description: string + link: string + name: string + slug: string + taxonomy: string + parent: number +} + +export interface WordPressCreateCategoryResponse extends ToolResponse { + output: { + category: WordPressCategory + } +} + +// List Categories +export interface WordPressListCategoriesParams extends WordPressBaseParams { + perPage?: number + page?: number + search?: string + parent?: number + hideEmpty?: boolean + orderBy?: 'id' | 'name' | 'slug' | 'count' + order?: 'asc' | 'desc' +} + +export interface WordPressListCategoriesResponse extends ToolResponse { + output: { + categories: WordPressCategory[] + total: number + totalPages: number + } +} + +// Create Tag +export interface WordPressCreateTagParams extends WordPressBaseParams { + name: string + description?: string + slug?: string +} + +export interface WordPressTag { + id: number + count: number + description: string + link: string + name: string + slug: string + taxonomy: string +} + +export interface WordPressCreateTagResponse extends ToolResponse { + output: { + tag: WordPressTag + } +} + +// List Tags +export interface WordPressListTagsParams extends WordPressBaseParams { + perPage?: number + page?: number + search?: string + hideEmpty?: boolean + orderBy?: 'id' | 'name' | 'slug' | 'count' + order?: 'asc' | 'desc' +} + +export interface WordPressListTagsResponse extends ToolResponse { + output: { + tags: WordPressTag[] + total: number + totalPages: number + } +} + +// ============================================ +// USER OPERATIONS +// ============================================ + +// Get Current User +export interface WordPressGetCurrentUserParams extends WordPressBaseParams {} + +export interface WordPressUser { + id: number + username: string + name: string + first_name: string + last_name: string + email?: string + url: string + description: string + link: string + slug: string + roles: string[] + avatar_urls?: Record +} + +export interface WordPressGetCurrentUserResponse extends ToolResponse { + output: { + user: WordPressUser + } +} + +// List Users +export interface WordPressListUsersParams extends WordPressBaseParams { + perPage?: number + page?: number + search?: string + roles?: string + orderBy?: 'id' | 'name' | 'slug' | 'email' | 'registered_date' + order?: 'asc' | 'desc' +} + +export interface WordPressListUsersResponse extends ToolResponse { + output: { + users: WordPressUser[] + total: number + totalPages: number + } +} + +// Get User +export interface WordPressGetUserParams extends WordPressBaseParams { + userId: number +} + +export interface WordPressGetUserResponse extends ToolResponse { + output: { + user: WordPressUser + } +} + +// ============================================ +// SEARCH OPERATIONS +// ============================================ + +// Search Content +export interface WordPressSearchContentParams extends WordPressBaseParams { + query: string + perPage?: number + page?: number + type?: 'post' | 'page' | 'attachment' + subtype?: string +} + +export interface WordPressSearchResult { + id: number + title: string + url: string + type: string + subtype: string +} + +export interface WordPressSearchContentResponse extends ToolResponse { + output: { + results: WordPressSearchResult[] + total: number + totalPages: number + } +} + +// Union type for all WordPress responses +export type WordPressResponse = + | WordPressCreatePostResponse + | WordPressUpdatePostResponse + | WordPressDeletePostResponse + | WordPressGetPostResponse + | WordPressListPostsResponse + | WordPressSearchPostsResponse + | WordPressCreatePageResponse + | WordPressUpdatePageResponse + | WordPressDeletePageResponse + | WordPressGetPageResponse + | WordPressListPagesResponse + | WordPressUploadMediaResponse + | WordPressGetMediaResponse + | WordPressListMediaResponse + | WordPressDeleteMediaResponse + | WordPressCreateCommentResponse + | WordPressGetCommentResponse + | WordPressListCommentsResponse + | WordPressUpdateCommentResponse + | WordPressDeleteCommentResponse + | WordPressCreateCategoryResponse + | WordPressListCategoriesResponse + | WordPressCreateTagResponse + | WordPressListTagsResponse + | WordPressGetCurrentUserResponse + | WordPressListUsersResponse + | WordPressGetUserResponse + | WordPressSearchContentResponse diff --git a/apps/sim/tools/wordpress/update_comment.ts b/apps/sim/tools/wordpress/update_comment.ts new file mode 100644 index 00000000000..2e5eddb0c45 --- /dev/null +++ b/apps/sim/tools/wordpress/update_comment.ts @@ -0,0 +1,124 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressUpdateCommentParams, WordPressUpdateCommentResponse } from './types' + +export const updateCommentTool: ToolConfig< + WordPressUpdateCommentParams, + WordPressUpdateCommentResponse +> = { + id: 'wordpress_update_comment', + name: 'WordPress Update Comment', + description: 'Update a comment in WordPress (content or status)', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + commentId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the comment to update', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated comment content', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comment status: approved, hold, spam, trash', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/comments/${params.commentId}` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.content) body.content = params.content + if (params.status) body.status = params.status + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + comment: { + id: data.id, + post: data.post, + parent: data.parent, + author: data.author, + author_name: data.author_name, + author_email: data.author_email, + author_url: data.author_url, + date: data.date, + content: data.content, + link: data.link, + status: data.status, + }, + }, + } + }, + + outputs: { + comment: { + type: 'object', + description: 'The updated comment', + properties: { + id: { type: 'number', description: 'Comment ID' }, + post: { type: 'number', description: 'Post ID' }, + parent: { type: 'number', description: 'Parent comment ID' }, + author: { type: 'number', description: 'Author user ID' }, + author_name: { type: 'string', description: 'Author display name' }, + author_email: { type: 'string', description: 'Author email' }, + author_url: { type: 'string', description: 'Author URL' }, + date: { type: 'string', description: 'Comment date' }, + content: { type: 'object', description: 'Comment content object' }, + link: { type: 'string', description: 'Comment permalink' }, + status: { type: 'string', description: 'Comment status' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/update_page.ts b/apps/sim/tools/wordpress/update_page.ts new file mode 100644 index 00000000000..27ae17705fe --- /dev/null +++ b/apps/sim/tools/wordpress/update_page.ts @@ -0,0 +1,169 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressUpdatePageParams, WordPressUpdatePageResponse } from './types' + +export const updatePageTool: ToolConfig = { + id: 'wordpress_update_page', + name: 'WordPress Update Page', + description: 'Update an existing page in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + pageId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page title', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page content (HTML or plain text)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Page status: publish, draft, pending, private', + }, + excerpt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page excerpt', + }, + parent: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Parent page ID for hierarchical pages', + }, + menuOrder: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Order in page menu', + }, + featuredMedia: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Featured image media ID', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the page', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.content) body.content = params.content + if (params.status) body.status = params.status + if (params.excerpt) body.excerpt = params.excerpt + if (params.slug) body.slug = params.slug + if (params.parent !== undefined) body.parent = params.parent + if (params.menuOrder !== undefined) body.menu_order = params.menuOrder + if (params.featuredMedia) body.featured_media = params.featuredMedia + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + page: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + parent: data.parent, + menu_order: data.menu_order, + }, + }, + } + }, + + outputs: { + page: { + type: 'object', + description: 'The updated page', + properties: { + id: { type: 'number', description: 'Page ID' }, + date: { type: 'string', description: 'Page creation date' }, + modified: { type: 'string', description: 'Page modification date' }, + slug: { type: 'string', description: 'Page slug' }, + status: { type: 'string', description: 'Page status' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Page URL' }, + title: { type: 'object', description: 'Page title object' }, + content: { type: 'object', description: 'Page content object' }, + excerpt: { type: 'object', description: 'Page excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + parent: { type: 'number', description: 'Parent page ID' }, + menu_order: { type: 'number', description: 'Menu order' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/update_post.ts b/apps/sim/tools/wordpress/update_post.ts new file mode 100644 index 00000000000..75753328a66 --- /dev/null +++ b/apps/sim/tools/wordpress/update_post.ts @@ -0,0 +1,181 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressUpdatePostParams, WordPressUpdatePostResponse } from './types' + +export const updatePostTool: ToolConfig = { + id: 'wordpress_update_post', + name: 'WordPress Update Post', + description: 'Update an existing blog post in WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + postId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the post to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post title', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post content (HTML or plain text)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Post status: publish, draft, pending, private, or future', + }, + excerpt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Post excerpt', + }, + categories: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated category IDs', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated tag IDs', + }, + featuredMedia: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Featured image media ID', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL slug for the post', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( + 'base64' + ) + return { + 'Content-Type': 'application/json', + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.content) body.content = params.content + if (params.status) body.status = params.status + if (params.excerpt) body.excerpt = params.excerpt + if (params.slug) body.slug = params.slug + if (params.featuredMedia) body.featured_media = params.featuredMedia + + if (params.categories) { + body.categories = params.categories + .split(',') + .map((id: string) => Number.parseInt(id.trim(), 10)) + .filter((id: number) => !Number.isNaN(id)) + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((id: string) => Number.parseInt(id.trim(), 10)) + .filter((id: number) => !Number.isNaN(id)) + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + post: { + id: data.id, + date: data.date, + modified: data.modified, + slug: data.slug, + status: data.status, + type: data.type, + link: data.link, + title: data.title, + content: data.content, + excerpt: data.excerpt, + author: data.author, + featured_media: data.featured_media, + categories: data.categories || [], + tags: data.tags || [], + }, + }, + } + }, + + outputs: { + post: { + type: 'object', + description: 'The updated post', + properties: { + id: { type: 'number', description: 'Post ID' }, + date: { type: 'string', description: 'Post creation date' }, + modified: { type: 'string', description: 'Post modification date' }, + slug: { type: 'string', description: 'Post slug' }, + status: { type: 'string', description: 'Post status' }, + type: { type: 'string', description: 'Post type' }, + link: { type: 'string', description: 'Post URL' }, + title: { type: 'object', description: 'Post title object' }, + content: { type: 'object', description: 'Post content object' }, + excerpt: { type: 'object', description: 'Post excerpt object' }, + author: { type: 'number', description: 'Author ID' }, + featured_media: { type: 'number', description: 'Featured media ID' }, + categories: { type: 'array', description: 'Category IDs' }, + tags: { type: 'array', description: 'Tag IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/wordpress/upload_media.ts b/apps/sim/tools/wordpress/upload_media.ts new file mode 100644 index 00000000000..ff1306688d1 --- /dev/null +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -0,0 +1,160 @@ +import type { ToolConfig } from '@/tools/types' +import type { WordPressUploadMediaParams, WordPressUploadMediaResponse } from './types' + +export const uploadMediaTool: ToolConfig = + { + id: 'wordpress_upload_media', + name: 'WordPress Upload Media', + description: 'Upload a media file (image, video, document) to WordPress', + version: '1.0.0', + + params: { + siteUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress site URL (e.g., https://example.com)', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress username', + }, + applicationPassword: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress Application Password', + }, + file: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Base64 encoded file data or URL to fetch file from', + }, + filename: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Filename with extension (e.g., image.jpg)', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Media title', + }, + caption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Media caption', + }, + altText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Alternative text for accessibility', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Media description', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.siteUrl.replace(/\/$/, '') + return `${baseUrl}/wp-json/wp/v2/media` + }, + method: 'POST', + headers: (params) => { + const credentials = Buffer.from( + `${params.username}:${params.applicationPassword}` + ).toString('base64') + + // Determine content type from filename + const ext = params.filename.split('.').pop()?.toLowerCase() || '' + const mimeTypes: Record = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + pdf: 'application/pdf', + mp4: 'video/mp4', + mp3: 'audio/mpeg', + wav: 'audio/wav', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + } + const contentType = mimeTypes[ext] || 'application/octet-stream' + + return { + 'Content-Type': contentType, + 'Content-Disposition': `attachment; filename="${params.filename}"`, + Authorization: `Basic ${credentials}`, + } + }, + body: (params) => { + // If the file is a base64 string, we need to decode it + // The body function returns the data directly for binary uploads + // In this case, we return the file data as-is and let the executor handle it + return params.file as any + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `WordPress API error: ${response.status}`) + } + + const data = await response.json() + + return { + success: true, + output: { + media: { + id: data.id, + date: data.date, + slug: data.slug, + type: data.type, + link: data.link, + title: data.title, + caption: data.caption, + alt_text: data.alt_text, + media_type: data.media_type, + mime_type: data.mime_type, + source_url: data.source_url, + media_details: data.media_details, + }, + }, + } + }, + + outputs: { + media: { + type: 'object', + description: 'The uploaded media item', + properties: { + id: { type: 'number', description: 'Media ID' }, + date: { type: 'string', description: 'Upload date' }, + slug: { type: 'string', description: 'Media slug' }, + type: { type: 'string', description: 'Content type' }, + link: { type: 'string', description: 'Media page URL' }, + title: { type: 'object', description: 'Media title object' }, + caption: { type: 'object', description: 'Media caption object' }, + alt_text: { type: 'string', description: 'Alt text' }, + media_type: { type: 'string', description: 'Media type (image, video, etc.)' }, + mime_type: { type: 'string', description: 'MIME type' }, + source_url: { type: 'string', description: 'Direct URL to the media file' }, + media_details: { type: 'object', description: 'Media details (dimensions, etc.)' }, + }, + }, + }, + } diff --git a/apps/sim/tools/zoom/create_meeting.ts b/apps/sim/tools/zoom/create_meeting.ts new file mode 100644 index 00000000000..98fd3f3f4fa --- /dev/null +++ b/apps/sim/tools/zoom/create_meeting.ts @@ -0,0 +1,235 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomCreateMeetingParams, ZoomCreateMeetingResponse } from '@/tools/zoom/types' + +export const zoomCreateMeetingTool: ToolConfig = + { + id: 'zoom_create_meeting', + name: 'Zoom Create Meeting', + description: 'Create a new Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + userId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The user ID or email address. Use "me" for the authenticated user.', + }, + topic: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Meeting topic', + }, + type: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting start time in ISO 8601 format (e.g., 2025-06-03T10:00:00Z)', + }, + duration: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Meeting duration in minutes', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Timezone for the meeting (e.g., America/Los_Angeles)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting password', + }, + agenda: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting agenda', + }, + hostVideo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Start with host video on', + }, + participantVideo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Start with participant video on', + }, + joinBeforeHost: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Allow participants to join before host', + }, + muteUponEntry: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mute participants upon entry', + }, + waitingRoom: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Enable waiting room', + }, + autoRecording: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Auto recording setting: local, cloud, or none', + }, + }, + + request: { + url: (params) => `https://api.zoom.us/v2/users/${encodeURIComponent(params.userId)}/meetings`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + if (!params.topic || !params.topic.trim()) { + throw new Error('Topic is required to create a Zoom meeting') + } + + const body: Record = { + topic: params.topic, + type: params.type || 2, // Default to scheduled meeting + } + + if (params.startTime) { + body.start_time = params.startTime + } + if (params.duration != null) { + body.duration = params.duration + } + if (params.timezone) { + body.timezone = params.timezone + } + if (params.password) { + body.password = params.password + } + if (params.agenda) { + body.agenda = params.agenda + } + + // Build settings object + const settings: Record = {} + if (params.hostVideo != null) { + settings.host_video = params.hostVideo + } + if (params.participantVideo != null) { + settings.participant_video = params.participantVideo + } + if (params.joinBeforeHost != null) { + settings.join_before_host = params.joinBeforeHost + } + if (params.muteUponEntry != null) { + settings.mute_upon_entry = params.muteUponEntry + } + if (params.waitingRoom != null) { + settings.waiting_room = params.waitingRoom + } + if (params.autoRecording) { + settings.auto_recording = params.autoRecording + } + + if (Object.keys(settings).length > 0) { + body.settings = settings + } + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { meeting: {} as any }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + meeting: { + id: data.id, + uuid: data.uuid, + host_id: data.host_id, + host_email: data.host_email, + topic: data.topic, + type: data.type, + status: data.status, + start_time: data.start_time, + duration: data.duration, + timezone: data.timezone, + agenda: data.agenda, + created_at: data.created_at, + start_url: data.start_url, + join_url: data.join_url, + password: data.password, + h323_password: data.h323_password, + pstn_password: data.pstn_password, + encrypted_password: data.encrypted_password, + settings: data.settings, + }, + }, + } + }, + + outputs: { + meeting: { + type: 'object', + description: 'The created meeting with all its properties', + properties: { + id: { type: 'number', description: 'Meeting ID' }, + uuid: { type: 'string', description: 'Meeting UUID' }, + host_id: { type: 'string', description: 'Host user ID' }, + host_email: { type: 'string', description: 'Host email' }, + topic: { type: 'string', description: 'Meeting topic' }, + type: { type: 'number', description: 'Meeting type' }, + status: { type: 'string', description: 'Meeting status' }, + start_time: { type: 'string', description: 'Start time' }, + duration: { type: 'number', description: 'Duration in minutes' }, + timezone: { type: 'string', description: 'Timezone' }, + agenda: { type: 'string', description: 'Meeting agenda' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + start_url: { type: 'string', description: 'Host start URL' }, + join_url: { type: 'string', description: 'Participant join URL' }, + password: { type: 'string', description: 'Meeting password' }, + settings: { type: 'object', description: 'Meeting settings' }, + }, + }, + }, + } diff --git a/apps/sim/tools/zoom/delete_meeting.ts b/apps/sim/tools/zoom/delete_meeting.ts new file mode 100644 index 00000000000..bc4d64d4374 --- /dev/null +++ b/apps/sim/tools/zoom/delete_meeting.ts @@ -0,0 +1,98 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomDeleteMeetingParams, ZoomDeleteMeetingResponse } from '@/tools/zoom/types' + +export const zoomDeleteMeetingTool: ToolConfig = + { + id: 'zoom_delete_meeting', + name: 'Zoom Delete Meeting', + description: 'Delete or cancel a Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID to delete', + }, + occurrenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Occurrence ID for deleting a specific occurrence of a recurring meeting', + }, + scheduleForReminder: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send cancellation reminder email to registrants', + }, + cancelMeetingReminder: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send cancellation email to registrants and alternative hosts', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}` + const queryParams = new URLSearchParams() + + if (params.occurrenceId) { + queryParams.append('occurrence_id', params.occurrenceId) + } + if (params.scheduleForReminder != null) { + queryParams.append('schedule_for_reminder', String(params.scheduleForReminder)) + } + if (params.cancelMeetingReminder != null) { + queryParams.append('cancel_meeting_reminder', String(params.cancelMeetingReminder)) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { success: false }, + } + } + + // Zoom returns 204 No Content on successful deletion + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the meeting was deleted successfully', + }, + }, + } diff --git a/apps/sim/tools/zoom/get_meeting.ts b/apps/sim/tools/zoom/get_meeting.ts new file mode 100644 index 00000000000..9da9442302d --- /dev/null +++ b/apps/sim/tools/zoom/get_meeting.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomGetMeetingParams, ZoomGetMeetingResponse } from '@/tools/zoom/types' + +export const zoomGetMeetingTool: ToolConfig = { + id: 'zoom_get_meeting', + name: 'Zoom Get Meeting', + description: 'Get details of a specific Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID', + }, + occurrenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Occurrence ID for recurring meetings', + }, + showPreviousOccurrences: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Show previous occurrences for recurring meetings', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}` + const queryParams = new URLSearchParams() + + if (params.occurrenceId) { + queryParams.append('occurrence_id', params.occurrenceId) + } + if (params.showPreviousOccurrences != null) { + queryParams.append('show_previous_occurrences', String(params.showPreviousOccurrences)) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { meeting: {} as any }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + meeting: { + id: data.id, + uuid: data.uuid, + host_id: data.host_id, + host_email: data.host_email, + topic: data.topic, + type: data.type, + status: data.status, + start_time: data.start_time, + duration: data.duration, + timezone: data.timezone, + agenda: data.agenda, + created_at: data.created_at, + start_url: data.start_url, + join_url: data.join_url, + password: data.password, + h323_password: data.h323_password, + pstn_password: data.pstn_password, + encrypted_password: data.encrypted_password, + settings: data.settings, + recurrence: data.recurrence, + occurrences: data.occurrences, + }, + }, + } + }, + + outputs: { + meeting: { + type: 'object', + description: 'The meeting details', + properties: { + id: { type: 'number', description: 'Meeting ID' }, + uuid: { type: 'string', description: 'Meeting UUID' }, + host_id: { type: 'string', description: 'Host user ID' }, + host_email: { type: 'string', description: 'Host email' }, + topic: { type: 'string', description: 'Meeting topic' }, + type: { type: 'number', description: 'Meeting type' }, + status: { type: 'string', description: 'Meeting status' }, + start_time: { type: 'string', description: 'Start time' }, + duration: { type: 'number', description: 'Duration in minutes' }, + timezone: { type: 'string', description: 'Timezone' }, + agenda: { type: 'string', description: 'Meeting agenda' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + start_url: { type: 'string', description: 'Host start URL' }, + join_url: { type: 'string', description: 'Participant join URL' }, + password: { type: 'string', description: 'Meeting password' }, + settings: { type: 'object', description: 'Meeting settings' }, + recurrence: { type: 'object', description: 'Recurrence settings' }, + occurrences: { type: 'array', description: 'Meeting occurrences' }, + }, + }, + }, +} diff --git a/apps/sim/tools/zoom/get_meeting_invitation.ts b/apps/sim/tools/zoom/get_meeting_invitation.ts new file mode 100644 index 00000000000..666acb9e04d --- /dev/null +++ b/apps/sim/tools/zoom/get_meeting_invitation.ts @@ -0,0 +1,71 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomGetMeetingInvitationParams, + ZoomGetMeetingInvitationResponse, +} from '@/tools/zoom/types' + +export const zoomGetMeetingInvitationTool: ToolConfig< + ZoomGetMeetingInvitationParams, + ZoomGetMeetingInvitationResponse +> = { + id: 'zoom_get_meeting_invitation', + name: 'Zoom Get Meeting Invitation', + description: 'Get the meeting invitation text for a Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID', + }, + }, + + request: { + url: (params) => + `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}/invitation`, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { invitation: '' }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + invitation: data.invitation || '', + }, + } + }, + + outputs: { + invitation: { + type: 'string', + description: 'The meeting invitation text', + }, + }, +} diff --git a/apps/sim/tools/zoom/index.ts b/apps/sim/tools/zoom/index.ts new file mode 100644 index 00000000000..9ac76046a38 --- /dev/null +++ b/apps/sim/tools/zoom/index.ts @@ -0,0 +1,26 @@ +// Zoom tools exports +export { zoomCreateMeetingTool } from './create_meeting' +export { zoomDeleteMeetingTool } from './delete_meeting' +export { zoomGetMeetingTool } from './get_meeting' +export { zoomGetMeetingInvitationTool } from './get_meeting_invitation' +export { zoomListMeetingsTool } from './list_meetings' +// Type exports +export type { + ZoomCreateMeetingParams, + ZoomCreateMeetingResponse, + ZoomDeleteMeetingParams, + ZoomDeleteMeetingResponse, + ZoomGetMeetingInvitationParams, + ZoomGetMeetingInvitationResponse, + ZoomGetMeetingParams, + ZoomGetMeetingResponse, + ZoomListMeetingsParams, + ZoomListMeetingsResponse, + ZoomMeeting, + ZoomMeetingSettings, + ZoomMeetingType, + ZoomResponse, + ZoomUpdateMeetingParams, + ZoomUpdateMeetingResponse, +} from './types' +export { zoomUpdateMeetingTool } from './update_meeting' diff --git a/apps/sim/tools/zoom/list_meetings.ts b/apps/sim/tools/zoom/list_meetings.ts new file mode 100644 index 00000000000..30a863dfe21 --- /dev/null +++ b/apps/sim/tools/zoom/list_meetings.ts @@ -0,0 +1,137 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomListMeetingsParams, ZoomListMeetingsResponse } from '@/tools/zoom/types' + +export const zoomListMeetingsTool: ToolConfig = { + id: 'zoom_list_meetings', + name: 'Zoom List Meetings', + description: 'List all meetings for a Zoom user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + userId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The user ID or email address. Use "me" for the authenticated user.', + }, + type: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Meeting type filter: scheduled, live, upcoming, upcoming_meetings, or previous_meetings', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records per page (max 300)', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination to get next page of results', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/users/${encodeURIComponent(params.userId)}/meetings` + const queryParams = new URLSearchParams() + + if (params.type) { + queryParams.append('type', params.type) + } + if (params.pageSize) { + queryParams.append('page_size', String(params.pageSize)) + } + if (params.nextPageToken) { + queryParams.append('next_page_token', params.nextPageToken) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { + meetings: [], + pageInfo: { + pageCount: 0, + pageNumber: 0, + pageSize: 0, + totalRecords: 0, + }, + }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + meetings: (data.meetings || []).map((meeting: any) => ({ + id: meeting.id, + uuid: meeting.uuid, + host_id: meeting.host_id, + topic: meeting.topic, + type: meeting.type, + start_time: meeting.start_time, + duration: meeting.duration, + timezone: meeting.timezone, + agenda: meeting.agenda, + created_at: meeting.created_at, + join_url: meeting.join_url, + })), + pageInfo: { + pageCount: data.page_count || 0, + pageNumber: data.page_number || 0, + pageSize: data.page_size || 0, + totalRecords: data.total_records || 0, + nextPageToken: data.next_page_token, + }, + }, + } + }, + + outputs: { + meetings: { + type: 'array', + description: 'List of meetings', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + properties: { + pageCount: { type: 'number', description: 'Total number of pages' }, + pageNumber: { type: 'number', description: 'Current page number' }, + pageSize: { type: 'number', description: 'Number of records per page' }, + totalRecords: { type: 'number', description: 'Total number of records' }, + nextPageToken: { type: 'string', description: 'Token for next page' }, + }, + }, + }, +} diff --git a/apps/sim/tools/zoom/types.ts b/apps/sim/tools/zoom/types.ts new file mode 100644 index 00000000000..ab9ddaff4e6 --- /dev/null +++ b/apps/sim/tools/zoom/types.ts @@ -0,0 +1,186 @@ +// Common types for Zoom tools +import type { ToolResponse } from '@/tools/types' + +// Common parameters for all Zoom tools +export interface ZoomBaseParams { + accessToken: string +} + +// Meeting types +export type ZoomMeetingType = 1 | 2 | 3 | 8 // 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time + +export interface ZoomMeetingSettings { + host_video?: boolean + participant_video?: boolean + join_before_host?: boolean + mute_upon_entry?: boolean + watermark?: boolean + audio?: 'both' | 'telephony' | 'voip' + auto_recording?: 'local' | 'cloud' | 'none' + waiting_room?: boolean + meeting_authentication?: boolean + approval_type?: 0 | 1 | 2 // 0=auto, 1=manual, 2=none +} + +export interface ZoomMeeting { + id: number + uuid: string + host_id: string + host_email?: string + topic: string + type: ZoomMeetingType + status?: string + start_time?: string + duration?: number + timezone?: string + agenda?: string + created_at?: string + start_url?: string + join_url: string + password?: string + h323_password?: string + pstn_password?: string + encrypted_password?: string + settings?: ZoomMeetingSettings + recurrence?: { + type: number + repeat_interval?: number + weekly_days?: string + monthly_day?: number + monthly_week?: number + monthly_week_day?: number + end_times?: number + end_date_time?: string + } + occurrences?: Array<{ + occurrence_id: string + start_time: string + duration: number + status: string + }> +} + +export interface ZoomMeetingListResponse { + page_count: number + page_number: number + page_size: number + total_records: number + next_page_token?: string + meetings: ZoomMeeting[] +} + +// Create Meeting tool types +export interface ZoomCreateMeetingParams extends ZoomBaseParams { + userId: string + topic: string + type?: ZoomMeetingType + startTime?: string + duration?: number + timezone?: string + password?: string + agenda?: string + hostVideo?: boolean + participantVideo?: boolean + joinBeforeHost?: boolean + muteUponEntry?: boolean + waitingRoom?: boolean + autoRecording?: 'local' | 'cloud' | 'none' +} + +export interface ZoomCreateMeetingResponse extends ToolResponse { + output: { + meeting: ZoomMeeting + } +} + +// List Meetings tool types +export interface ZoomListMeetingsParams extends ZoomBaseParams { + userId: string + type?: 'scheduled' | 'live' | 'upcoming' | 'upcoming_meetings' | 'previous_meetings' + pageSize?: number + nextPageToken?: string +} + +export interface ZoomListMeetingsResponse extends ToolResponse { + output: { + meetings: ZoomMeeting[] + pageInfo: { + pageCount: number + pageNumber: number + pageSize: number + totalRecords: number + nextPageToken?: string + } + } +} + +// Get Meeting tool types +export interface ZoomGetMeetingParams extends ZoomBaseParams { + meetingId: string + occurrenceId?: string + showPreviousOccurrences?: boolean +} + +export interface ZoomGetMeetingResponse extends ToolResponse { + output: { + meeting: ZoomMeeting + } +} + +// Update Meeting tool types +export interface ZoomUpdateMeetingParams extends ZoomBaseParams { + meetingId: string + topic?: string + type?: ZoomMeetingType + startTime?: string + duration?: number + timezone?: string + password?: string + agenda?: string + hostVideo?: boolean + participantVideo?: boolean + joinBeforeHost?: boolean + muteUponEntry?: boolean + waitingRoom?: boolean + autoRecording?: 'local' | 'cloud' | 'none' +} + +export interface ZoomUpdateMeetingResponse extends ToolResponse { + output: { + success: boolean + } +} + +// Delete Meeting tool types +export interface ZoomDeleteMeetingParams extends ZoomBaseParams { + meetingId: string + occurrenceId?: string + scheduleForReminder?: boolean + cancelMeetingReminder?: boolean +} + +export interface ZoomDeleteMeetingResponse extends ToolResponse { + output: { + success: boolean + } +} + +// Get Meeting Invitation tool types +export interface ZoomGetMeetingInvitationParams extends ZoomBaseParams { + meetingId: string +} + +export interface ZoomGetMeetingInvitationResponse extends ToolResponse { + output: { + invitation: string + } +} + +// Combined response type for block +export type ZoomResponse = + | ZoomCreateMeetingResponse + | ZoomListMeetingsResponse + | ZoomGetMeetingResponse + | ZoomUpdateMeetingResponse + | ZoomDeleteMeetingResponse + | ZoomGetMeetingInvitationResponse diff --git a/apps/sim/tools/zoom/update_meeting.ts b/apps/sim/tools/zoom/update_meeting.ts new file mode 100644 index 00000000000..f676cd84493 --- /dev/null +++ b/apps/sim/tools/zoom/update_meeting.ts @@ -0,0 +1,195 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomUpdateMeetingParams, ZoomUpdateMeetingResponse } from '@/tools/zoom/types' + +export const zoomUpdateMeetingTool: ToolConfig = + { + id: 'zoom_update_meeting', + name: 'Zoom Update Meeting', + description: 'Update an existing Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID to update', + }, + topic: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting topic', + }, + type: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Meeting type: 1=instant, 2=scheduled, 3=recurring no fixed time, 8=recurring fixed time', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting start time in ISO 8601 format (e.g., 2025-06-03T10:00:00Z)', + }, + duration: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Meeting duration in minutes', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Timezone for the meeting (e.g., America/Los_Angeles)', + }, + password: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting password', + }, + agenda: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Meeting agenda', + }, + hostVideo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Start with host video on', + }, + participantVideo: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Start with participant video on', + }, + joinBeforeHost: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Allow participants to join before host', + }, + muteUponEntry: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mute participants upon entry', + }, + waitingRoom: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Enable waiting room', + }, + autoRecording: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Auto recording setting: local, cloud, or none', + }, + }, + + request: { + url: (params) => `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}`, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const body: Record = {} + + if (params.topic) { + body.topic = params.topic + } + if (params.type != null) { + body.type = params.type + } + if (params.startTime) { + body.start_time = params.startTime + } + if (params.duration != null) { + body.duration = params.duration + } + if (params.timezone) { + body.timezone = params.timezone + } + if (params.password) { + body.password = params.password + } + if (params.agenda) { + body.agenda = params.agenda + } + + // Build settings object + const settings: Record = {} + if (params.hostVideo != null) { + settings.host_video = params.hostVideo + } + if (params.participantVideo != null) { + settings.participant_video = params.participantVideo + } + if (params.joinBeforeHost != null) { + settings.join_before_host = params.joinBeforeHost + } + if (params.muteUponEntry != null) { + settings.mute_upon_entry = params.muteUponEntry + } + if (params.waitingRoom != null) { + settings.waiting_room = params.waitingRoom + } + if (params.autoRecording) { + settings.auto_recording = params.autoRecording + } + + if (Object.keys(settings).length > 0) { + body.settings = settings + } + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { success: false }, + } + } + + // Zoom returns 204 No Content on successful update + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the meeting was updated successfully', + }, + }, + } diff --git a/bun.lock b/bun.lock index 8a698a9ea3f..9ae8476ada3 100644 --- a/bun.lock +++ b/bun.lock @@ -167,6 +167,7 @@ "resend": "^4.1.2", "sharp": "0.34.3", "socket.io": "^4.8.1", + "ssh2": "^1.17.0", "stripe": "18.5.0", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", @@ -189,6 +190,7 @@ "@types/prismjs": "^1.26.5", "@types/react": "^19", "@types/react-dom": "^19", + "@types/ssh2": "^1.15.5", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^3.0.8", "autoprefixer": "10.4.21", @@ -1407,6 +1409,8 @@ "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], + "@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], "@types/three": ["@types/three@0.177.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A=="], @@ -1531,6 +1535,8 @@ "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + "before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="], "better-auth": ["better-auth@1.3.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" }, "peerDependencies": { "@lynx-js/react": "*", "@sveltejs/kit": "^2.0.0", "next": "^14.0.0 || ^15.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@sveltejs/kit", "next", "react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-FckxiAexCkkk2F0EOPmhXjWhYYE8eYg2x68lOIirSgyQ0TWc4JDvA5y8Vax5Jc7iyXk5MjJBY3DfwTPDZ87Lbg=="], @@ -1573,6 +1579,8 @@ "bufrw": ["bufrw@1.4.0", "", { "dependencies": { "ansi-color": "^0.2.1", "error": "^7.0.0", "hexer": "^1.5.0", "xtend": "^4.0.0" } }, "sha512-sWm8iPbqvL9+5SiYxXH73UOkyEbGQg7kyHQmReF89WJHQJw2eV4P/yZ0E+b71cczJ4pPobVhXxgQcmfSTgGHxQ=="], + "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -1697,6 +1705,8 @@ "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], "critters": ["critters@0.0.25", "", { "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.2", "htmlparser2": "^8.0.2", "postcss": "^8.4.23", "postcss-media-query-parser": "^0.2.3" } }, "sha512-ROF/tjJyyRdM8/6W0VqoN5Ql05xAGnkf5b7f3sTEl1bI5jTQQf8O918RD/V9tEb9pRY/TKcvJekDbJtniHyPtQ=="], @@ -2493,6 +2503,8 @@ "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], + "nan": ["nan@2.23.1", "", {}, "sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw=="], + "nano-spawn": ["nano-spawn@1.0.3", "", {}, "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -2943,6 +2955,8 @@ "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], @@ -3095,6 +3109,8 @@ "turbo-windows-arm64": ["turbo-windows-arm64@2.6.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q=="], + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + "twilio": ["twilio@5.9.0", "", { "dependencies": { "axios": "^1.11.0", "dayjs": "^1.11.9", "https-proxy-agent": "^5.0.0", "jsonwebtoken": "^9.0.2", "qs": "^6.9.4", "scmp": "^2.1.0", "xmlbuilder": "^13.0.2" } }, "sha512-Ij+xT9MZZSjP64lsy+x6vYsCCb5m2Db9KffkMXBrN3zWbG3rbkXxl+MZVVzrvpwEdSbQD0vMuin+TTlQ6kR6Xg=="], "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -3711,6 +3727,8 @@ "@types/papaparse/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@types/ssh2/@types/node": ["@types/node@18.19.128", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-m7wxXGpPpqxp2QDi/rpih5O772APRuBIa/6XiGqLNoM1txkjI8Sz1V4oSXJxQLTz/yP5mgy9z6UXEO6/lP70Gg=="], + "@types/through/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -4235,6 +4253,8 @@ "@types/papaparse/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/through/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], From 09c33e821147dc09c95b135c01f44ee2b089aeee Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 13:29:47 -0800 Subject: [PATCH 02/11] added polymarket & kalshi, fixed ssh --- apps/docs/components/icons.tsx | 29 + apps/docs/components/ui/icon-mapping.ts | 6 +- apps/docs/content/docs/en/tools/ahrefs.mdx | 7 + apps/docs/content/docs/en/tools/box.mdx | 276 -------- apps/docs/content/docs/en/tools/datadog.mdx | 3 - apps/docs/content/docs/en/tools/gitlab.mdx | 2 +- apps/docs/content/docs/en/tools/kalshi.mdx | 183 ++++++ apps/docs/content/docs/en/tools/meta.json | 3 +- .../docs/content/docs/en/tools/polymarket.mdx | 195 ++++++ apps/docs/content/docs/en/tools/shopify.mdx | 2 +- apps/docs/content/docs/en/tools/ssh.mdx | 2 +- .../app/api/auth/shopify/authorize/route.ts | 202 ++++++ .../app/api/auth/shopify/callback/route.ts | 125 ++++ apps/sim/app/api/auth/shopify/store/route.ts | 97 +++ .../tools/ssh/check-command-exists/route.ts | 14 +- .../api/tools/ssh/check-file-exists/route.ts | 6 +- .../api/tools/ssh/create-directory/route.ts | 21 +- .../app/api/tools/ssh/delete-file/route.ts | 23 +- .../app/api/tools/ssh/download-file/route.ts | 6 +- .../api/tools/ssh/execute-command/route.ts | 8 +- .../app/api/tools/ssh/execute-script/route.ts | 27 +- .../api/tools/ssh/get-system-info/route.ts | 6 +- .../app/api/tools/ssh/list-directory/route.ts | 6 +- .../app/api/tools/ssh/move-rename/route.ts | 26 +- .../api/tools/ssh/read-file-content/route.ts | 6 +- .../app/api/tools/ssh/upload-file/route.ts | 8 +- apps/sim/app/api/tools/ssh/utils.ts | 164 ++++- .../api/tools/ssh/write-file-content/route.ts | 8 +- .../components/oauth-required-modal.tsx | 19 + apps/sim/blocks/blocks/ahrefs.ts | 50 ++ apps/sim/blocks/blocks/box.ts | 513 --------------- apps/sim/blocks/blocks/datadog.ts | 15 + apps/sim/blocks/blocks/gitlab.ts | 29 +- apps/sim/blocks/blocks/kalshi.ts | 227 +++++++ apps/sim/blocks/blocks/polymarket.ts | 259 ++++++++ apps/sim/blocks/blocks/shopify.ts | 6 +- apps/sim/blocks/blocks/ssh.ts | 2 +- apps/sim/blocks/registry.ts | 8 +- apps/sim/components/icons.tsx | 29 + apps/sim/lib/core/config/env.ts | 2 - apps/sim/lib/oauth/oauth.ts | 63 -- apps/sim/tools/ahrefs/backlinks.ts | 15 +- apps/sim/tools/ahrefs/backlinks_stats.ts | 17 +- apps/sim/tools/ahrefs/broken_backlinks.ts | 17 +- apps/sim/tools/ahrefs/domain_rating.ts | 18 +- apps/sim/tools/ahrefs/keyword_overview.ts | 4 +- apps/sim/tools/ahrefs/organic_keywords.ts | 15 +- apps/sim/tools/ahrefs/referring_domains.ts | 29 +- apps/sim/tools/ahrefs/top_pages.ts | 17 +- apps/sim/tools/ahrefs/types.ts | 1 + apps/sim/tools/box/copy_file.ts | 92 --- apps/sim/tools/box/create_collaboration.ts | 138 ---- apps/sim/tools/box/create_folder.ts | 81 --- apps/sim/tools/box/create_shared_link.ts | 142 ---- apps/sim/tools/box/delete_file.ts | 82 --- apps/sim/tools/box/delete_folder.ts | 76 --- apps/sim/tools/box/download_file.ts | 120 ---- apps/sim/tools/box/get_current_user.ts | 75 --- apps/sim/tools/box/get_file.ts | 84 --- apps/sim/tools/box/index.ts | 16 - apps/sim/tools/box/list_folder.ts | 112 ---- apps/sim/tools/box/search.ts | 145 ---- apps/sim/tools/box/types.ts | 619 ------------------ apps/sim/tools/box/update_file.ts | 111 ---- apps/sim/tools/datadog/create_event.ts | 6 - apps/sim/tools/datadog/send_logs.ts | 6 - apps/sim/tools/datadog/submit_metrics.ts | 6 - apps/sim/tools/datadog/types.ts | 16 +- apps/sim/tools/gitlab/cancel_pipeline.ts | 21 +- apps/sim/tools/gitlab/create_issue.ts | 23 +- apps/sim/tools/gitlab/create_issue_note.ts | 23 +- apps/sim/tools/gitlab/create_merge_request.ts | 23 +- .../tools/gitlab/create_merge_request_note.ts | 23 +- apps/sim/tools/gitlab/create_pipeline.ts | 23 +- apps/sim/tools/gitlab/delete_issue.ts | 21 +- apps/sim/tools/gitlab/get_issue.ts | 23 +- apps/sim/tools/gitlab/get_merge_request.ts | 21 +- apps/sim/tools/gitlab/get_pipeline.ts | 21 +- apps/sim/tools/gitlab/get_project.ts | 21 +- apps/sim/tools/gitlab/list_issues.ts | 21 +- apps/sim/tools/gitlab/list_merge_requests.ts | 21 +- apps/sim/tools/gitlab/list_pipelines.ts | 21 +- apps/sim/tools/gitlab/list_projects.ts | 21 +- apps/sim/tools/gitlab/merge_merge_request.ts | 23 +- apps/sim/tools/gitlab/retry_pipeline.ts | 21 +- apps/sim/tools/gitlab/types.ts | 2 +- apps/sim/tools/gitlab/update_issue.ts | 23 +- apps/sim/tools/gitlab/update_merge_request.ts | 23 +- apps/sim/tools/kalshi/get_balance.ts | 89 +++ apps/sim/tools/kalshi/get_event.ts | 87 +++ apps/sim/tools/kalshi/get_events.ts | 116 ++++ apps/sim/tools/kalshi/get_market.ts | 73 +++ apps/sim/tools/kalshi/get_markets.ts | 115 ++++ apps/sim/tools/kalshi/get_orders.ts | 131 ++++ apps/sim/tools/kalshi/get_positions.ts | 134 ++++ apps/sim/tools/kalshi/index.ts | 7 + apps/sim/tools/kalshi/types.ts | 170 +++++ apps/sim/tools/polymarket/get_event.ts | 88 +++ apps/sim/tools/polymarket/get_events.ts | 121 ++++ apps/sim/tools/polymarket/get_market.ts | 88 +++ apps/sim/tools/polymarket/get_markets.ts | 121 ++++ apps/sim/tools/polymarket/get_midpoint.ts | 79 +++ apps/sim/tools/polymarket/get_orderbook.ts | 80 +++ apps/sim/tools/polymarket/get_price.ts | 89 +++ .../sim/tools/polymarket/get_price_history.ts | 114 ++++ apps/sim/tools/polymarket/index.ts | 9 + apps/sim/tools/polymarket/types.ts | 134 ++++ apps/sim/tools/registry.ts | 60 +- apps/sim/tools/shopify/adjust_inventory.ts | 3 +- apps/sim/tools/shopify/cancel_order.ts | 3 +- apps/sim/tools/shopify/create_customer.ts | 3 +- apps/sim/tools/shopify/create_product.ts | 3 +- apps/sim/tools/shopify/delete_customer.ts | 3 +- apps/sim/tools/shopify/delete_product.ts | 3 +- apps/sim/tools/shopify/get_customer.ts | 3 +- apps/sim/tools/shopify/get_inventory_level.ts | 3 +- apps/sim/tools/shopify/get_order.ts | 3 +- apps/sim/tools/shopify/get_product.ts | 3 +- apps/sim/tools/shopify/list_customers.ts | 3 +- apps/sim/tools/shopify/list_orders.ts | 3 +- apps/sim/tools/shopify/list_products.ts | 3 +- apps/sim/tools/shopify/types.ts | 1 + apps/sim/tools/shopify/update_customer.ts | 3 +- apps/sim/tools/shopify/update_order.ts | 3 +- apps/sim/tools/shopify/update_product.ts | 3 +- apps/sim/tools/zendesk/get_tickets.ts | 36 +- 126 files changed, 3929 insertions(+), 3223 deletions(-) delete mode 100644 apps/docs/content/docs/en/tools/box.mdx create mode 100644 apps/docs/content/docs/en/tools/kalshi.mdx create mode 100644 apps/docs/content/docs/en/tools/polymarket.mdx create mode 100644 apps/sim/app/api/auth/shopify/authorize/route.ts create mode 100644 apps/sim/app/api/auth/shopify/callback/route.ts create mode 100644 apps/sim/app/api/auth/shopify/store/route.ts delete mode 100644 apps/sim/blocks/blocks/box.ts create mode 100644 apps/sim/blocks/blocks/kalshi.ts create mode 100644 apps/sim/blocks/blocks/polymarket.ts delete mode 100644 apps/sim/tools/box/copy_file.ts delete mode 100644 apps/sim/tools/box/create_collaboration.ts delete mode 100644 apps/sim/tools/box/create_folder.ts delete mode 100644 apps/sim/tools/box/create_shared_link.ts delete mode 100644 apps/sim/tools/box/delete_file.ts delete mode 100644 apps/sim/tools/box/delete_folder.ts delete mode 100644 apps/sim/tools/box/download_file.ts delete mode 100644 apps/sim/tools/box/get_current_user.ts delete mode 100644 apps/sim/tools/box/get_file.ts delete mode 100644 apps/sim/tools/box/index.ts delete mode 100644 apps/sim/tools/box/list_folder.ts delete mode 100644 apps/sim/tools/box/search.ts delete mode 100644 apps/sim/tools/box/types.ts delete mode 100644 apps/sim/tools/box/update_file.ts create mode 100644 apps/sim/tools/kalshi/get_balance.ts create mode 100644 apps/sim/tools/kalshi/get_event.ts create mode 100644 apps/sim/tools/kalshi/get_events.ts create mode 100644 apps/sim/tools/kalshi/get_market.ts create mode 100644 apps/sim/tools/kalshi/get_markets.ts create mode 100644 apps/sim/tools/kalshi/get_orders.ts create mode 100644 apps/sim/tools/kalshi/get_positions.ts create mode 100644 apps/sim/tools/kalshi/index.ts create mode 100644 apps/sim/tools/kalshi/types.ts create mode 100644 apps/sim/tools/polymarket/get_event.ts create mode 100644 apps/sim/tools/polymarket/get_events.ts create mode 100644 apps/sim/tools/polymarket/get_market.ts create mode 100644 apps/sim/tools/polymarket/get_markets.ts create mode 100644 apps/sim/tools/polymarket/get_midpoint.ts create mode 100644 apps/sim/tools/polymarket/get_orderbook.ts create mode 100644 apps/sim/tools/polymarket/get_price.ts create mode 100644 apps/sim/tools/polymarket/get_price_history.ts create mode 100644 apps/sim/tools/polymarket/index.ts create mode 100644 apps/sim/tools/polymarket/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 5cce8179a97..4837d5c53b4 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4123,3 +4123,32 @@ export function DatadogIcon(props: SVGProps) { ) } + +export function KalshiIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + +export function PolymarketIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 09525077ddf..acc6a615b2b 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -10,7 +10,6 @@ import { ApolloIcon, ArxivIcon, AsanaIcon, - BoxCompanyIcon, BrainIcon, BrowserUseIcon, CalendlyIcon, @@ -45,6 +44,7 @@ import { IntercomIcon, JinaAIIcon, JiraIcon, + KalshiIcon, LinearIcon, LinkedInIcon, LinkupIcon, @@ -68,6 +68,7 @@ import { PerplexityIcon, PineconeIcon, PipedriveIcon, + PolymarketIcon, PostgresIcon, PosthogIcon, PylonIcon, @@ -156,6 +157,7 @@ export const blockTypeToIconMap: Record = { pylon: PylonIcon, posthog: PosthogIcon, postgresql: PostgresIcon, + polymarket: PolymarketIcon, pipedrive: PipedriveIcon, pinecone: PineconeIcon, perplexity: PerplexityIcon, @@ -179,6 +181,7 @@ export const blockTypeToIconMap: Record = { linkedin: LinkedInIcon, linear: LinearIcon, knowledge: PackageSearchIcon, + kalshi: KalshiIcon, jira: JiraIcon, jina: JinaAIIcon, intercom: IntercomIcon, @@ -211,7 +214,6 @@ export const blockTypeToIconMap: Record = { clay: ClayIcon, calendly: CalendlyIcon, browser_use: BrowserUseIcon, - box: BoxCompanyIcon, asana: AsanaIcon, arxiv: ArxivIcon, apollo: ApolloIcon, diff --git a/apps/docs/content/docs/en/tools/ahrefs.mdx b/apps/docs/content/docs/en/tools/ahrefs.mdx index d21e29b8c48..4943e936aad 100644 --- a/apps/docs/content/docs/en/tools/ahrefs.mdx +++ b/apps/docs/content/docs/en/tools/ahrefs.mdx @@ -42,6 +42,7 @@ Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating sh | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain to analyze \(e.g., example.com\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -61,6 +62,7 @@ Get a list of backlinks pointing to a target domain or URL. Returns details abou | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain or URL to analyze | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return \(default: 100\) | | `offset` | number | No | Number of results to skip for pagination | | `apiKey` | string | Yes | Ahrefs API Key | @@ -81,6 +83,7 @@ Get backlink statistics for a target domain or URL. Returns totals for different | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain or URL to analyze | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `apiKey` | string | Yes | Ahrefs API Key | #### Output @@ -99,6 +102,7 @@ Get a list of domains that link to a target domain or URL. Returns unique referr | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain or URL to analyze | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return \(default: 100\) | | `offset` | number | No | Number of results to skip for pagination | | `apiKey` | string | Yes | Ahrefs API Key | @@ -120,6 +124,7 @@ Get organic keywords that a target domain or URL ranks for in Google search resu | `target` | string | Yes | The target domain or URL to analyze | | `country` | string | No | Country code for search results \(e.g., us, gb, de\). Default: us | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return \(default: 100\) | | `offset` | number | No | Number of results to skip for pagination | | `apiKey` | string | Yes | Ahrefs API Key | @@ -141,6 +146,7 @@ Get the top pages of a target domain sorted by organic traffic. Returns page URL | `target` | string | Yes | The target domain to analyze | | `country` | string | No | Country code for traffic data \(e.g., us, gb, de\). Default: us | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return \(default: 100\) | | `offset` | number | No | Number of results to skip for pagination | | `apiKey` | string | Yes | Ahrefs API Key | @@ -179,6 +185,7 @@ Get a list of broken backlinks pointing to a target domain or URL. Useful for id | --------- | ---- | -------- | ----------- | | `target` | string | Yes | The target domain or URL to analyze | | `mode` | string | No | Analysis mode: domain \(entire domain\), prefix \(URL prefix\), subdomains \(include all subdomains\), exact \(exact URL match\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | | `limit` | number | No | Maximum number of results to return \(default: 100\) | | `offset` | number | No | Number of results to skip for pagination | | `apiKey` | string | Yes | Ahrefs API Key | diff --git a/apps/docs/content/docs/en/tools/box.mdx b/apps/docs/content/docs/en/tools/box.mdx deleted file mode 100644 index 1b2268e428e..00000000000 --- a/apps/docs/content/docs/en/tools/box.mdx +++ /dev/null @@ -1,276 +0,0 @@ ---- -title: Box -description: Manage files and folders in Box cloud storage ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -[Box](https://box.com/) is a leading cloud content management and collaboration platform that enables users and organizations to securely store, access, and collaborate on files and folders from anywhere. - -With Box, you can: - -- **Manage files and folders**: Upload, organize, move, copy, rename, or delete content in your Box cloud storage -- **Collaborate with others**: Share files and folders, set permissions, and invite collaborators for secure teamwork -- **Search content**: Find files or folders by name or content within your Box account -- **Create shared links**: Generate secure, shareable links for files and folders - -In Sim, the Box integration allows your agents to interact programmatically with your Box account, automating the management of your files and folders. Using the tools below, you can: - -- Retrieve information about specific files (`box_get_file`) -- List the contents of any Box folder (`box_list_folder`) -- Create new folders (`box_create_folder`) -- Search for files or folders -- And more — enabling workflow automation involving your Box data. - -These capabilities let your agents manage content, streamline collaboration, and automate file operations as part of your workflow execution in Sim. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Integrate Box into the workflow. Can manage files and folders, search content, create shared links, and collaborate with users. Supports file operations like copy, move, delete, and download. - - - -## Tools - -### `box_get_file` - -Get information about a file in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `fileId` | string | Yes | The ID of the file to retrieve | -| `fields` | string | No | Comma-separated list of fields to include \(e.g., "id,name,size,modified_at,shared_link"\) | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `file` | object | The file information | - -### `box_list_folder` - -List items in a Box folder - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `folderId` | string | Yes | The ID of the folder to list \(use "0" for root folder\) | -| `limit` | number | No | Maximum number of items to return \(default: 100, max: 1000\) | -| `offset` | number | No | Offset for pagination \(default: 0\) | -| `fields` | string | No | Comma-separated list of fields to include | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `items` | array | Array of files and folders in the folder | - -### `box_create_folder` - -Create a new folder in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `parentFolderId` | string | Yes | The ID of the parent folder \(use "0" for root folder\) | -| `folderName` | string | Yes | The name of the new folder | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `folder` | object | The created folder | - -### `box_delete_file` - -Delete a file from Box. Standard delete moves to trash. Use permanent flag for files already in trash. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `fileId` | string | Yes | The ID of the file to delete | -| `permanent` | boolean | No | If true, permanently delete a file that is already in trash | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `success` | boolean | Whether the file was successfully deleted | -| `fileId` | string | The ID of the deleted file | - -### `box_delete_folder` - -Delete a folder from Box (moves to trash by default) - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `folderId` | string | Yes | The ID of the folder to delete | -| `recursive` | boolean | No | Delete folder contents recursively \(default: true\) | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `success` | boolean | Whether the folder was successfully deleted | -| `folderId` | string | The ID of the deleted folder | - -### `box_copy_file` - -Create a copy of a file in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `fileId` | string | Yes | The ID of the file to copy | -| `parentFolderId` | string | Yes | The ID of the destination folder | -| `newName` | string | No | Optional new name for the copy | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `file` | object | The copied file | - -### `box_update_file` - -Update file information in Box (rename, move, or update description) - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `fileId` | string | Yes | The ID of the file to update | -| `name` | string | No | New name for the file | -| `description` | string | No | New description for the file | -| `parentId` | string | No | ID of the new parent folder \(to move the file\) | -| `tags` | array | No | Array of tags to set on the file | -| `items` | string | No | No description | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `file` | object | The updated file | - -### `box_search` - -Search for files and folders in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `query` | string | Yes | The search query string | -| `scope` | string | No | Search scope: "user_content" \(default\) or "enterprise_content" | -| `fileExtensions` | string | No | Comma-separated file extensions to filter by \(e.g., "pdf,docx"\) | -| `ancestorFolderIds` | string | No | Comma-separated folder IDs to search within | -| `contentTypes` | string | No | Comma-separated content types: "name", "description", "file_content", "comments", "tag" | -| `limit` | number | No | Maximum number of results \(default: 30, max: 200\) | -| `offset` | number | No | Offset for pagination \(default: 0\) | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `results` | array | Array of search results | - -### `box_download_file` - -Get the download URL for a file in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `fileId` | string | Yes | The ID of the file to download | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | File content \(for text files\) | -| `downloadUrl` | string | URL to download the file | -| `fileName` | string | Name of the file | -| `mimeType` | string | MIME type of the file | - -### `box_get_current_user` - -Get information about the authenticated Box user - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `user` | object | The authenticated user | - -### `box_create_shared_link` - -Create a shared link for a file or folder in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `itemId` | string | Yes | The ID of the file or folder | -| `itemType` | string | Yes | The type of item: "file" or "folder" | -| `access` | string | No | Access level: "open" \(anyone\), "company", or "collaborators" | -| `password` | string | No | Password to protect the shared link | -| `unsharedAt` | string | No | Expiration date in ISO 8601 format | -| `canDownload` | boolean | No | Whether the link allows downloads | -| `canPreview` | boolean | No | Whether the link allows previews | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `sharedLink` | object | The shared link details | - -### `box_create_collaboration` - -Share a file or folder with a user or group in Box - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `itemId` | string | Yes | The ID of the file or folder to share | -| `itemType` | string | Yes | The type of item: "file" or "folder" | -| `accessibleByLogin` | string | No | Email address of the user to share with | -| `accessibleById` | string | No | ID of the user or group to share with | -| `accessibleByType` | string | Yes | Type of collaborator: "user" or "group" | -| `role` | string | Yes | Access role: "editor", "viewer", "previewer", "uploader", or "co-owner" | -| `canViewPath` | boolean | No | Whether the collaborator can see the full path to the folder \(folders only\) | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `collaboration` | object | The created collaboration | - - - -## Notes - -- Category: `tools` -- Type: `box` diff --git a/apps/docs/content/docs/en/tools/datadog.mdx b/apps/docs/content/docs/en/tools/datadog.mdx index ff79e95bf29..45978068a8b 100644 --- a/apps/docs/content/docs/en/tools/datadog.mdx +++ b/apps/docs/content/docs/en/tools/datadog.mdx @@ -43,7 +43,6 @@ Submit custom metrics to Datadog. Use for tracking application performance, busi | --------- | ---- | -------- | ----------- | | `series` | string | Yes | JSON array of metric series to submit. Each series should include metric name, type \(gauge/rate/count\), points \(timestamp/value pairs\), and optional tags. | | `apiKey` | string | Yes | Datadog API key | -| `applicationKey` | string | Yes | Datadog Application key | | `site` | string | No | Datadog site/region \(default: datadoghq.com\) | #### Output @@ -93,7 +92,6 @@ Post an event to the Datadog event stream. Use for deployment notifications, ale | `sourceTypeName` | string | No | Source type name for the event | | `dateHappened` | number | No | Unix timestamp when the event occurred \(defaults to now\) | | `apiKey` | string | Yes | Datadog API key | -| `applicationKey` | string | Yes | Datadog Application key | | `site` | string | No | Datadog site/region \(default: datadoghq.com\) | #### Output @@ -228,7 +226,6 @@ Send log entries to Datadog for centralized logging and analysis. | --------- | ---- | -------- | ----------- | | `logs` | string | Yes | JSON array of log entries. Each entry should have message and optionally ddsource, ddtags, hostname, service. | | `apiKey` | string | Yes | Datadog API key | -| `applicationKey` | string | Yes | Datadog Application key | | `site` | string | No | Datadog site/region \(default: datadoghq.com\) | #### Output diff --git a/apps/docs/content/docs/en/tools/gitlab.mdx b/apps/docs/content/docs/en/tools/gitlab.mdx index 1f20c89c4b1..f5b71f443da 100644 --- a/apps/docs/content/docs/en/tools/gitlab.mdx +++ b/apps/docs/content/docs/en/tools/gitlab.mdx @@ -108,7 +108,7 @@ Get details of a specific GitLab issue | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Project ID or URL-encoded path | -| `issueIid` | number | Yes | Issue internal ID \(IID\) | +| `issueIid` | number | Yes | Issue number within the project \(the # shown in GitLab UI\) | #### Output diff --git a/apps/docs/content/docs/en/tools/kalshi.mdx b/apps/docs/content/docs/en/tools/kalshi.mdx new file mode 100644 index 00000000000..6248de43872 --- /dev/null +++ b/apps/docs/content/docs/en/tools/kalshi.mdx @@ -0,0 +1,183 @@ +--- +title: Kalshi +description: Access prediction markets data from Kalshi +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + +{/* MANUAL-CONTENT-START:intro */} +[Kalshi](https://kalshi.com) is a federally regulated exchange that enables users to trade directly on the outcomes of future events, known as prediction markets. Kalshi offers an open API, giving developers access to real-time market and event data, as well as account and trading information for automated or data-driven workflows. + +With Kalshi's API and Sim integration, you can enable agents to retrieve up-to-date prediction market information, explore open events, check account balances, manage orders, and track positions—all programmatically. This provides powerful capabilities for research, portfolio management, and creating intelligent workflows based on real-world event probabilities and outcomes. + +Key features of the Kalshi integration include: + +- **Market Listing & Filtering:** Retrieve lists of markets or individual market details, including filtering by status, series, or event. +- **Event Data:** Explore and filter real-world event listings and retrieve detailed event information. +- **Account and Trading:** Access user account balances, open positions, and manage order flow. +- **Automation Ready:** Build intelligent agents that leverage real-time prediction market data for automated actions and insights. + +By using the documented API tools, you can seamlessly incorporate Kalshi's rich event and financial market data into your AI workflows, dashboards, and trading automations. +{/* MANUAL-CONTENT-END */} + +## Usage Instructions + +Integrate Kalshi prediction markets into the workflow. Can get markets, get market, get events, get event, get balance, get positions, get orders. + + + +## Tools + +### `kalshi_get_markets` + +Retrieve a list of prediction markets from Kalshi with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `status` | string | No | Filter by status \(unopened, open, closed, settled\) | +| `seriesTicker` | string | No | Filter by series ticker | +| `eventTicker` | string | No | Filter by event ticker | +| `limit` | string | No | Number of results \(1-1000, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Markets data and metadata | + +### `kalshi_get_market` + +Retrieve details of a specific prediction market by ticker + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `ticker` | string | Yes | The market ticker \(e.g., "KXBTC-24DEC31"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Market data and metadata | + +### `kalshi_get_events` + +Retrieve a list of events from Kalshi with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `status` | string | No | Filter by status \(open, closed, settled\) | +| `seriesTicker` | string | No | Filter by series ticker | +| `withNestedMarkets` | string | No | Include nested markets in response \(true/false\) | +| `limit` | string | No | Number of results \(1-200, default: 200\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Events data and metadata | + +### `kalshi_get_event` + +Retrieve details of a specific event by ticker + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `eventTicker` | string | Yes | The event ticker | +| `withNestedMarkets` | string | No | Include nested markets in response \(true/false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Event data and metadata | + +### `kalshi_get_balance` + +Retrieve your account balance and portfolio value from Kalshi + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `keyId` | string | Yes | Your Kalshi API Key ID | +| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Balance data and metadata | + +### `kalshi_get_positions` + +Retrieve your open positions from Kalshi + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `keyId` | string | Yes | Your Kalshi API Key ID | +| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) | +| `ticker` | string | No | Filter by market ticker | +| `eventTicker` | string | No | Filter by event ticker \(max 10 comma-separated\) | +| `settlementStatus` | string | No | Filter by settlement status \(all, unsettled, settled\). Default: unsettled | +| `limit` | string | No | Number of results \(1-1000, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Positions data and metadata | + +### `kalshi_get_orders` + +Retrieve your orders from Kalshi with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `keyId` | string | Yes | Your Kalshi API Key ID | +| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) | +| `ticker` | string | No | Filter by market ticker | +| `eventTicker` | string | No | Filter by event ticker \(max 10 comma-separated\) | +| `status` | string | No | Filter by status \(resting, canceled, executed\) | +| `limit` | string | No | Number of results \(1-200, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Orders data and metadata | + + + +## Notes + +- Category: `tools` +- Type: `kalshi` diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 60f9b076f4c..8226a574690 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -7,7 +7,6 @@ "apollo", "arxiv", "asana", - "box", "browser_use", "calendly", "clay", @@ -40,6 +39,7 @@ "intercom", "jina", "jira", + "kalshi", "knowledge", "linear", "linkedin", @@ -63,6 +63,7 @@ "perplexity", "pinecone", "pipedrive", + "polymarket", "postgresql", "posthog", "pylon", diff --git a/apps/docs/content/docs/en/tools/polymarket.mdx b/apps/docs/content/docs/en/tools/polymarket.mdx new file mode 100644 index 00000000000..d3f9501d8b5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/polymarket.mdx @@ -0,0 +1,195 @@ +--- +title: Polymarket +description: Access prediction markets data from Polymarket +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Polymarket](https://polymarket.com) is a decentralized prediction markets platform where users can trade on the outcome of future events using blockchain technology. Polymarket provides a comprehensive API, enabling developers and agents to access live market data, event listings, price information, and orderbook statistics to power data-driven workflows and AI automations. + +With Polymarket’s API and Sim integration, you can enable agents to programmatically retrieve prediction market information, explore open markets and associated events, analyze historical price data, and access orderbooks and market midpoints. This creates new possibilities for research, automated analysis, and developing intelligent agents that react to real-time event probabilities derived from market prices. + +Key features of the Polymarket integration include: + +- **Market Listing & Filtering:** List all current or historical prediction markets, filter by tag, sort, and paginate through results. +- **Market Detail:** Retrieve details for a single market by market ID or slug, including its outcomes and status. +- **Event Listings:** Access lists of Polymarket events and detailed event information. +- **Orderbook & Price Data:** Analyze the orderbook, get the latest market prices, view the midpoint, or obtain historical price information for any market. +- **Automation Ready:** Build agents or tools that react programmatically to market developments, changing odds, or specific event outcomes. + +By using these documented API endpoints, you can seamlessly integrate Polymarket’s rich on-chain prediction market data into your own AI workflows, dashboards, research tools, and trading automations. +{/* MANUAL-CONTENT-END */} + +## Usage Instructions + +Integrate Polymarket prediction markets into the workflow. Can get markets, get market, get events, get event, get orderbook, get price, get midpoint, and get price history. + + + +## Tools + +### `polymarket_get_markets` + +Retrieve a list of prediction markets from Polymarket with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `closed` | string | No | Filter by closed status \(true/false\). Use false for active markets only. | +| `order` | string | No | Sort field \(e.g., id, volume, liquidity\) | +| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) | +| `tagId` | string | No | Filter by tag ID | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Markets data and metadata | + +### `polymarket_get_market` + +Retrieve details of a specific prediction market by ID or slug + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `marketId` | string | No | The market ID. Required if slug is not provided. | +| `slug` | string | No | The market slug \(e.g., "will-trump-win"\). Required if marketId is not provided. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Market data and metadata | + +### `polymarket_get_events` + +Retrieve a list of events from Polymarket with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `closed` | string | No | Filter by closed status \(true/false\). Use false for active events only. | +| `order` | string | No | Sort field \(e.g., id, volume\) | +| `ascending` | string | No | Sort direction \(true for ascending, false for descending\) | +| `tagId` | string | No | Filter by tag ID | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Events data and metadata | + +### `polymarket_get_event` + +Retrieve details of a specific event by ID or slug + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `eventId` | string | No | The event ID. Required if slug is not provided. | +| `slug` | string | No | The event slug \(e.g., "2024-presidential-election"\). Required if eventId is not provided. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Event data and metadata | + +### `polymarket_get_orderbook` + +Retrieve the order book summary for a specific token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Orderbook data and metadata | + +### `polymarket_get_price` + +Retrieve the market price for a specific token and side + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | +| `side` | string | Yes | Order side: buy or sell | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Price data and metadata | + +### `polymarket_get_midpoint` + +Retrieve the midpoint price for a specific token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Midpoint price data and metadata | + +### `polymarket_get_price_history` + +Retrieve historical price data for a specific market token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | +| `interval` | string | No | Duration ending at current time \(1m, 1h, 6h, 1d, 1w, max\). Mutually exclusive with startTs/endTs. | +| `fidelity` | number | No | Data resolution in minutes \(e.g., 60 for hourly\) | +| `startTs` | number | No | Start timestamp \(Unix seconds UTC\) | +| `endTs` | number | No | End timestamp \(Unix seconds UTC\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Price history data and metadata | + + + +## Notes + +- Category: `tools` +- Type: `polymarket` diff --git a/apps/docs/content/docs/en/tools/shopify.mdx b/apps/docs/content/docs/en/tools/shopify.mdx index 6ff9a1206cf..d29d6d2e9d1 100644 --- a/apps/docs/content/docs/en/tools/shopify.mdx +++ b/apps/docs/content/docs/en/tools/shopify.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/docs/content/docs/en/tools/ssh.mdx b/apps/docs/content/docs/en/tools/ssh.mdx index 3fb3466e12d..663ca7ac31d 100644 --- a/apps/docs/content/docs/en/tools/ssh.mdx +++ b/apps/docs/content/docs/en/tools/ssh.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} diff --git a/apps/sim/app/api/auth/shopify/authorize/route.ts b/apps/sim/app/api/auth/shopify/authorize/route.ts new file mode 100644 index 00000000000..0a65ec76f12 --- /dev/null +++ b/apps/sim/app/api/auth/shopify/authorize/route.ts @@ -0,0 +1,202 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { env } from '@/lib/core/config/env' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('ShopifyAuthorize') + +export const dynamic = 'force-dynamic' + +// Shopify OAuth scopes needed for the integration +const SHOPIFY_SCOPES = [ + 'read_products', + 'write_products', + 'read_orders', + 'write_orders', + 'read_customers', + 'write_customers', + 'read_inventory', + 'write_inventory', + 'read_fulfillments', + 'write_fulfillments', +].join(',') + +export async function GET(request: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const clientId = env.SHOPIFY_CLIENT_ID + + if (!clientId) { + logger.error('SHOPIFY_CLIENT_ID not configured') + return NextResponse.json({ error: 'Shopify client ID not configured' }, { status: 500 }) + } + + // Get shop domain from query parameter + const shopDomain = request.nextUrl.searchParams.get('shop') + + if (!shopDomain) { + // Return a page that asks for the shop domain + return new NextResponse( + ` + + + Connect Shopify Store + + + + + +

+

Connect Your Shopify Store

+

Enter your Shopify store domain to continue

+
+ + +
+

Your store domain looks like: yourstore.myshopify.com

+
+ + + +`, + { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store, no-cache, must-revalidate', + }, + } + ) + } + + // Validate and clean shop domain + let cleanShop = shopDomain.toLowerCase().trim() + cleanShop = cleanShop.replace('https://', '').replace('http://', '') + if (!cleanShop.endsWith('.myshopify.com')) { + cleanShop = `${cleanShop.replace('.myshopify.com', '')}.myshopify.com` + } + + const baseUrl = getBaseUrl() + const redirectUri = `${baseUrl}/api/auth/shopify/callback` + + // Generate a random state for CSRF protection + const state = crypto.randomUUID() + + // Store state in a cookie for verification + const response = NextResponse.redirect( + `https://${cleanShop}/admin/oauth/authorize?` + + new URLSearchParams({ + client_id: clientId, + scope: SHOPIFY_SCOPES, + redirect_uri: redirectUri, + state: state, + }).toString() + ) + + // Set state cookie for CSRF verification + response.cookies.set('shopify_oauth_state', state, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 10, // 10 minutes + path: '/', + }) + + // Store the shop domain for the callback + response.cookies.set('shopify_shop_domain', cleanShop, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 10, // 10 minutes + path: '/', + }) + + return response + } catch (error) { + logger.error('Error initiating Shopify authorization:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/auth/shopify/callback/route.ts b/apps/sim/app/api/auth/shopify/callback/route.ts new file mode 100644 index 00000000000..a95287f36d6 --- /dev/null +++ b/apps/sim/app/api/auth/shopify/callback/route.ts @@ -0,0 +1,125 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { env } from '@/lib/core/config/env' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('ShopifyCallback') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + const baseUrl = getBaseUrl() + + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`) + } + + const { searchParams } = request.nextUrl + const code = searchParams.get('code') + const state = searchParams.get('state') + const shop = searchParams.get('shop') + + // Verify state to prevent CSRF + const storedState = request.cookies.get('shopify_oauth_state')?.value + const storedShop = request.cookies.get('shopify_shop_domain')?.value + + if (!state || state !== storedState) { + logger.error('State mismatch in Shopify OAuth callback') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_state_mismatch`) + } + + if (!code) { + logger.error('No code received from Shopify') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_code`) + } + + // Use shop from query params or cookie + const shopDomain = shop || storedShop + if (!shopDomain) { + logger.error('No shop domain available') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_shop`) + } + + const clientId = env.SHOPIFY_CLIENT_ID + const clientSecret = env.SHOPIFY_CLIENT_SECRET + + if (!clientId || !clientSecret) { + logger.error('Shopify credentials not configured') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_config_error`) + } + + // Exchange code for access token + const tokenResponse = await fetch(`https://${shopDomain}/admin/oauth/access_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + client_id: clientId, + client_secret: clientSecret, + code: code, + }), + }) + + if (!tokenResponse.ok) { + const errorText = await tokenResponse.text() + logger.error('Failed to exchange code for token:', { + status: tokenResponse.status, + body: errorText, + }) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_token_error`) + } + + const tokenData = await tokenResponse.json() + const accessToken = tokenData.access_token + const scope = tokenData.scope + + if (!accessToken) { + logger.error('No access token in response') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_token`) + } + + // Store the token - redirect to store endpoint with token data + const storeUrl = new URL(`${baseUrl}/api/auth/shopify/store`) + + // Create response with redirect to store page that will handle token storage + const response = NextResponse.redirect(storeUrl) + + // Pass token data via secure cookies (will be consumed by store endpoint) + response.cookies.set('shopify_pending_token', accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60, // 1 minute - just enough to complete the flow + path: '/', + }) + + response.cookies.set('shopify_pending_shop', shopDomain, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60, + path: '/', + }) + + response.cookies.set('shopify_pending_scope', scope || '', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60, + path: '/', + }) + + // Clear the OAuth state cookies + response.cookies.delete('shopify_oauth_state') + response.cookies.delete('shopify_shop_domain') + + return response + } catch (error) { + logger.error('Error in Shopify OAuth callback:', error) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_callback_error`) + } +} diff --git a/apps/sim/app/api/auth/shopify/store/route.ts b/apps/sim/app/api/auth/shopify/store/route.ts new file mode 100644 index 00000000000..5e1f3a1eb33 --- /dev/null +++ b/apps/sim/app/api/auth/shopify/store/route.ts @@ -0,0 +1,97 @@ +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/../../packages/db' +import { account } from '@/../../packages/db/schema' + +const logger = createLogger('ShopifyStore') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + const baseUrl = getBaseUrl() + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn('Unauthorized attempt to store Shopify token') + return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`) + } + + // Get token data from cookies (set by callback) + const accessToken = request.cookies.get('shopify_pending_token')?.value + const shopDomain = request.cookies.get('shopify_pending_shop')?.value + const scope = request.cookies.get('shopify_pending_scope')?.value + + if (!accessToken || !shopDomain) { + logger.error('Missing token or shop domain in cookies') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_missing_data`) + } + + // Validate the token by making a simple API call + const shopResponse = await fetch(`https://${shopDomain}/admin/api/2024-10/shop.json`, { + headers: { + 'X-Shopify-Access-Token': accessToken, + 'Content-Type': 'application/json', + }, + }) + + if (!shopResponse.ok) { + const errorText = await shopResponse.text() + logger.error('Invalid Shopify token', { + status: shopResponse.status, + error: errorText, + }) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_token`) + } + + const shopData = await shopResponse.json() + const shopInfo = shopData.shop + + // Check if account already exists for this user and provider + const existing = await db.query.account.findFirst({ + where: and(eq(account.userId, session.user.id), eq(account.providerId, 'shopify')), + }) + + const now = new Date() + + // Store the shop domain in the accessTokenExpiresAt field as metadata + // Since Shopify tokens don't expire, we use this field to store the shop domain + // The actual shop domain is needed for API calls + const accountData = { + accessToken: accessToken, + accountId: shopInfo.id?.toString() || shopDomain, + scope: scope || '', + updatedAt: now, + // Store shop domain in idToken field (repurposed for shop domain since Shopify doesn't use idToken) + idToken: shopDomain, + } + + if (existing) { + await db.update(account).set(accountData).where(eq(account.id, existing.id)) + logger.info('Updated existing Shopify account', { accountId: existing.id }) + } else { + await db.insert(account).values({ + id: `shopify_${session.user.id}_${Date.now()}`, + userId: session.user.id, + providerId: 'shopify', + ...accountData, + createdAt: now, + }) + logger.info('Created new Shopify account for user', { userId: session.user.id }) + } + + // Clear the pending cookies + const response = NextResponse.redirect(`${baseUrl}/workspace?shopify_connected=true`) + response.cookies.delete('shopify_pending_token') + response.cookies.delete('shopify_pending_shop') + response.cookies.delete('shopify_pending_scope') + + return response + } catch (error) { + logger.error('Error storing Shopify token:', error) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_store_error`) + } +} diff --git a/apps/sim/app/api/tools/ssh/check-command-exists/route.ts b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts index 35d5579cc09..6cdddc27b56 100644 --- a/apps/sim/app/api/tools/ssh/check-command-exists/route.ts +++ b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createLogger } from '@/lib/logs/console/logger' -import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils' +import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHCheckCommandExistsAPI') @@ -10,9 +10,9 @@ const CheckCommandExistsSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), commandName: z.string().min(1, 'Command name is required'), }) @@ -45,10 +45,12 @@ export async function POST(request: NextRequest) { }) try { + const escapedCommand = escapeShellArg(params.commandName) + // Use which/command to check if the command exists const result = await executeSSHCommand( client, - `command -v '${params.commandName}' 2>/dev/null || which '${params.commandName}' 2>/dev/null` + `command -v '${escapedCommand}' 2>/dev/null || which '${escapedCommand}' 2>/dev/null` ) const exists = result.exitCode === 0 && result.stdout.trim().length > 0 @@ -60,7 +62,7 @@ export async function POST(request: NextRequest) { try { const versionResult = await executeSSHCommand( client, - `'${params.commandName}' --version 2>&1 | head -1 || '${params.commandName}' -v 2>&1 | head -1` + `'${escapedCommand}' --version 2>&1 | head -1 || '${escapedCommand}' -v 2>&1 | head -1` ) if (versionResult.exitCode === 0 && versionResult.stdout.trim()) { version = versionResult.stdout.trim() diff --git a/apps/sim/app/api/tools/ssh/check-file-exists/route.ts b/apps/sim/app/api/tools/ssh/check-file-exists/route.ts index c02e008d039..0830488db17 100644 --- a/apps/sim/app/api/tools/ssh/check-file-exists/route.ts +++ b/apps/sim/app/api/tools/ssh/check-file-exists/route.ts @@ -16,9 +16,9 @@ const CheckFileExistsSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), type: z.enum(['file', 'directory', 'any']).default('any'), }) diff --git a/apps/sim/app/api/tools/ssh/create-directory/route.ts b/apps/sim/app/api/tools/ssh/create-directory/route.ts index cf2733a3af5..06f7c412ffc 100644 --- a/apps/sim/app/api/tools/ssh/create-directory/route.ts +++ b/apps/sim/app/api/tools/ssh/create-directory/route.ts @@ -2,7 +2,12 @@ import { randomUUID } from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createLogger } from '@/lib/logs/console/logger' -import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' +import { + createSSHConnection, + escapeShellArg, + executeSSHCommand, + sanitizePath, +} from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHCreateDirectoryAPI') @@ -10,9 +15,9 @@ const CreateDirectorySchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), recursive: z.boolean().default(true), permissions: z.string().default('0755'), @@ -46,9 +51,13 @@ export async function POST(request: NextRequest) { try { const dirPath = sanitizePath(params.path) + const escapedPath = escapeShellArg(dirPath) // Check if directory already exists - const checkResult = await executeSSHCommand(client, `test -d '${dirPath}' && echo "exists"`) + const checkResult = await executeSSHCommand( + client, + `test -d '${escapedPath}' && echo "exists"` + ) const alreadyExists = checkResult.stdout.trim() === 'exists' if (alreadyExists) { @@ -63,7 +72,7 @@ export async function POST(request: NextRequest) { // Create directory const mkdirFlag = params.recursive ? '-p' : '' - const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${dirPath}'` + const command = `mkdir ${mkdirFlag} -m ${params.permissions} '${escapedPath}'` const result = await executeSSHCommand(client, command) if (result.exitCode !== 0) { diff --git a/apps/sim/app/api/tools/ssh/delete-file/route.ts b/apps/sim/app/api/tools/ssh/delete-file/route.ts index 9dd6b489005..a1cb694fa22 100644 --- a/apps/sim/app/api/tools/ssh/delete-file/route.ts +++ b/apps/sim/app/api/tools/ssh/delete-file/route.ts @@ -2,7 +2,12 @@ import { randomUUID } from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createLogger } from '@/lib/logs/console/logger' -import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' +import { + createSSHConnection, + escapeShellArg, + executeSSHCommand, + sanitizePath, +} from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHDeleteFileAPI') @@ -10,9 +15,9 @@ const DeleteFileSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), recursive: z.boolean().default(false), force: z.boolean().default(false), @@ -46,9 +51,13 @@ export async function POST(request: NextRequest) { try { const filePath = sanitizePath(params.path) + const escapedPath = escapeShellArg(filePath) // Check if path exists - const checkResult = await executeSSHCommand(client, `test -e '${filePath}' && echo "exists"`) + const checkResult = await executeSSHCommand( + client, + `test -e '${escapedPath}' && echo "exists"` + ) if (checkResult.stdout.trim() !== 'exists') { return NextResponse.json({ error: `Path does not exist: ${filePath}` }, { status: 404 }) } @@ -56,9 +65,9 @@ export async function POST(request: NextRequest) { // Build delete command let command: string if (params.recursive) { - command = params.force ? `rm -rf '${filePath}'` : `rm -r '${filePath}'` + command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'` } else { - command = params.force ? `rm -f '${filePath}'` : `rm '${filePath}'` + command = params.force ? `rm -f '${escapedPath}'` : `rm '${escapedPath}'` } const result = await executeSSHCommand(client, command) diff --git a/apps/sim/app/api/tools/ssh/download-file/route.ts b/apps/sim/app/api/tools/ssh/download-file/route.ts index 3689ab0324b..03f5b2cfab4 100644 --- a/apps/sim/app/api/tools/ssh/download-file/route.ts +++ b/apps/sim/app/api/tools/ssh/download-file/route.ts @@ -12,9 +12,9 @@ const DownloadFileSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), remotePath: z.string().min(1, 'Remote path is required'), }) diff --git a/apps/sim/app/api/tools/ssh/execute-command/route.ts b/apps/sim/app/api/tools/ssh/execute-command/route.ts index 187c7be7563..c553b3554ed 100644 --- a/apps/sim/app/api/tools/ssh/execute-command/route.ts +++ b/apps/sim/app/api/tools/ssh/execute-command/route.ts @@ -10,11 +10,11 @@ const ExecuteCommandSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), command: z.string().min(1, 'Command is required'), - workingDirectory: z.string().optional(), + workingDirectory: z.string().nullish(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/ssh/execute-script/route.ts b/apps/sim/app/api/tools/ssh/execute-script/route.ts index 5a24325cf49..0e7e44abfeb 100644 --- a/apps/sim/app/api/tools/ssh/execute-script/route.ts +++ b/apps/sim/app/api/tools/ssh/execute-script/route.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createLogger } from '@/lib/logs/console/logger' -import { createSSHConnection, executeSSHCommand } from '@/app/api/tools/ssh/utils' +import { createSSHConnection, escapeShellArg, executeSSHCommand } from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHExecuteScriptAPI') @@ -10,12 +10,12 @@ const ExecuteScriptSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), script: z.string().min(1, 'Script content is required'), interpreter: z.string().default('/bin/bash'), - workingDirectory: z.string().optional(), + workingDirectory: z.string().nullish(), }) export async function POST(request: NextRequest) { @@ -47,25 +47,26 @@ export async function POST(request: NextRequest) { try { // Create a temporary script file, execute it, and clean up const scriptPath = `/tmp/sim_script_${requestId}.sh` - - // Escape the script content for use in a heredoc - const escapedScript = params.script.replace(/'/g, "'\\''") + const escapedScriptPath = escapeShellArg(scriptPath) + const escapedInterpreter = escapeShellArg(params.interpreter) // Build the command to create, execute, and clean up the script - let command = `cat > '${scriptPath}' << 'SIMEOF' + // Note: heredoc with quoted delimiter ('SIMEOF') prevents variable expansion + let command = `cat > '${escapedScriptPath}' << 'SIMEOF' ${params.script} SIMEOF -chmod +x '${scriptPath}'` +chmod +x '${escapedScriptPath}'` if (params.workingDirectory) { + const escapedWorkDir = escapeShellArg(params.workingDirectory) command += ` -cd "${params.workingDirectory}"` +cd '${escapedWorkDir}'` } command += ` -'${params.interpreter}' '${scriptPath}' +'${escapedInterpreter}' '${escapedScriptPath}' exit_code=$? -rm -f '${scriptPath}' +rm -f '${escapedScriptPath}' exit $exit_code` const result = await executeSSHCommand(client, command) diff --git a/apps/sim/app/api/tools/ssh/get-system-info/route.ts b/apps/sim/app/api/tools/ssh/get-system-info/route.ts index 4bee0e25b1a..9f88415e55f 100644 --- a/apps/sim/app/api/tools/ssh/get-system-info/route.ts +++ b/apps/sim/app/api/tools/ssh/get-system-info/route.ts @@ -10,9 +10,9 @@ const GetSystemInfoSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), }) export async function POST(request: NextRequest) { diff --git a/apps/sim/app/api/tools/ssh/list-directory/route.ts b/apps/sim/app/api/tools/ssh/list-directory/route.ts index 987ab96a43e..dbcfeb78ebf 100644 --- a/apps/sim/app/api/tools/ssh/list-directory/route.ts +++ b/apps/sim/app/api/tools/ssh/list-directory/route.ts @@ -16,9 +16,9 @@ const ListDirectorySchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), detailed: z.boolean().default(true), recursive: z.boolean().default(false), diff --git a/apps/sim/app/api/tools/ssh/move-rename/route.ts b/apps/sim/app/api/tools/ssh/move-rename/route.ts index 21778533353..434ea8ba593 100644 --- a/apps/sim/app/api/tools/ssh/move-rename/route.ts +++ b/apps/sim/app/api/tools/ssh/move-rename/route.ts @@ -2,7 +2,12 @@ import { randomUUID } from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createLogger } from '@/lib/logs/console/logger' -import { createSSHConnection, executeSSHCommand, sanitizePath } from '@/app/api/tools/ssh/utils' +import { + createSSHConnection, + escapeShellArg, + executeSSHCommand, + sanitizePath, +} from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHMoveRenameAPI') @@ -10,9 +15,9 @@ const MoveRenameSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), sourcePath: z.string().min(1, 'Source path is required'), destinationPath: z.string().min(1, 'Destination path is required'), overwrite: z.boolean().default(false), @@ -49,11 +54,13 @@ export async function POST(request: NextRequest) { try { const sourcePath = sanitizePath(params.sourcePath) const destPath = sanitizePath(params.destinationPath) + const escapedSource = escapeShellArg(sourcePath) + const escapedDest = escapeShellArg(destPath) // Check if source exists const sourceCheck = await executeSSHCommand( client, - `test -e '${sourcePath}' && echo "exists"` + `test -e '${escapedSource}' && echo "exists"` ) if (sourceCheck.stdout.trim() !== 'exists') { return NextResponse.json( @@ -64,7 +71,10 @@ export async function POST(request: NextRequest) { // Check if destination exists (when overwrite is false) if (!params.overwrite) { - const destCheck = await executeSSHCommand(client, `test -e '${destPath}' && echo "exists"`) + const destCheck = await executeSSHCommand( + client, + `test -e '${escapedDest}' && echo "exists"` + ) if (destCheck.stdout.trim() === 'exists') { return NextResponse.json( { error: `Destination already exists and overwrite is disabled: ${destPath}` }, @@ -75,8 +85,8 @@ export async function POST(request: NextRequest) { // Move/rename const command = params.overwrite - ? `mv -f '${sourcePath}' '${destPath}'` - : `mv '${sourcePath}' '${destPath}'` + ? `mv -f '${escapedSource}' '${escapedDest}'` + : `mv '${escapedSource}' '${escapedDest}'` const result = await executeSSHCommand(client, command) if (result.exitCode !== 0) { diff --git a/apps/sim/app/api/tools/ssh/read-file-content/route.ts b/apps/sim/app/api/tools/ssh/read-file-content/route.ts index f4c6cb522fd..922c50797de 100644 --- a/apps/sim/app/api/tools/ssh/read-file-content/route.ts +++ b/apps/sim/app/api/tools/ssh/read-file-content/route.ts @@ -11,9 +11,9 @@ const ReadFileContentSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), encoding: z.string().default('utf-8'), maxSize: z.coerce.number().default(10), // MB diff --git a/apps/sim/app/api/tools/ssh/upload-file/route.ts b/apps/sim/app/api/tools/ssh/upload-file/route.ts index bbe74a14f44..feab68a2b5b 100644 --- a/apps/sim/app/api/tools/ssh/upload-file/route.ts +++ b/apps/sim/app/api/tools/ssh/upload-file/route.ts @@ -11,13 +11,13 @@ const UploadFileSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), fileContent: z.string().min(1, 'File content is required'), fileName: z.string().min(1, 'File name is required'), remotePath: z.string().min(1, 'Remote path is required'), - permissions: z.string().optional(), + permissions: z.string().nullish(), overwrite: z.boolean().default(true), }) diff --git a/apps/sim/app/api/tools/ssh/utils.ts b/apps/sim/app/api/tools/ssh/utils.ts index 4dfa1759ef7..8be9a4d4ee7 100644 --- a/apps/sim/app/api/tools/ssh/utils.ts +++ b/apps/sim/app/api/tools/ssh/utils.ts @@ -10,9 +10,9 @@ export interface SSHConnectionConfig { host: string port: number username: string - password?: string - privateKey?: string - passphrase?: string + password?: string | null + privateKey?: string | null + passphrase?: string | null timeout?: number keepaliveInterval?: number readyTimeout?: number @@ -24,32 +24,142 @@ export interface SSHCommandResult { exitCode: number } +/** + * Format SSH error with helpful troubleshooting context + */ +function formatSSHError(err: Error, config: { host: string; port: number }): Error { + const errorMessage = err.message.toLowerCase() + const host = config.host + const port = config.port + + // Connection refused - server not running or wrong port + if (errorMessage.includes('econnrefused') || errorMessage.includes('connection refused')) { + return new Error( + `Connection refused to ${host}:${port}. ` + + `Please verify: (1) SSH server is running on the target machine, ` + + `(2) Port ${port} is correct (default SSH port is 22), ` + + `(3) Firewall allows connections to port ${port}.` + ) + } + + // Connection reset - server closed connection unexpectedly + if (errorMessage.includes('econnreset') || errorMessage.includes('connection reset')) { + return new Error( + `Connection reset by ${host}:${port}. ` + + `This usually means: (1) Wrong port number (SSH default is 22), ` + + `(2) Server rejected the connection, ` + + `(3) Network/firewall interrupted the connection. ` + + `Verify your SSH server configuration and port number.` + ) + } + + // Timeout - server unreachable or slow + if (errorMessage.includes('etimedout') || errorMessage.includes('timeout')) { + return new Error( + `Connection timed out to ${host}:${port}. ` + + `Please verify: (1) Host "${host}" is reachable, ` + + `(2) No firewall is blocking the connection, ` + + `(3) The SSH server is responding.` + ) + } + + // DNS/hostname resolution + if (errorMessage.includes('enotfound') || errorMessage.includes('getaddrinfo')) { + return new Error( + `Could not resolve hostname "${host}". ` + + `Please verify the hostname or IP address is correct.` + ) + } + + // Authentication failure + if (errorMessage.includes('authentication') || errorMessage.includes('auth')) { + return new Error( + `Authentication failed for user on ${host}:${port}. ` + + `Please verify: (1) Username is correct, ` + + `(2) Password or private key is valid, ` + + `(3) User has SSH access on the server.` + ) + } + + // Private key format issues + if ( + errorMessage.includes('key') && + (errorMessage.includes('parse') || errorMessage.includes('invalid')) + ) { + return new Error( + `Invalid private key format. ` + + `Please ensure you're using a valid OpenSSH private key. ` + + `The key should start with "-----BEGIN" and end with "-----END".` + ) + } + + // Host key verification (first connection) + if (errorMessage.includes('host key') || errorMessage.includes('hostkey')) { + return new Error( + `Host key verification issue for ${host}. ` + + `This may be the first connection to this server or the server's key has changed.` + ) + } + + // Return original error with context if no specific match + return new Error(`SSH connection to ${host}:${port} failed: ${err.message}`) +} + /** * Create an SSH connection using the provided configuration + * + * Uses ssh2 library defaults which align with OpenSSH standards: + * - readyTimeout: 20000ms (20 seconds) + * - keepaliveInterval: 0 (disabled, same as OpenSSH ServerAliveInterval) + * - keepaliveCountMax: 3 (same as OpenSSH ServerAliveCountMax) */ export function createSSHConnection(config: SSHConnectionConfig): Promise { return new Promise((resolve, reject) => { const client = new Client() + const port = config.port || 22 + const host = config.host + + // Validate host + if (!host || host.trim() === '') { + reject(new Error('Host is required. Please provide a valid hostname or IP address.')) + return + } + + // Validate authentication + const hasPassword = config.password && config.password.trim() !== '' + const hasPrivateKey = config.privateKey && config.privateKey.trim() !== '' + + if (!hasPassword && !hasPrivateKey) { + reject(new Error('Authentication required. Please provide either a password or private key.')) + return + } const connectConfig: ConnectConfig = { - host: config.host, - port: config.port || 22, + host: host.trim(), + port, username: config.username, - readyTimeout: config.readyTimeout || 20000, - keepaliveInterval: config.keepaliveInterval || 10000, + // Use library defaults for timeouts (aligns with OpenSSH standards) + // readyTimeout: 20000 (default) + // keepaliveInterval: 0 (default, disabled) + // keepaliveCountMax: 3 (default) + } + + // Allow user overrides if provided + if (config.readyTimeout !== undefined) { + connectConfig.readyTimeout = config.readyTimeout + } + if (config.keepaliveInterval !== undefined) { + connectConfig.keepaliveInterval = config.keepaliveInterval } // Authentication: prioritize private key over password - if (config.privateKey) { - connectConfig.privateKey = config.privateKey - if (config.passphrase) { + if (hasPrivateKey) { + connectConfig.privateKey = config.privateKey! + if (config.passphrase && config.passphrase.trim() !== '') { connectConfig.passphrase = config.passphrase } - } else if (config.password) { - connectConfig.password = config.password - } else { - reject(new Error('Either password or privateKey must be provided')) - return + } else if (hasPassword) { + connectConfig.password = config.password! } client.on('ready', () => { @@ -57,10 +167,14 @@ export function createSSHConnection(config: SSHConnectionConfig): Promise { - reject(err) + reject(formatSSHError(err, { host, port })) }) - client.connect(connectConfig) + try { + client.connect(connectConfig) + } catch (err) { + reject(formatSSHError(err instanceof Error ? err : new Error(String(err)), { host, port })) + } }) } @@ -106,7 +220,7 @@ export function sanitizeCommand(command: string): string { } /** - * Sanitize file path to prevent directory traversal + * Sanitize file path - removes null bytes and trims whitespace */ export function sanitizePath(path: string): string { // Remove any null bytes @@ -118,6 +232,20 @@ export function sanitizePath(path: string): string { return sanitized } +/** + * Escape a string for safe use in single-quoted shell arguments + * This is standard practice for shell command construction. + * e.g., "/tmp/test'file" becomes "/tmp/test'\''file" + * + * The pattern 'foo'\''bar' works because: + * - First ' ends the current single-quoted string + * - \' inserts a literal single quote (escaped outside quotes) + * - Next ' starts a new single-quoted string + */ +export function escapeShellArg(arg: string): string { + return arg.replace(/'/g, "'\\''") +} + /** * Validate that authentication credentials are provided */ diff --git a/apps/sim/app/api/tools/ssh/write-file-content/route.ts b/apps/sim/app/api/tools/ssh/write-file-content/route.ts index 455ecdf2968..5ba72740159 100644 --- a/apps/sim/app/api/tools/ssh/write-file-content/route.ts +++ b/apps/sim/app/api/tools/ssh/write-file-content/route.ts @@ -11,13 +11,13 @@ const WriteFileContentSchema = z.object({ host: z.string().min(1, 'Host is required'), port: z.coerce.number().int().positive().default(22), username: z.string().min(1, 'Username is required'), - password: z.string().optional(), - privateKey: z.string().optional(), - passphrase: z.string().optional(), + password: z.string().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), path: z.string().min(1, 'Path is required'), content: z.string(), mode: z.enum(['overwrite', 'append', 'create']).default('overwrite'), - permissions: z.string().optional(), + permissions: z.string().nullish(), }) function getSFTP(client: Client): Promise { 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 e98c3a880bd..814c6dca553 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 @@ -232,6 +232,20 @@ const SCOPE_DESCRIPTIONS: Record = { 'webhooks:read': 'Read your Pipedrive webhooks', 'webhooks:full': 'Full access to manage your Pipedrive webhooks', w_member_social: 'Access your LinkedIn profile', + // Box scopes + root_readwrite: 'Read and write all files and folders in your Box account', + root_readonly: 'Read all files and folders in your Box account', + // Shopify scopes + read_products: 'View your Shopify products', + write_products: 'Create and manage your Shopify products', + read_orders: 'View your Shopify orders', + write_orders: 'Manage your Shopify orders', + read_customers: 'View your Shopify customers', + write_customers: 'Create and manage your Shopify customers', + read_inventory: 'View your Shopify inventory', + write_inventory: 'Manage your Shopify inventory levels', + read_fulfillments: 'View your Shopify fulfillments', + write_fulfillments: 'Create and manage your Shopify fulfillments', } function getScopeDescription(scope: string): string { @@ -289,6 +303,11 @@ export function OAuthRequiredModal({ return } + if (providerId === 'shopify') { + window.location.href = '/api/auth/shopify/authorize' + return + } + await client.oauth2.link({ providerId, callbackURL: window.location.href, diff --git a/apps/sim/blocks/blocks/ahrefs.ts b/apps/sim/blocks/blocks/ahrefs.ts index 85c62c9689b..bd62608ae8b 100644 --- a/apps/sim/blocks/blocks/ahrefs.ts +++ b/apps/sim/blocks/blocks/ahrefs.ts @@ -40,6 +40,13 @@ export const AhrefsBlock: BlockConfig = { condition: { field: 'operation', value: 'ahrefs_domain_rating' }, required: true, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_domain_rating' }, + }, // Backlinks operation inputs { id: 'target', @@ -76,6 +83,13 @@ export const AhrefsBlock: BlockConfig = { placeholder: '0', condition: { field: 'operation', value: 'ahrefs_backlinks' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_backlinks' }, + }, // Backlinks Stats operation inputs { id: 'target', @@ -98,6 +112,13 @@ export const AhrefsBlock: BlockConfig = { value: () => 'domain', condition: { field: 'operation', value: 'ahrefs_backlinks_stats' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_backlinks_stats' }, + }, // Referring Domains operation inputs { id: 'target', @@ -134,6 +155,13 @@ export const AhrefsBlock: BlockConfig = { placeholder: '0', condition: { field: 'operation', value: 'ahrefs_referring_domains' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_referring_domains' }, + }, // Organic Keywords operation inputs { id: 'target', @@ -194,6 +222,13 @@ export const AhrefsBlock: BlockConfig = { placeholder: '0', condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_organic_keywords' }, + }, // Top Pages operation inputs { id: 'target', @@ -253,6 +288,13 @@ export const AhrefsBlock: BlockConfig = { placeholder: '0', condition: { field: 'operation', value: 'ahrefs_top_pages' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_top_pages' }, + }, // Keyword Overview operation inputs { id: 'keyword', @@ -322,6 +364,13 @@ export const AhrefsBlock: BlockConfig = { placeholder: '0', condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, }, + { + id: 'date', + title: 'Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD (defaults to today)', + condition: { field: 'operation', value: 'ahrefs_broken_backlinks' }, + }, // API Key (common to all operations) { id: 'apiKey', @@ -383,6 +432,7 @@ export const AhrefsBlock: BlockConfig = { keyword: { type: 'string', description: 'Keyword to analyze' }, mode: { type: 'string', description: 'Analysis mode (domain, prefix, subdomains, exact)' }, country: { type: 'string', description: 'Country code for geo-specific data' }, + date: { type: 'string', description: 'Date for historical data in YYYY-MM-DD format' }, limit: { type: 'number', description: 'Maximum number of results to return' }, offset: { type: 'number', description: 'Number of results to skip for pagination' }, }, diff --git a/apps/sim/blocks/blocks/box.ts b/apps/sim/blocks/blocks/box.ts deleted file mode 100644 index 69ec29cd1d0..00000000000 --- a/apps/sim/blocks/blocks/box.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { BoxCompanyIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' -import { AuthMode } from '@/blocks/types' -import type { BoxResponse } from '@/tools/box/types' - -export const BoxBlock: BlockConfig = { - type: 'box', - name: 'Box', - description: 'Manage files and folders in Box cloud storage', - authMode: AuthMode.OAuth, - longDescription: - 'Integrate Box into the workflow. Can manage files and folders, search content, create shared links, and collaborate with users. Supports file operations like copy, move, delete, and download.', - docsLink: 'https://docs.sim.ai/tools/box', - category: 'tools', - icon: BoxCompanyIcon, - bgColor: '#0061D5', - subBlocks: [ - { - id: 'operation', - title: 'Operation', - type: 'dropdown', - options: [ - // File Operations - { label: 'Get File Info', id: 'box_get_file', group: 'Files' }, - { label: 'Download File', id: 'box_download_file', group: 'Files' }, - { label: 'Update File', id: 'box_update_file', group: 'Files' }, - { label: 'Copy File', id: 'box_copy_file', group: 'Files' }, - { label: 'Delete File', id: 'box_delete_file', group: 'Files' }, - // Folder Operations - { label: 'List Folder', id: 'box_list_folder', group: 'Folders' }, - { label: 'Create Folder', id: 'box_create_folder', group: 'Folders' }, - { label: 'Delete Folder', id: 'box_delete_folder', group: 'Folders' }, - // Search - { label: 'Search', id: 'box_search', group: 'Search' }, - // Sharing - { label: 'Create Shared Link', id: 'box_create_shared_link', group: 'Sharing' }, - { label: 'Create Collaboration', id: 'box_create_collaboration', group: 'Sharing' }, - // User - { label: 'Get Current User', id: 'box_get_current_user', group: 'User' }, - ], - grouped: true, - }, - // OAuth Credential - { - id: 'credential', - title: 'Box Account', - type: 'oauth-input', - serviceId: 'box', - requiredScopes: ['root_readwrite'], - placeholder: 'Select Box account', - required: true, - }, - // File ID input (for file operations) - { - id: 'fileId', - title: 'File ID', - type: 'short-input', - placeholder: 'Enter file ID', - condition: { - field: 'operation', - value: [ - 'box_get_file', - 'box_download_file', - 'box_update_file', - 'box_copy_file', - 'box_delete_file', - ], - }, - required: { - field: 'operation', - value: [ - 'box_get_file', - 'box_download_file', - 'box_update_file', - 'box_copy_file', - 'box_delete_file', - ], - }, - }, - // Folder ID input (for folder operations) - { - id: 'folderId', - title: 'Folder ID', - type: 'short-input', - placeholder: 'Enter folder ID (0 for root)', - condition: { - field: 'operation', - value: ['box_list_folder', 'box_delete_folder'], - }, - required: { - field: 'operation', - value: ['box_list_folder', 'box_delete_folder'], - }, - }, - // Parent Folder ID (for create folder and copy) - { - id: 'parentFolderId', - title: 'Parent Folder ID', - type: 'short-input', - placeholder: 'Enter parent folder ID (0 for root)', - condition: { - field: 'operation', - value: ['box_create_folder', 'box_copy_file'], - }, - required: { - field: 'operation', - value: ['box_create_folder', 'box_copy_file'], - }, - }, - // Folder Name (for create folder) - { - id: 'folderName', - title: 'Folder Name', - type: 'short-input', - placeholder: 'Enter folder name', - condition: { field: 'operation', value: 'box_create_folder' }, - required: { field: 'operation', value: 'box_create_folder' }, - }, - // New Name (for update file and copy) - { - id: 'name', - title: 'New Name', - type: 'short-input', - placeholder: 'Enter new name (optional)', - condition: { - field: 'operation', - value: ['box_update_file', 'box_copy_file'], - }, - }, - // Description (for update file) - { - id: 'description', - title: 'Description', - type: 'long-input', - placeholder: 'Enter description (optional)', - condition: { field: 'operation', value: 'box_update_file' }, - }, - // Parent ID for move (update file) - { - id: 'parentId', - title: 'Move to Folder ID', - type: 'short-input', - placeholder: 'Enter destination folder ID to move file', - condition: { field: 'operation', value: 'box_update_file' }, - }, - // Tags (for update file) - { - id: 'tags', - title: 'Tags', - type: 'short-input', - placeholder: 'Comma-separated tags (optional)', - condition: { field: 'operation', value: 'box_update_file' }, - }, - // Search Query - { - id: 'query', - title: 'Search Query', - type: 'short-input', - placeholder: 'Enter search query', - condition: { field: 'operation', value: 'box_search' }, - required: { field: 'operation', value: 'box_search' }, - }, - // Search Scope - { - id: 'scope', - title: 'Search Scope', - type: 'dropdown', - options: [ - { label: 'User Content', id: 'user_content' }, - { label: 'Enterprise Content', id: 'enterprise_content' }, - ], - condition: { field: 'operation', value: 'box_search' }, - }, - // File Extensions filter - { - id: 'fileExtensions', - title: 'File Extensions', - type: 'short-input', - placeholder: 'e.g., pdf,docx,xlsx', - condition: { field: 'operation', value: 'box_search' }, - }, - // Limit - { - id: 'limit', - title: 'Limit', - type: 'short-input', - placeholder: 'Max results (default: 30)', - condition: { - field: 'operation', - value: ['box_list_folder', 'box_search'], - }, - }, - // Offset - { - id: 'offset', - title: 'Offset', - type: 'short-input', - placeholder: 'Offset for pagination', - condition: { - field: 'operation', - value: ['box_list_folder', 'box_search'], - }, - }, - // Recursive delete (for folder) - { - id: 'recursive', - title: 'Delete Contents', - type: 'switch', - defaultValue: true, - condition: { field: 'operation', value: 'box_delete_folder' }, - }, - // Permanent delete (for file) - { - id: 'permanent', - title: 'Permanent Delete', - type: 'switch', - defaultValue: false, - condition: { field: 'operation', value: 'box_delete_file' }, - }, - // Shared Link - Item ID - { - id: 'itemId', - title: 'Item ID', - type: 'short-input', - placeholder: 'Enter file or folder ID', - condition: { - field: 'operation', - value: ['box_create_shared_link', 'box_create_collaboration'], - }, - required: { - field: 'operation', - value: ['box_create_shared_link', 'box_create_collaboration'], - }, - }, - // Item Type (for shared link and collaboration) - { - id: 'itemType', - title: 'Item Type', - type: 'dropdown', - options: [ - { label: 'File', id: 'file' }, - { label: 'Folder', id: 'folder' }, - ], - condition: { - field: 'operation', - value: ['box_create_shared_link', 'box_create_collaboration'], - }, - required: { - field: 'operation', - value: ['box_create_shared_link', 'box_create_collaboration'], - }, - }, - // Shared Link Access Level - { - id: 'access', - title: 'Access Level', - type: 'dropdown', - options: [ - { label: 'Open (Anyone with link)', id: 'open' }, - { label: 'Company (Only company members)', id: 'company' }, - { label: 'Collaborators Only', id: 'collaborators' }, - ], - condition: { field: 'operation', value: 'box_create_shared_link' }, - }, - // Shared Link Password - { - id: 'password', - title: 'Password', - type: 'short-input', - placeholder: 'Optional password protection', - password: true, - condition: { field: 'operation', value: 'box_create_shared_link' }, - }, - // Shared Link Expiration - { - id: 'unsharedAt', - title: 'Expiration Date', - type: 'short-input', - placeholder: 'ISO 8601 date (e.g., 2024-12-31)', - condition: { field: 'operation', value: 'box_create_shared_link' }, - }, - // Can Download - { - id: 'canDownload', - title: 'Allow Download', - type: 'switch', - defaultValue: true, - condition: { field: 'operation', value: 'box_create_shared_link' }, - }, - // Can Preview - { - id: 'canPreview', - title: 'Allow Preview', - type: 'switch', - defaultValue: true, - condition: { field: 'operation', value: 'box_create_shared_link' }, - }, - // Collaboration - Email - { - id: 'accessibleByLogin', - title: 'User Email', - type: 'short-input', - placeholder: 'Enter user email to share with', - condition: { field: 'operation', value: 'box_create_collaboration' }, - }, - // Collaboration - Type - { - id: 'accessibleByType', - title: 'Collaborator Type', - type: 'dropdown', - options: [ - { label: 'User', id: 'user' }, - { label: 'Group', id: 'group' }, - ], - condition: { field: 'operation', value: 'box_create_collaboration' }, - required: { field: 'operation', value: 'box_create_collaboration' }, - }, - // Collaboration - Role - { - id: 'role', - title: 'Role', - type: 'dropdown', - options: [ - { label: 'Editor', id: 'editor' }, - { label: 'Viewer', id: 'viewer' }, - { label: 'Previewer', id: 'previewer' }, - { label: 'Uploader', id: 'uploader' }, - { label: 'Co-Owner', id: 'co-owner' }, - ], - condition: { field: 'operation', value: 'box_create_collaboration' }, - required: { field: 'operation', value: 'box_create_collaboration' }, - }, - ], - tools: { - access: [ - 'box_get_file', - 'box_list_folder', - 'box_create_folder', - 'box_delete_file', - 'box_delete_folder', - 'box_copy_file', - 'box_update_file', - 'box_search', - 'box_download_file', - 'box_get_current_user', - 'box_create_shared_link', - 'box_create_collaboration', - ], - config: { - tool: (params) => params.operation || 'box_list_folder', - params: (params) => { - const baseParams: Record = {} - - switch (params.operation) { - case 'box_get_file': - case 'box_download_file': - return { fileId: params.fileId } - - case 'box_update_file': - return { - fileId: params.fileId, - name: params.name || undefined, - description: params.description || undefined, - parentId: params.parentId || undefined, - tags: params.tags ? params.tags.split(',').map((t: string) => t.trim()) : undefined, - } - - case 'box_copy_file': - return { - fileId: params.fileId, - parentFolderId: params.parentFolderId, - newName: params.name || undefined, - } - - case 'box_delete_file': - return { - fileId: params.fileId, - permanent: params.permanent || false, - } - - case 'box_list_folder': - return { - folderId: params.folderId || '0', - limit: params.limit ? Number(params.limit) : undefined, - offset: params.offset ? Number(params.offset) : undefined, - } - - case 'box_create_folder': - return { - parentFolderId: params.parentFolderId || '0', - folderName: params.folderName, - } - - case 'box_delete_folder': - return { - folderId: params.folderId, - recursive: params.recursive !== false, - } - - case 'box_search': - return { - query: params.query, - scope: params.scope || undefined, - fileExtensions: params.fileExtensions || undefined, - limit: params.limit ? Number(params.limit) : undefined, - offset: params.offset ? Number(params.offset) : undefined, - } - - case 'box_get_current_user': - return {} - - case 'box_create_shared_link': - return { - itemId: params.itemId, - itemType: params.itemType, - access: params.access || undefined, - password: params.password || undefined, - unsharedAt: params.unsharedAt || undefined, - canDownload: params.canDownload, - canPreview: params.canPreview, - } - - case 'box_create_collaboration': - return { - itemId: params.itemId, - itemType: params.itemType, - accessibleByLogin: params.accessibleByLogin || undefined, - accessibleByType: params.accessibleByType, - role: params.role, - } - - default: - return baseParams - } - }, - }, - }, - inputs: { - operation: { type: 'string', description: 'The Box operation to perform' }, - credential: { type: 'string', description: 'Box OAuth credential' }, - fileId: { type: 'string', description: 'File ID' }, - folderId: { type: 'string', description: 'Folder ID' }, - parentFolderId: { type: 'string', description: 'Parent folder ID' }, - folderName: { type: 'string', description: 'Folder name' }, - name: { type: 'string', description: 'New name for file/folder' }, - description: { type: 'string', description: 'Description' }, - parentId: { type: 'string', description: 'Destination folder ID for move' }, - query: { type: 'string', description: 'Search query' }, - scope: { type: 'string', description: 'Search scope' }, - fileExtensions: { type: 'string', description: 'File extensions filter' }, - limit: { type: 'number', description: 'Maximum results' }, - offset: { type: 'number', description: 'Pagination offset' }, - recursive: { type: 'boolean', description: 'Delete folder contents recursively' }, - permanent: { type: 'boolean', description: 'Permanently delete file' }, - itemId: { type: 'string', description: 'Item ID for sharing' }, - itemType: { type: 'string', description: 'Item type (file or folder)' }, - access: { type: 'string', description: 'Shared link access level' }, - password: { type: 'string', description: 'Shared link password' }, - unsharedAt: { type: 'string', description: 'Shared link expiration date' }, - canDownload: { type: 'boolean', description: 'Allow download via shared link' }, - canPreview: { type: 'boolean', description: 'Allow preview via shared link' }, - tags: { type: 'string', description: 'Comma-separated tags for file' }, - accessibleByLogin: { type: 'string', description: 'Email of user to share with' }, - accessibleByType: { type: 'string', description: 'Type of collaborator' }, - role: { type: 'string', description: 'Collaboration role' }, - }, - outputs: { - file: { - type: 'json', - description: 'File information', - }, - folder: { - type: 'json', - description: 'Folder information', - }, - items: { - type: 'json', - description: 'List of files and folders', - }, - results: { - type: 'json', - description: 'Search results', - }, - sharedLink: { - type: 'json', - description: 'Shared link information', - }, - collaboration: { - type: 'json', - description: 'Collaboration information', - }, - user: { - type: 'json', - description: 'User information', - }, - success: { - type: 'boolean', - description: 'Whether the operation was successful', - }, - totalCount: { - type: 'number', - description: 'Total count of items', - }, - downloadUrl: { - type: 'string', - description: 'Download URL for file', - }, - content: { - type: 'string', - description: 'File content (for text files)', - }, - }, -} diff --git a/apps/sim/blocks/blocks/datadog.ts b/apps/sim/blocks/blocks/datadog.ts index 8a713dd398b..32f1134c5e0 100644 --- a/apps/sim/blocks/blocks/datadog.ts +++ b/apps/sim/blocks/blocks/datadog.ts @@ -383,12 +383,27 @@ export const DatadogBlock: BlockConfig = { password: true, required: true, }, + // Application Key - REQUIRED only for read/manage operations (not needed for submit_metrics, create_event, send_logs) { id: 'applicationKey', title: 'Application Key', type: 'short-input', placeholder: 'Enter your Datadog application key', password: true, + condition: { + field: 'operation', + value: [ + 'datadog_query_timeseries', + 'datadog_create_monitor', + 'datadog_get_monitor', + 'datadog_list_monitors', + 'datadog_mute_monitor', + 'datadog_query_logs', + 'datadog_create_downtime', + 'datadog_list_downtimes', + 'datadog_cancel_downtime', + ], + }, required: true, }, { diff --git a/apps/sim/blocks/blocks/gitlab.ts b/apps/sim/blocks/blocks/gitlab.ts index 350e2e64a86..743d7939123 100644 --- a/apps/sim/blocks/blocks/gitlab.ts +++ b/apps/sim/blocks/blocks/gitlab.ts @@ -7,7 +7,7 @@ export const GitLabBlock: BlockConfig = { type: 'gitlab', name: 'GitLab', description: 'Interact with GitLab projects, issues, merge requests, and pipelines', - authMode: AuthMode.OAuth, + authMode: AuthMode.ApiKey, triggerAllowed: false, longDescription: 'Integrate GitLab into the workflow. Can manage projects, issues, merge requests, pipelines, and add comments. Supports all core GitLab DevOps operations.', @@ -48,12 +48,11 @@ export const GitLabBlock: BlockConfig = { value: () => 'gitlab_list_projects', }, { - id: 'credential', - title: 'GitLab Account', - type: 'oauth-input', - serviceId: 'gitlab', - requiredScopes: ['api', 'read_user', 'read_repository', 'write_repository'], - placeholder: 'Select GitLab account', + id: 'accessToken', + title: 'Personal Access Token', + type: 'short-input', + placeholder: 'Enter your GitLab Personal Access Token', + password: true, required: true, }, // Project ID (required for most operations) @@ -87,12 +86,12 @@ export const GitLabBlock: BlockConfig = { ], }, }, - // Issue IID (for issue operations) + // Issue Number (IID) - the # shown in GitLab UI { id: 'issueIid', - title: 'Issue IID', + title: 'Issue Number', type: 'short-input', - placeholder: 'Enter issue internal ID (IID)', + placeholder: 'Enter issue number (e.g., 1 for issue #1)', required: true, condition: { field: 'operation', @@ -104,12 +103,12 @@ export const GitLabBlock: BlockConfig = { ], }, }, - // Merge Request IID + // Merge Request Number (IID) - the ! number shown in GitLab UI { id: 'mergeRequestIid', - title: 'Merge Request IID', + title: 'MR Number', type: 'short-input', - placeholder: 'Enter merge request internal ID (IID)', + placeholder: 'Enter MR number (e.g., 1 for !1)', required: true, condition: { field: 'operation', @@ -415,7 +414,7 @@ export const GitLabBlock: BlockConfig = { }, params: (params) => { const baseParams: Record = { - credential: params.credential, + accessToken: params.accessToken, } switch (params.operation) { @@ -450,7 +449,7 @@ export const GitLabBlock: BlockConfig = { case 'gitlab_get_issue': if (!params.projectId?.trim() || !params.issueIid) { - throw new Error('Project ID and Issue IID are required.') + throw new Error('Project ID and Issue Number are required.') } return { ...baseParams, diff --git a/apps/sim/blocks/blocks/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts new file mode 100644 index 00000000000..0623b1c6cbe --- /dev/null +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -0,0 +1,227 @@ +import { KalshiIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const KalshiBlock: BlockConfig = { + type: 'kalshi', + name: 'Kalshi', + description: 'Access prediction markets data from Kalshi', + longDescription: + 'Integrate Kalshi prediction markets into the workflow. Can get markets, get market, get events, get event, get balance, get positions, get orders.', + docsLink: 'https://docs.sim.ai/tools/kalshi', + authMode: AuthMode.ApiKey, + category: 'tools', + bgColor: '#09C285', + icon: KalshiIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Markets', id: 'get_markets' }, + { label: 'Get Market', id: 'get_market' }, + { label: 'Get Events', id: 'get_events' }, + { label: 'Get Event', id: 'get_event' }, + { label: 'Get Balance', id: 'get_balance' }, + { label: 'Get Positions', id: 'get_positions' }, + { label: 'Get Orders', id: 'get_orders' }, + ], + value: () => 'get_markets', + }, + // Auth fields (for authenticated operations) + { + id: 'keyId', + title: 'API Key ID', + type: 'short-input', + placeholder: 'Your Kalshi API Key ID', + condition: { field: 'operation', value: ['get_balance', 'get_positions', 'get_orders'] }, + required: true, + }, + { + id: 'privateKey', + title: 'Private Key', + type: 'long-input', + password: true, + placeholder: 'Your RSA Private Key (PEM format)', + condition: { field: 'operation', value: ['get_balance', 'get_positions', 'get_orders'] }, + required: true, + }, + // Get Markets fields + { + id: 'status', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Unopened', id: 'unopened' }, + { label: 'Open', id: 'open' }, + { label: 'Closed', id: 'closed' }, + { label: 'Settled', id: 'settled' }, + ], + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'seriesTicker', + title: 'Series Ticker', + type: 'short-input', + placeholder: 'Filter by series ticker', + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'eventTicker', + title: 'Event Ticker', + type: 'short-input', + placeholder: 'Event ticker', + required: { + field: 'operation', + value: ['get_event'], + }, + condition: { + field: 'operation', + value: ['get_markets', 'get_event', 'get_positions', 'get_orders'], + }, + }, + // Get Market fields - ticker is REQUIRED for get_market (path param) + { + id: 'ticker', + title: 'Market Ticker', + type: 'short-input', + placeholder: 'Market ticker (e.g., KXBTC-24DEC31)', + required: true, + condition: { field: 'operation', value: ['get_market'] }, + }, + // Ticker filter for get_orders and get_positions - OPTIONAL + { + id: 'tickerFilter', + title: 'Market Ticker', + type: 'short-input', + placeholder: 'Filter by market ticker (optional)', + condition: { field: 'operation', value: ['get_orders', 'get_positions'] }, + }, + // Nested markets option + { + id: 'withNestedMarkets', + title: 'Include Markets', + type: 'dropdown', + options: [ + { label: 'No', id: '' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: ['get_events', 'get_event'] }, + }, + // Get Positions fields + { + id: 'settlementStatus', + title: 'Settlement Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Unsettled', id: 'unsettled' }, + { label: 'Settled', id: 'settled' }, + ], + condition: { field: 'operation', value: ['get_positions'] }, + }, + // Get Orders fields + { + id: 'orderStatus', + title: 'Order Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Resting', id: 'resting' }, + { label: 'Canceled', id: 'canceled' }, + { label: 'Executed', id: 'executed' }, + ], + condition: { field: 'operation', value: ['get_orders'] }, + }, + // Pagination fields + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of results (1-1000, default: 100)', + condition: { + field: 'operation', + value: ['get_markets', 'get_events', 'get_positions', 'get_orders'], + }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor', + condition: { + field: 'operation', + value: ['get_markets', 'get_events', 'get_positions', 'get_orders'], + }, + }, + ], + tools: { + access: [ + 'kalshi_get_markets', + 'kalshi_get_market', + 'kalshi_get_events', + 'kalshi_get_event', + 'kalshi_get_balance', + 'kalshi_get_positions', + 'kalshi_get_orders', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'get_markets': + return 'kalshi_get_markets' + case 'get_market': + return 'kalshi_get_market' + case 'get_events': + return 'kalshi_get_events' + case 'get_event': + return 'kalshi_get_event' + case 'get_balance': + return 'kalshi_get_balance' + case 'get_positions': + return 'kalshi_get_positions' + case 'get_orders': + return 'kalshi_get_orders' + default: + return 'kalshi_get_markets' + } + }, + params: (params) => { + const { operation, orderStatus, tickerFilter, ...rest } = params + const cleanParams: Record = {} + + // Map orderStatus to status for get_orders + if (operation === 'get_orders' && orderStatus) { + cleanParams.status = orderStatus + } + + // Map tickerFilter to ticker for get_orders and get_positions + if ((operation === 'get_orders' || operation === 'get_positions') && tickerFilter) { + cleanParams.ticker = tickerFilter + } + + Object.entries(rest).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + cleanParams[key] = value + } + }) + + return cleanParams + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + keyId: { type: 'string', description: 'Kalshi API Key ID' }, + privateKey: { type: 'string', description: 'RSA Private Key (PEM format)' }, + ticker: { type: 'string', description: 'Market ticker' }, + eventTicker: { type: 'string', description: 'Event ticker' }, + status: { type: 'string', description: 'Filter by status' }, + }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { type: 'json', description: 'Operation result data' }, + }, +} diff --git a/apps/sim/blocks/blocks/polymarket.ts b/apps/sim/blocks/blocks/polymarket.ts new file mode 100644 index 00000000000..334331720ed --- /dev/null +++ b/apps/sim/blocks/blocks/polymarket.ts @@ -0,0 +1,259 @@ +import { PolymarketIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const PolymarketBlock: BlockConfig = { + type: 'polymarket', + name: 'Polymarket', + description: 'Access prediction markets data from Polymarket', + longDescription: + 'Integrate Polymarket prediction markets into the workflow. Can get markets, get market, get events, get event, get orderbook, get price, get midpoint, and get price history.', + docsLink: 'https://docs.sim.ai/tools/polymarket', + category: 'tools', + bgColor: '#4C82FB', + icon: PolymarketIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Markets', id: 'get_markets' }, + { label: 'Get Market', id: 'get_market' }, + { label: 'Get Events', id: 'get_events' }, + { label: 'Get Event', id: 'get_event' }, + { label: 'Get Orderbook', id: 'get_orderbook' }, + { label: 'Get Price', id: 'get_price' }, + { label: 'Get Midpoint', id: 'get_midpoint' }, + { label: 'Get Price History', id: 'get_price_history' }, + ], + value: () => 'get_markets', + }, + // Get Market fields - marketId or slug (one is required) + { + id: 'marketId', + title: 'Market ID', + type: 'short-input', + placeholder: 'Market ID (required if no slug)', + condition: { field: 'operation', value: ['get_market'] }, + }, + { + id: 'marketSlug', + title: 'Market Slug', + type: 'short-input', + placeholder: 'Market slug (required if no ID)', + condition: { field: 'operation', value: ['get_market'] }, + }, + // Get Event fields - eventId or slug (one is required) + { + id: 'eventId', + title: 'Event ID', + type: 'short-input', + placeholder: 'Event ID (required if no slug)', + condition: { field: 'operation', value: ['get_event'] }, + }, + { + id: 'eventSlug', + title: 'Event Slug', + type: 'short-input', + placeholder: 'Event slug (required if no ID)', + condition: { field: 'operation', value: ['get_event'] }, + }, + // Token ID for CLOB operations + { + id: 'tokenId', + title: 'Token ID', + type: 'short-input', + placeholder: 'CLOB Token ID from market', + required: true, + condition: { + field: 'operation', + value: ['get_orderbook', 'get_price', 'get_midpoint', 'get_price_history'], + }, + }, + // Side for price query + { + id: 'side', + title: 'Side', + type: 'dropdown', + options: [ + { label: 'Buy', id: 'buy' }, + { label: 'Sell', id: 'sell' }, + ], + condition: { field: 'operation', value: ['get_price'] }, + required: true, + }, + // Price history specific fields + { + id: 'interval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'None (use timestamps)', id: '' }, + { label: '1 Minute', id: '1m' }, + { label: '1 Hour', id: '1h' }, + { label: '6 Hours', id: '6h' }, + { label: '1 Day', id: '1d' }, + { label: '1 Week', id: '1w' }, + { label: 'Max', id: 'max' }, + ], + condition: { field: 'operation', value: ['get_price_history'] }, + }, + { + id: 'fidelity', + title: 'Fidelity (minutes)', + type: 'short-input', + placeholder: 'Data resolution in minutes (e.g., 60)', + condition: { field: 'operation', value: ['get_price_history'] }, + }, + { + id: 'startTs', + title: 'Start Timestamp', + type: 'short-input', + placeholder: 'Unix timestamp UTC (if no interval)', + condition: { field: 'operation', value: ['get_price_history'] }, + }, + { + id: 'endTs', + title: 'End Timestamp', + type: 'short-input', + placeholder: 'Unix timestamp UTC (if no interval)', + condition: { field: 'operation', value: ['get_price_history'] }, + }, + // Filters for list operations + { + id: 'closed', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Active Only', id: 'false' }, + { label: 'Closed Only', id: 'true' }, + ], + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'order', + title: 'Sort By', + type: 'short-input', + placeholder: 'Sort field (e.g., id, volume, liquidity)', + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'ascending', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Descending (newest first)', id: 'false' }, + { label: 'Ascending (oldest first)', id: 'true' }, + ], + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'tagId', + title: 'Tag ID', + type: 'short-input', + placeholder: 'Filter by tag ID', + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + // Pagination fields + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Number of results (recommended: 25-50)', + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + placeholder: 'Pagination offset', + condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + }, + ], + tools: { + access: [ + 'polymarket_get_markets', + 'polymarket_get_market', + 'polymarket_get_events', + 'polymarket_get_event', + 'polymarket_get_orderbook', + 'polymarket_get_price', + 'polymarket_get_midpoint', + 'polymarket_get_price_history', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'get_markets': + return 'polymarket_get_markets' + case 'get_market': + return 'polymarket_get_market' + case 'get_events': + return 'polymarket_get_events' + case 'get_event': + return 'polymarket_get_event' + case 'get_orderbook': + return 'polymarket_get_orderbook' + case 'get_price': + return 'polymarket_get_price' + case 'get_midpoint': + return 'polymarket_get_midpoint' + case 'get_price_history': + return 'polymarket_get_price_history' + default: + return 'polymarket_get_markets' + } + }, + params: (params) => { + const { operation, marketSlug, eventSlug, ...rest } = params + const cleanParams: Record = {} + + // Map marketSlug to slug for get_market + if (operation === 'get_market' && marketSlug) { + cleanParams.slug = marketSlug + } + + // Map eventSlug to slug for get_event + if (operation === 'get_event' && eventSlug) { + cleanParams.slug = eventSlug + } + + // Convert numeric fields from string to number for get_price_history + if (operation === 'get_price_history') { + if (rest.fidelity) cleanParams.fidelity = Number(rest.fidelity) + if (rest.startTs) cleanParams.startTs = Number(rest.startTs) + if (rest.endTs) cleanParams.endTs = Number(rest.endTs) + rest.fidelity = undefined + rest.startTs = undefined + rest.endTs = undefined + } + + Object.entries(rest).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + cleanParams[key] = value + } + }) + + return cleanParams + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + marketId: { type: 'string', description: 'Market ID' }, + marketSlug: { type: 'string', description: 'Market slug' }, + eventId: { type: 'string', description: 'Event ID' }, + eventSlug: { type: 'string', description: 'Event slug' }, + tokenId: { type: 'string', description: 'CLOB Token ID' }, + side: { type: 'string', description: 'Order side (buy/sell)' }, + interval: { type: 'string', description: 'Price history interval' }, + fidelity: { type: 'number', description: 'Data resolution in minutes' }, + startTs: { type: 'number', description: 'Start timestamp (Unix)' }, + endTs: { type: 'number', description: 'End timestamp (Unix)' }, + }, + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { type: 'json', description: 'Operation result data' }, + }, +} diff --git a/apps/sim/blocks/blocks/shopify.ts b/apps/sim/blocks/blocks/shopify.ts index cee32bfec78..f614f5a3068 100644 --- a/apps/sim/blocks/blocks/shopify.ts +++ b/apps/sim/blocks/blocks/shopify.ts @@ -18,7 +18,7 @@ export const ShopifyBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/shopify', category: 'tools', icon: ShopifyIcon, - bgColor: '#96BF48', + bgColor: '#FFFFFF', subBlocks: [ { id: 'operation', @@ -70,8 +70,8 @@ export const ShopifyBlock: BlockConfig = { id: 'shopDomain', title: 'Shop Domain', type: 'short-input', - placeholder: 'mystore.myshopify.com', - required: true, + placeholder: 'Auto-detected from OAuth or enter manually', + // Not required - will use shop domain from OAuth credential if not provided }, // Product ID (for get/update/delete operations) { diff --git a/apps/sim/blocks/blocks/ssh.ts b/apps/sim/blocks/blocks/ssh.ts index ae68dcc6b74..924b26c457d 100644 --- a/apps/sim/blocks/blocks/ssh.ts +++ b/apps/sim/blocks/blocks/ssh.ts @@ -12,7 +12,7 @@ export const SSHBlock: BlockConfig = { 'Execute commands, transfer files, and manage remote servers via SSH. Supports password and private key authentication for secure server access.', docsLink: 'https://docs.sim.ai/tools/ssh', category: 'tools', - bgColor: '#2C3E50', + bgColor: '#000000', icon: SshIcon, subBlocks: [ // Operation selector diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index daacbf6218f..3a411614e3e 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -7,7 +7,7 @@ import { ApifyBlock } from '@/blocks/blocks/apify' import { ApolloBlock } from '@/blocks/blocks/apollo' import { ArxivBlock } from '@/blocks/blocks/arxiv' import { AsanaBlock } from '@/blocks/blocks/asana' -import { BoxBlock } from '@/blocks/blocks/box' +// import { BoxBlock } from '@/blocks/blocks/box' // TODO: Box OAuth integration import { BrowserUseBlock } from '@/blocks/blocks/browser_use' import { CalendlyBlock } from '@/blocks/blocks/calendly' import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger' @@ -48,6 +48,7 @@ import { InputTriggerBlock } from '@/blocks/blocks/input_trigger' import { IntercomBlock } from '@/blocks/blocks/intercom' import { JinaBlock } from '@/blocks/blocks/jina' import { JiraBlock } from '@/blocks/blocks/jira' +import { KalshiBlock } from '@/blocks/blocks/kalshi' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LinearBlock } from '@/blocks/blocks/linear' import { LinkedInBlock } from '@/blocks/blocks/linkedin' @@ -74,6 +75,7 @@ import { ParallelBlock } from '@/blocks/blocks/parallel' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' import { PipedriveBlock } from '@/blocks/blocks/pipedrive' +import { PolymarketBlock } from '@/blocks/blocks/polymarket' import { PostgreSQLBlock } from '@/blocks/blocks/postgresql' import { PostHogBlock } from '@/blocks/blocks/posthog' import { PylonBlock } from '@/blocks/blocks/pylon' @@ -141,7 +143,7 @@ export const registry: Record = { apollo: ApolloBlock, arxiv: ArxivBlock, asana: AsanaBlock, - box: BoxBlock, + // box: BoxBlock, // TODO: Box OAuth integration browser_use: BrowserUseBlock, calendly: CalendlyBlock, chat_trigger: ChatTriggerBlock, @@ -181,6 +183,7 @@ export const registry: Record = { intercom: IntercomBlock, jina: JinaBlock, jira: JiraBlock, + kalshi: KalshiBlock, knowledge: KnowledgeBlock, linear: LinearBlock, linkedin: LinkedInBlock, @@ -207,6 +210,7 @@ export const registry: Record = { perplexity: PerplexityBlock, pinecone: PineconeBlock, pipedrive: PipedriveBlock, + polymarket: PolymarketBlock, postgresql: PostgreSQLBlock, posthog: PostHogBlock, pylon: PylonBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0da6f731c16..dc9f6bab2ec 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4039,3 +4039,32 @@ export function DatadogIcon(props: SVGProps) { ) } + +export function KalshiIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + +export function PolymarketIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 211b771ec9d..476d4a5dac0 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -181,8 +181,6 @@ export const env = createEnv({ GITHUB_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret GITHUB_REPO_CLIENT_ID: z.string().optional(), // GitHub OAuth client ID for repo access GITHUB_REPO_CLIENT_SECRET: z.string().optional(), // GitHub OAuth client secret for repo access - GITLAB_CLIENT_ID: z.string().optional(), // GitLab OAuth client ID for GitLab integration - GITLAB_CLIENT_SECRET: z.string().optional(), // GitLab OAuth client secret X_CLIENT_ID: z.string().optional(), // X (Twitter) OAuth client ID X_CLIENT_SECRET: z.string().optional(), // X (Twitter) OAuth client secret CONFLUENCE_CLIENT_ID: z.string().optional(), // Atlassian Confluence OAuth client ID diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 40d82aac24e..128d4a4f230 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -2,12 +2,10 @@ import type { ReactNode } from 'react' import { AirtableIcon, AsanaIcon, - BoxCompanyIcon, ConfluenceIcon, // DiscordIcon, DropboxIcon, GithubIcon, - GitLabIcon, GmailIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -47,7 +45,6 @@ const logger = createLogger('OAuth') export type OAuthProvider = | 'google' | 'github' - | 'gitlab' | 'x' // | 'supabase' | 'confluence' @@ -69,7 +66,6 @@ export type OAuthProvider = | 'salesforce' | 'linkedin' | 'shopify' - | 'box' | 'zoom' | string @@ -83,7 +79,6 @@ export type OAuthService = | 'google-vault' | 'google-forms' | 'github' - | 'gitlab' | 'x' // | 'supabase' | 'confluence' @@ -110,7 +105,6 @@ export type OAuthService = | 'salesforce' | 'linkedin' | 'shopify' - | 'box' | 'zoom' export interface OAuthProviderConfig { id: OAuthProvider @@ -355,23 +349,6 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'github', }, - gitlab: { - id: 'gitlab', - name: 'GitLab', - icon: (props) => GitLabIcon(props), - services: { - gitlab: { - id: 'gitlab', - name: 'GitLab', - description: 'Manage GitLab projects, issues, merge requests, and pipelines.', - providerId: 'gitlab', - icon: (props) => GitLabIcon(props), - baseProviderIcon: (props) => GitLabIcon(props), - scopes: ['api', 'read_user', 'read_repository', 'write_repository'], - }, - }, - defaultService: 'gitlab', - }, x: { id: 'x', name: 'X', @@ -854,23 +831,6 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'salesforce', }, - box: { - id: 'box', - name: 'Box', - icon: (props) => BoxCompanyIcon(props), - services: { - box: { - id: 'box', - name: 'Box', - description: 'Manage files and folders in Box cloud storage.', - providerId: 'box', - icon: (props) => BoxCompanyIcon(props), - baseProviderIcon: (props) => BoxCompanyIcon(props), - scopes: ['root_readwrite'], - }, - }, - defaultService: 'box', - }, zoom: { id: 'zoom', name: 'Zoom', @@ -1133,19 +1093,6 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { additionalHeaders: { Accept: 'application/json' }, } } - case 'gitlab': { - const { clientId, clientSecret } = getCredentials( - env.GITLAB_CLIENT_ID, - env.GITLAB_CLIENT_SECRET - ) - return { - tokenEndpoint: 'https://gitlab.com/oauth/token', - clientId, - clientSecret, - useBasicAuth: false, - supportsRefreshTokenRotation: true, - } - } case 'x': { const { clientId, clientSecret } = getCredentials(env.X_CLIENT_ID, env.X_CLIENT_SECRET) return { @@ -1430,16 +1377,6 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } - case 'box': { - const { clientId, clientSecret } = getCredentials(env.BOX_CLIENT_ID, env.BOX_CLIENT_SECRET) - return { - tokenEndpoint: 'https://api.box.com/oauth2/token', - clientId, - clientSecret, - useBasicAuth: false, - supportsRefreshTokenRotation: true, - } - } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/tools/ahrefs/backlinks.ts b/apps/sim/tools/ahrefs/backlinks.ts index 81a9bd35702..e2521bd5c26 100644 --- a/apps/sim/tools/ahrefs/backlinks.ts +++ b/apps/sim/tools/ahrefs/backlinks.ts @@ -22,6 +22,12 @@ export const backlinksTool: ToolConfig { const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks') url.searchParams.set('target', params.target) + // Date is required - default to today if not provided + const date = params.date || new Date().toISOString().split('T')[0] + url.searchParams.set('date', date) if (params.mode) url.searchParams.set('mode', params.mode) if (params.limit) url.searchParams.set('limit', String(params.limit)) if (params.offset) url.searchParams.set('offset', String(params.offset)) @@ -62,15 +71,15 @@ export const backlinksTool: ToolConfig ({ urlFrom: link.url_from || '', urlTo: link.url_to || '', anchor: link.anchor || '', - domainRatingSource: link.domain_rating_source ?? 0, - isDofollow: link.is_dofollow ?? false, + domainRatingSource: link.domain_rating_source ?? link.domain_rating ?? 0, + isDofollow: link.is_dofollow ?? link.dofollow ?? false, firstSeen: link.first_seen || '', lastVisited: link.last_visited || '', })) diff --git a/apps/sim/tools/ahrefs/backlinks_stats.ts b/apps/sim/tools/ahrefs/backlinks_stats.ts index 0c363534024..73331d029ea 100644 --- a/apps/sim/tools/ahrefs/backlinks_stats.ts +++ b/apps/sim/tools/ahrefs/backlinks_stats.ts @@ -25,6 +25,12 @@ export const backlinksStatsTool: ToolConfig< description: 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', }, + date: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Date for historical data in YYYY-MM-DD format (defaults to today)', + }, apiKey: { type: 'string', required: true, @@ -37,6 +43,9 @@ export const backlinksStatsTool: ToolConfig< url: (params) => { const url = new URL('https://api.ahrefs.com/v3/site-explorer/backlinks-stats') url.searchParams.set('target', params.target) + // Date is required - default to today if not provided + const date = params.date || new Date().toISOString().split('T')[0] + url.searchParams.set('date', date) if (params.mode) url.searchParams.set('mode', params.mode) return url.toString() }, @@ -51,16 +60,16 @@ export const backlinksStatsTool: ToolConfig< const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to get backlinks stats') + throw new Error(data.error?.message || data.error || 'Failed to get backlinks stats') } return { success: true, output: { stats: { - total: data.live ?? 0, - dofollow: data.live_dofollow ?? 0, - nofollow: data.live_nofollow ?? 0, + total: data.live ?? data.total ?? 0, + dofollow: data.live_dofollow ?? data.dofollow ?? 0, + nofollow: data.live_nofollow ?? data.nofollow ?? 0, text: data.text ?? 0, image: data.image ?? 0, redirect: data.redirect ?? 0, diff --git a/apps/sim/tools/ahrefs/broken_backlinks.ts b/apps/sim/tools/ahrefs/broken_backlinks.ts index f85e4452e48..0bf5d9f1d96 100644 --- a/apps/sim/tools/ahrefs/broken_backlinks.ts +++ b/apps/sim/tools/ahrefs/broken_backlinks.ts @@ -28,6 +28,12 @@ export const brokenBacklinksTool: ToolConfig< description: 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', }, + date: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Date for historical data in YYYY-MM-DD format (defaults to today)', + }, limit: { type: 'number', required: false, @@ -52,6 +58,9 @@ export const brokenBacklinksTool: ToolConfig< url: (params) => { const url = new URL('https://api.ahrefs.com/v3/site-explorer/broken-backlinks') url.searchParams.set('target', params.target) + // Date is required - default to today if not provided + const date = params.date || new Date().toISOString().split('T')[0] + url.searchParams.set('date', date) if (params.mode) url.searchParams.set('mode', params.mode) if (params.limit) url.searchParams.set('limit', String(params.limit)) if (params.offset) url.searchParams.set('offset', String(params.offset)) @@ -68,15 +77,15 @@ export const brokenBacklinksTool: ToolConfig< const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to get broken backlinks') + throw new Error(data.error?.message || data.error || 'Failed to get broken backlinks') } - const brokenBacklinks = (data.backlinks || []).map((link: any) => ({ + const brokenBacklinks = (data.backlinks || data.broken_backlinks || []).map((link: any) => ({ urlFrom: link.url_from || '', urlTo: link.url_to || '', - httpCode: link.http_code ?? 404, + httpCode: link.http_code ?? link.status_code ?? 404, anchor: link.anchor || '', - domainRatingSource: link.domain_rating_source ?? 0, + domainRatingSource: link.domain_rating_source ?? link.domain_rating ?? 0, })) return { diff --git a/apps/sim/tools/ahrefs/domain_rating.ts b/apps/sim/tools/ahrefs/domain_rating.ts index 0576d4bca35..75066ae0c5d 100644 --- a/apps/sim/tools/ahrefs/domain_rating.ts +++ b/apps/sim/tools/ahrefs/domain_rating.ts @@ -15,6 +15,12 @@ export const domainRatingTool: ToolConfig - `https://api.ahrefs.com/v3/site-explorer/domain-rating?target=${encodeURIComponent(params.target)}`, + url: (params) => { + const url = new URL('https://api.ahrefs.com/v3/site-explorer/domain-rating') + url.searchParams.set('target', params.target) + // Date is required - default to today if not provided + const date = params.date || new Date().toISOString().split('T')[0] + url.searchParams.set('date', date) + return url.toString() + }, method: 'GET', headers: (params) => ({ Accept: 'application/json', @@ -37,7 +49,7 @@ export const domainRatingTool: ToolConfig ({ + const keywords = (data.keywords || data.organic_keywords || []).map((kw: any) => ({ keyword: kw.keyword || '', volume: kw.volume ?? 0, position: kw.position ?? 0, url: kw.url || '', traffic: kw.traffic ?? 0, - keywordDifficulty: kw.keyword_difficulty ?? 0, + keywordDifficulty: kw.keyword_difficulty ?? kw.difficulty ?? 0, })) return { diff --git a/apps/sim/tools/ahrefs/referring_domains.ts b/apps/sim/tools/ahrefs/referring_domains.ts index 90b7e6aba25..87dce46c4eb 100644 --- a/apps/sim/tools/ahrefs/referring_domains.ts +++ b/apps/sim/tools/ahrefs/referring_domains.ts @@ -28,6 +28,12 @@ export const referringDomainsTool: ToolConfig< description: 'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains), exact (exact URL match)', }, + date: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Date for historical data in YYYY-MM-DD format (defaults to today)', + }, limit: { type: 'number', required: false, @@ -52,6 +58,9 @@ export const referringDomainsTool: ToolConfig< url: (params) => { const url = new URL('https://api.ahrefs.com/v3/site-explorer/referring-domains') url.searchParams.set('target', params.target) + // Date is required - default to today if not provided + const date = params.date || new Date().toISOString().split('T')[0] + url.searchParams.set('date', date) if (params.mode) url.searchParams.set('mode', params.mode) if (params.limit) url.searchParams.set('limit', String(params.limit)) if (params.offset) url.searchParams.set('offset', String(params.offset)) @@ -68,17 +77,19 @@ export const referringDomainsTool: ToolConfig< const data = await response.json() if (!response.ok) { - throw new Error(data.error?.message || 'Failed to get referring domains') + throw new Error(data.error?.message || data.error || 'Failed to get referring domains') } - const referringDomains = (data.refdomains || []).map((domain: any) => ({ - domain: domain.domain || '', - domainRating: domain.domain_rating ?? 0, - backlinks: domain.backlinks ?? 0, - dofollowBacklinks: domain.dofollow_backlinks ?? 0, - firstSeen: domain.first_seen || '', - lastVisited: domain.last_visited || '', - })) + const referringDomains = (data.refdomains || data.referring_domains || []).map( + (domain: any) => ({ + domain: domain.domain || domain.refdomain || '', + domainRating: domain.domain_rating ?? 0, + backlinks: domain.backlinks ?? 0, + dofollowBacklinks: domain.dofollow_backlinks ?? domain.dofollow ?? 0, + firstSeen: domain.first_seen || '', + lastVisited: domain.last_visited || '', + }) + ) return { success: true, diff --git a/apps/sim/tools/ahrefs/top_pages.ts b/apps/sim/tools/ahrefs/top_pages.ts index a9ee97e2b79..fb35f4d9d51 100644 --- a/apps/sim/tools/ahrefs/top_pages.ts +++ b/apps/sim/tools/ahrefs/top_pages.ts @@ -28,6 +28,12 @@ export const topPagesTool: ToolConfig ({ + const pages = (data.pages || data.top_pages || []).map((page: any) => ({ url: page.url || '', traffic: page.traffic ?? 0, - keywords: page.keywords ?? 0, + keywords: page.keywords ?? page.keyword_count ?? 0, topKeyword: page.top_keyword || '', - value: page.value ?? 0, + value: page.value ?? page.traffic_value ?? 0, })) return { diff --git a/apps/sim/tools/ahrefs/types.ts b/apps/sim/tools/ahrefs/types.ts index 58cb3877b75..797aed60470 100644 --- a/apps/sim/tools/ahrefs/types.ts +++ b/apps/sim/tools/ahrefs/types.ts @@ -4,6 +4,7 @@ import type { ToolResponse } from '@/tools/types' // Common parameters for all Ahrefs tools export interface AhrefsBaseParams { apiKey: string + date?: string // Date in YYYY-MM-DD format, defaults to today } // Target mode for analysis diff --git a/apps/sim/tools/box/copy_file.ts b/apps/sim/tools/box/copy_file.ts deleted file mode 100644 index 7456949b7d4..00000000000 --- a/apps/sim/tools/box/copy_file.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { BoxCopyFileParams, BoxCopyFileResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxCopyFileTool: ToolConfig = { - id: 'box_copy_file', - name: 'Box Copy File', - description: 'Create a copy of a file in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - fileId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file to copy', - }, - parentFolderId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the destination folder', - }, - newName: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Optional new name for the copy', - }, - }, - - request: { - url: (params) => `https://api.box.com/2.0/files/${params.fileId}/copy`, - method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params) => { - const body: Record = { - parent: { - id: params.parentFolderId, - }, - } - if (params.newName) { - body.name = params.newName - } - return body - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to copy file', - output: {}, - } - } - - return { - success: true, - output: { - file: data, - }, - } - }, - - outputs: { - file: { - type: 'object', - description: 'The copied file', - properties: { - id: { type: 'string', description: 'File ID' }, - name: { type: 'string', description: 'File name' }, - parent: { type: 'object', description: 'Parent folder' }, - created_at: { type: 'string', description: 'Creation timestamp' }, - }, - }, - }, -} diff --git a/apps/sim/tools/box/create_collaboration.ts b/apps/sim/tools/box/create_collaboration.ts deleted file mode 100644 index 2fb9ec9c7ec..00000000000 --- a/apps/sim/tools/box/create_collaboration.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { - BoxCreateCollaborationParams, - BoxCreateCollaborationResponse, -} from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxCreateCollaborationTool: ToolConfig< - BoxCreateCollaborationParams, - BoxCreateCollaborationResponse -> = { - id: 'box_create_collaboration', - name: 'Box Create Collaboration', - description: 'Share a file or folder with a user or group in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - itemId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file or folder to share', - }, - itemType: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The type of item: "file" or "folder"', - }, - accessibleByLogin: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Email address of the user to share with', - }, - accessibleById: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'ID of the user or group to share with', - }, - accessibleByType: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Type of collaborator: "user" or "group"', - }, - role: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Access role: "editor", "viewer", "previewer", "uploader", or "co-owner"', - }, - canViewPath: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether the collaborator can see the full path to the folder (folders only)', - }, - }, - - request: { - url: 'https://api.box.com/2.0/collaborations', - method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params) => { - const accessibleBy: Record = { - type: params.accessibleByType, - } - - if (params.accessibleByLogin) { - accessibleBy.login = params.accessibleByLogin - } else if (params.accessibleById) { - accessibleBy.id = params.accessibleById - } - - const body: Record = { - item: { - type: params.itemType, - id: params.itemId, - }, - accessible_by: accessibleBy, - role: params.role, - } - - if (params.canViewPath !== undefined && params.itemType === 'folder') { - body.can_view_path = params.canViewPath - } - - return body - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to create collaboration', - output: {}, - } - } - - return { - success: true, - output: { - collaboration: data, - }, - } - }, - - outputs: { - collaboration: { - type: 'object', - description: 'The created collaboration', - properties: { - id: { type: 'string', description: 'Collaboration ID' }, - role: { type: 'string', description: 'Access role' }, - status: { type: 'string', description: 'Collaboration status' }, - accessible_by: { type: 'object', description: 'User or group with access' }, - created_at: { type: 'string', description: 'Creation timestamp' }, - }, - }, - }, -} diff --git a/apps/sim/tools/box/create_folder.ts b/apps/sim/tools/box/create_folder.ts deleted file mode 100644 index 085c689c8a9..00000000000 --- a/apps/sim/tools/box/create_folder.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { BoxCreateFolderParams, BoxCreateFolderResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxCreateFolderTool: ToolConfig = { - id: 'box_create_folder', - name: 'Box Create Folder', - description: 'Create a new folder in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - parentFolderId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the parent folder (use "0" for root folder)', - }, - folderName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the new folder', - }, - }, - - request: { - url: 'https://api.box.com/2.0/folders', - method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params) => ({ - name: params.folderName, - parent: { - id: params.parentFolderId, - }, - }), - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to create folder', - output: {}, - } - } - - return { - success: true, - output: { - folder: data, - }, - } - }, - - outputs: { - folder: { - type: 'object', - description: 'The created folder', - properties: { - id: { type: 'string', description: 'Folder ID' }, - name: { type: 'string', description: 'Folder name' }, - created_at: { type: 'string', description: 'Creation timestamp' }, - parent: { type: 'object', description: 'Parent folder' }, - }, - }, - }, -} diff --git a/apps/sim/tools/box/create_shared_link.ts b/apps/sim/tools/box/create_shared_link.ts deleted file mode 100644 index c77d219bf56..00000000000 --- a/apps/sim/tools/box/create_shared_link.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { BoxCreateSharedLinkParams, BoxCreateSharedLinkResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxCreateSharedLinkTool: ToolConfig< - BoxCreateSharedLinkParams, - BoxCreateSharedLinkResponse -> = { - id: 'box_create_shared_link', - name: 'Box Create Shared Link', - description: 'Create a shared link for a file or folder in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - itemId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file or folder', - }, - itemType: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The type of item: "file" or "folder"', - }, - access: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Access level: "open" (anyone), "company", or "collaborators"', - }, - password: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Password to protect the shared link', - }, - unsharedAt: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Expiration date in ISO 8601 format', - }, - canDownload: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether the link allows downloads', - }, - canPreview: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether the link allows previews', - }, - }, - - request: { - url: (params) => `https://api.box.com/2.0/${params.itemType}s/${params.itemId}`, - method: 'PUT', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params) => { - const sharedLink: Record = {} - - if (params.access) { - sharedLink.access = params.access - } - if (params.password) { - sharedLink.password = params.password - } - if (params.unsharedAt) { - sharedLink.unshared_at = params.unsharedAt - } - - // Always include permissions object with defaults (can_download: true, can_preview: true) - // Box API requires explicit permissions for proper shared link creation - sharedLink.permissions = { - can_download: params.canDownload !== undefined ? params.canDownload : true, - can_preview: params.canPreview !== undefined ? params.canPreview : true, - } - - return { - shared_link: sharedLink, - } - }, - }, - - transformResponse: async (response, params) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to create shared link', - output: {}, - } - } - - return { - success: true, - output: { - sharedLink: data.shared_link, - itemId: data.id, - itemType: params?.itemType, - }, - } - }, - - outputs: { - sharedLink: { - type: 'object', - description: 'The shared link details', - properties: { - url: { type: 'string', description: 'The shared link URL' }, - download_url: { type: 'string', description: 'Direct download URL' }, - access: { type: 'string', description: 'Access level' }, - is_password_enabled: { type: 'boolean', description: 'Whether password is enabled' }, - }, - }, - itemId: { - type: 'string', - description: 'The ID of the item', - }, - itemType: { - type: 'string', - description: 'The type of item', - }, - }, -} diff --git a/apps/sim/tools/box/delete_file.ts b/apps/sim/tools/box/delete_file.ts deleted file mode 100644 index 359d618d98f..00000000000 --- a/apps/sim/tools/box/delete_file.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { BoxDeleteFileParams, BoxDeleteFileResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxDeleteFileTool: ToolConfig = { - id: 'box_delete_file', - name: 'Box Delete File', - description: - 'Delete a file from Box. Standard delete moves to trash. Use permanent flag for files already in trash.', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - fileId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file to delete', - }, - permanent: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'If true, permanently delete a file that is already in trash', - }, - }, - - request: { - url: (params) => { - // Standard delete always moves file to trash - // To permanently delete, the file must already be in trash (use /files/{id}/trash endpoint) - // The permanent flag here deletes from trash if the file is already there - if (params.permanent) { - return `https://api.box.com/2.0/files/${params.fileId}/trash` - } - return `https://api.box.com/2.0/files/${params.fileId}` - }, - method: 'DELETE', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response, params) => { - if (response.status === 204) { - return { - success: true, - output: { - success: true, - fileId: params?.fileId, - }, - } - } - - const data = await response.json().catch(() => ({})) - return { - success: false, - error: data.message || data.error_description || 'Failed to delete file', - output: {}, - } - }, - - outputs: { - success: { - type: 'boolean', - description: 'Whether the file was successfully deleted', - }, - fileId: { - type: 'string', - description: 'The ID of the deleted file', - }, - }, -} diff --git a/apps/sim/tools/box/delete_folder.ts b/apps/sim/tools/box/delete_folder.ts deleted file mode 100644 index e9a4de9f1f3..00000000000 --- a/apps/sim/tools/box/delete_folder.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { BoxDeleteFolderParams, BoxDeleteFolderResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxDeleteFolderTool: ToolConfig = { - id: 'box_delete_folder', - name: 'Box Delete Folder', - description: 'Delete a folder from Box (moves to trash by default)', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - folderId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the folder to delete', - }, - recursive: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Delete folder contents recursively (default: true)', - }, - }, - - request: { - url: (params) => { - const recursive = params.recursive !== false ? 'true' : 'false' - return `https://api.box.com/2.0/folders/${params.folderId}?recursive=${recursive}` - }, - method: 'DELETE', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response, params) => { - if (response.status === 204) { - return { - success: true, - output: { - success: true, - folderId: params?.folderId, - }, - } - } - - const data = await response.json().catch(() => ({})) - return { - success: false, - error: data.message || data.error_description || 'Failed to delete folder', - output: {}, - } - }, - - outputs: { - success: { - type: 'boolean', - description: 'Whether the folder was successfully deleted', - }, - folderId: { - type: 'string', - description: 'The ID of the deleted folder', - }, - }, -} diff --git a/apps/sim/tools/box/download_file.ts b/apps/sim/tools/box/download_file.ts deleted file mode 100644 index ee28352c829..00000000000 --- a/apps/sim/tools/box/download_file.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { BoxDownloadFileParams, BoxDownloadFileResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxDownloadFileTool: ToolConfig = { - id: 'box_download_file', - name: 'Box Download File', - description: 'Get the download URL for a file in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - fileId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file to download', - }, - }, - - request: { - url: (params) => `https://api.box.com/2.0/files/${params.fileId}/content`, - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, - }, - - transformResponse: async (response, params) => { - // Box returns a 302 redirect with the download URL - // If we get a redirect, extract the location - if (response.status === 302 || response.status === 301) { - const downloadUrl = response.headers.get('location') - if (downloadUrl) { - return { - success: true, - output: { - downloadUrl, - fileId: params?.fileId, - }, - } - } - } - - // If we get the content directly (some API implementations) - if (response.ok) { - const contentType = response.headers.get('content-type') || 'application/octet-stream' - const contentDisposition = response.headers.get('content-disposition') - let fileName = 'download' - - if (contentDisposition) { - const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) - if (match) { - fileName = match[1].replace(/['"]/g, '') - } - } - - // For text-based content, we can return the content - if (contentType.includes('text') || contentType.includes('json')) { - const content = await response.text() - return { - success: true, - output: { - content, - fileName, - mimeType: contentType, - }, - } - } - - // For binary content, return info about the file - return { - success: true, - output: { - fileName, - mimeType: contentType, - message: 'Binary file content available. Use the download URL to retrieve the file.', - }, - } - } - - const data = await response.json().catch(() => ({})) - return { - success: false, - error: data.message || data.error_description || 'Failed to download file', - output: {}, - } - }, - - outputs: { - content: { - type: 'string', - description: 'File content (for text files)', - optional: true, - }, - downloadUrl: { - type: 'string', - description: 'URL to download the file', - optional: true, - }, - fileName: { - type: 'string', - description: 'Name of the file', - optional: true, - }, - mimeType: { - type: 'string', - description: 'MIME type of the file', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/box/get_current_user.ts b/apps/sim/tools/box/get_current_user.ts deleted file mode 100644 index ff3c9e88a69..00000000000 --- a/apps/sim/tools/box/get_current_user.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { BoxGetCurrentUserParams, BoxGetCurrentUserResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxGetCurrentUserTool: ToolConfig = - { - id: 'box_get_current_user', - name: 'Box Get Current User', - description: 'Get information about the authenticated Box user', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: {}, - - request: { - url: 'https://api.box.com/2.0/users/me', - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to get current user', - output: {}, - } - } - - return { - success: true, - output: { - user: { - id: data.id, - type: data.type, - name: data.name, - login: data.login, - avatar_url: data.avatar_url, - space_used: data.space_used, - space_amount: data.space_amount, - max_upload_size: data.max_upload_size, - }, - }, - } - }, - - outputs: { - user: { - type: 'object', - description: 'The authenticated user', - properties: { - id: { type: 'string', description: 'User ID' }, - name: { type: 'string', description: 'User name' }, - login: { type: 'string', description: 'User email/login' }, - avatar_url: { type: 'string', description: 'Avatar URL' }, - space_used: { type: 'number', description: 'Space used in bytes' }, - space_amount: { type: 'number', description: 'Total space available in bytes' }, - max_upload_size: { type: 'number', description: 'Maximum upload size in bytes' }, - }, - }, - }, - } diff --git a/apps/sim/tools/box/get_file.ts b/apps/sim/tools/box/get_file.ts deleted file mode 100644 index eed8029451f..00000000000 --- a/apps/sim/tools/box/get_file.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { BoxGetFileParams, BoxGetFileResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxGetFileTool: ToolConfig = { - id: 'box_get_file', - name: 'Box Get File', - description: 'Get information about a file in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - fileId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file to retrieve', - }, - fields: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'Comma-separated list of fields to include (e.g., "id,name,size,modified_at,shared_link")', - }, - }, - - request: { - url: (params) => { - const fields = - params.fields || - 'id,type,name,size,created_at,modified_at,created_by,modified_by,owned_by,parent,shared_link,description,path_collection' - return `https://api.box.com/2.0/files/${params.fileId}?fields=${encodeURIComponent(fields)}` - }, - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to get file', - output: {}, - } - } - - return { - success: true, - output: { - file: data, - }, - } - }, - - outputs: { - file: { - type: 'object', - description: 'The file information', - properties: { - id: { type: 'string', description: 'File ID' }, - name: { type: 'string', description: 'File name' }, - size: { type: 'number', description: 'File size in bytes' }, - created_at: { type: 'string', description: 'Creation timestamp' }, - modified_at: { type: 'string', description: 'Last modification timestamp' }, - parent: { type: 'object', description: 'Parent folder' }, - shared_link: { type: 'object', description: 'Shared link information if exists' }, - }, - }, - }, -} diff --git a/apps/sim/tools/box/index.ts b/apps/sim/tools/box/index.ts deleted file mode 100644 index 8da9f4e418e..00000000000 --- a/apps/sim/tools/box/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Box Tools - -export { boxCopyFileTool } from './copy_file' -export { boxCreateCollaborationTool } from './create_collaboration' -export { boxCreateFolderTool } from './create_folder' -export { boxCreateSharedLinkTool } from './create_shared_link' -export { boxDeleteFileTool } from './delete_file' -export { boxDeleteFolderTool } from './delete_folder' -export { boxDownloadFileTool } from './download_file' -export { boxGetCurrentUserTool } from './get_current_user' -export { boxGetFileTool } from './get_file' -export { boxListFolderTool } from './list_folder' -export { boxSearchTool } from './search' -// Re-export types -export * from './types' -export { boxUpdateFileTool } from './update_file' diff --git a/apps/sim/tools/box/list_folder.ts b/apps/sim/tools/box/list_folder.ts deleted file mode 100644 index d954e2ce053..00000000000 --- a/apps/sim/tools/box/list_folder.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { BoxListFolderItemsParams, BoxListFolderItemsResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxListFolderTool: ToolConfig = { - id: 'box_list_folder', - name: 'Box List Folder', - description: 'List items in a Box folder', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - folderId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the folder to list (use "0" for root folder)', - }, - limit: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Maximum number of items to return (default: 100, max: 1000)', - }, - offset: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Offset for pagination (default: 0)', - }, - fields: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of fields to include', - }, - }, - - request: { - url: (params) => { - const limit = params.limit || 100 - const offset = params.offset || 0 - const fields = - params.fields || 'id,type,name,size,modified_at,created_at,parent,shared_link,description' - return `https://api.box.com/2.0/folders/${params.folderId}/items?limit=${limit}&offset=${offset}&fields=${encodeURIComponent(fields)}` - }, - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to list folder', - output: {}, - } - } - - return { - success: true, - output: { - items: data.entries || [], - totalCount: data.total_count, - offset: data.offset, - limit: data.limit, - }, - } - }, - - outputs: { - items: { - type: 'array', - description: 'Array of files and folders in the folder', - items: { - type: 'object', - properties: { - id: { type: 'string', description: 'Item ID' }, - type: { type: 'string', description: 'Item type (file or folder)' }, - name: { type: 'string', description: 'Item name' }, - size: { type: 'number', description: 'Size in bytes (for files)' }, - modified_at: { type: 'string', description: 'Last modification timestamp' }, - }, - }, - }, - totalCount: { - type: 'number', - description: 'Total number of items in the folder', - }, - offset: { - type: 'number', - description: 'Current offset for pagination', - }, - limit: { - type: 'number', - description: 'Maximum items returned', - }, - }, -} diff --git a/apps/sim/tools/box/search.ts b/apps/sim/tools/box/search.ts deleted file mode 100644 index 7ab1a630fda..00000000000 --- a/apps/sim/tools/box/search.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { BoxSearchParams, BoxSearchResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxSearchTool: ToolConfig = { - id: 'box_search', - name: 'Box Search', - description: 'Search for files and folders in Box', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - query: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The search query string', - }, - scope: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Search scope: "user_content" (default) or "enterprise_content"', - }, - fileExtensions: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated file extensions to filter by (e.g., "pdf,docx")', - }, - ancestorFolderIds: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated folder IDs to search within', - }, - contentTypes: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'Comma-separated content types: "name", "description", "file_content", "comments", "tag"', - }, - limit: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Maximum number of results (default: 30, max: 200)', - }, - offset: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Offset for pagination (default: 0)', - }, - }, - - request: { - url: (params) => { - const queryParams = new URLSearchParams() - queryParams.append('query', params.query) - - if (params.scope) { - queryParams.append('scope', params.scope) - } - if (params.fileExtensions) { - queryParams.append('file_extensions', params.fileExtensions) - } - if (params.ancestorFolderIds) { - queryParams.append('ancestor_folder_ids', params.ancestorFolderIds) - } - if (params.contentTypes) { - queryParams.append('content_types', params.contentTypes) - } - queryParams.append('limit', String(params.limit || 30)) - queryParams.append('offset', String(params.offset || 0)) - - return `https://api.box.com/2.0/search?${queryParams.toString()}` - }, - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to search', - output: {}, - } - } - - return { - success: true, - output: { - results: data.entries || [], - totalCount: data.total_count, - offset: data.offset, - limit: data.limit, - }, - } - }, - - outputs: { - results: { - type: 'array', - description: 'Array of search results', - items: { - type: 'object', - properties: { - id: { type: 'string', description: 'Item ID' }, - type: { type: 'string', description: 'Item type' }, - name: { type: 'string', description: 'Item name' }, - parent: { type: 'object', description: 'Parent folder' }, - modified_at: { type: 'string', description: 'Last modification timestamp' }, - }, - }, - }, - totalCount: { - type: 'number', - description: 'Total number of search results', - }, - offset: { - type: 'number', - description: 'Current offset', - }, - limit: { - type: 'number', - description: 'Maximum results returned', - }, - }, -} diff --git a/apps/sim/tools/box/types.ts b/apps/sim/tools/box/types.ts deleted file mode 100644 index dc3672f4e30..00000000000 --- a/apps/sim/tools/box/types.ts +++ /dev/null @@ -1,619 +0,0 @@ -import type { ToolResponse } from '@/tools/types' - -// ===== Core Types ===== - -export interface BoxFile { - id: string - type: 'file' - name: string - size?: number - created_at?: string - modified_at?: string - content_created_at?: string - content_modified_at?: string - created_by?: BoxUser - modified_by?: BoxUser - owned_by?: BoxUser - shared_link?: BoxSharedLink | null - parent?: BoxFolder - path_collection?: { - total_count: number - entries: BoxFolder[] - } - description?: string - sha1?: string - file_version?: { - id: string - type: 'file_version' - sha1: string - } - etag?: string - sequence_id?: string -} - -export interface BoxFolder { - id: string - type: 'folder' - name: string - size?: number - created_at?: string - modified_at?: string - created_by?: BoxUser - modified_by?: BoxUser - owned_by?: BoxUser - shared_link?: BoxSharedLink | null - parent?: BoxFolder | null - path_collection?: { - total_count: number - entries: BoxFolder[] - } - description?: string - item_collection?: { - total_count: number - entries: (BoxFile | BoxFolder)[] - offset: number - limit: number - } - etag?: string - sequence_id?: string -} - -export interface BoxUser { - id: string - type: 'user' - name: string - login?: string - avatar_url?: string -} - -export interface BoxSharedLink { - url: string - download_url?: string - vanity_url?: string - vanity_name?: string - access: 'open' | 'company' | 'collaborators' - effective_access?: 'open' | 'company' | 'collaborators' - effective_permission?: 'can_download' | 'can_preview' - unshared_at?: string - is_password_enabled?: boolean - permissions?: { - can_download: boolean - can_preview: boolean - } - download_count?: number - preview_count?: number -} - -export interface BoxCollaboration { - id: string - type: 'collaboration' - role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' | 'owner' - status: 'accepted' | 'pending' | 'rejected' - created_at?: string - modified_at?: string - accessible_by?: BoxUser | BoxGroup - created_by?: BoxUser - invite_email?: string - item?: BoxFile | BoxFolder -} - -export interface BoxGroup { - id: string - type: 'group' - name: string - description?: string -} - -export interface BoxComment { - id: string - type: 'comment' - message: string - created_at: string - modified_at?: string - created_by: BoxUser - item?: BoxFile -} - -export interface BoxTask { - id: string - type: 'task' - message?: string - due_at?: string - created_at: string - completion_rule?: 'all_assignees' | 'any_assignee' - action?: 'review' | 'complete' - item?: BoxFile - task_assignment_collection?: { - total_count: number - entries: BoxTaskAssignment[] - } -} - -export interface BoxTaskAssignment { - id: string - type: 'task_assignment' - status?: 'incomplete' | 'approved' | 'rejected' | 'completed' - message?: string - assigned_to?: BoxUser - assigned_at?: string - completed_at?: string -} - -export interface BoxFileVersion { - id: string - type: 'file_version' - sha1: string - name: string - size: number - created_at: string - modified_at: string - modified_by: BoxUser - trashed_at?: string - trashed_by?: BoxUser - purged_at?: string -} - -export interface BoxSearchResult { - id: string - type: 'file' | 'folder' | 'web_link' - name: string - parent?: BoxFolder - modified_at?: string - size?: number -} - -// ===== Request Params ===== - -export interface BoxBaseParams { - accessToken?: string -} - -// File Operations -export interface BoxGetFileParams extends BoxBaseParams { - fileId: string - fields?: string -} - -export interface BoxUploadFileParams extends BoxBaseParams { - parentFolderId: string - fileName: string - fileContent: string -} - -export interface BoxDownloadFileParams extends BoxBaseParams { - fileId: string -} - -export interface BoxUpdateFileParams extends BoxBaseParams { - fileId: string - name?: string - description?: string - parentId?: string - tags?: string[] -} - -export interface BoxCopyFileParams extends BoxBaseParams { - fileId: string - parentFolderId: string - newName?: string -} - -export interface BoxDeleteFileParams extends BoxBaseParams { - fileId: string - permanent?: boolean -} - -export interface BoxRestoreFileParams extends BoxBaseParams { - fileId: string - newName?: string - parentFolderId?: string -} - -// Folder Operations -export interface BoxGetFolderParams extends BoxBaseParams { - folderId: string - fields?: string -} - -export interface BoxListFolderItemsParams extends BoxBaseParams { - folderId: string - limit?: number - offset?: number - fields?: string -} - -export interface BoxCreateFolderParams extends BoxBaseParams { - parentFolderId: string - folderName: string -} - -export interface BoxUpdateFolderParams extends BoxBaseParams { - folderId: string - name?: string - description?: string - parentId?: string -} - -export interface BoxCopyFolderParams extends BoxBaseParams { - folderId: string - parentFolderId: string - newName?: string -} - -export interface BoxDeleteFolderParams extends BoxBaseParams { - folderId: string - recursive?: boolean -} - -// Search Operations -export interface BoxSearchParams extends BoxBaseParams { - query: string - scope?: 'user_content' | 'enterprise_content' - fileExtensions?: string - ancestorFolderIds?: string - limit?: number - offset?: number - contentTypes?: string -} - -// Collaboration Operations -export interface BoxCreateCollaborationParams extends BoxBaseParams { - itemId: string - itemType: 'file' | 'folder' - accessibleByLogin?: string - accessibleById?: string - accessibleByType: 'user' | 'group' - role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' - canViewPath?: boolean -} - -export interface BoxListCollaborationsParams extends BoxBaseParams { - itemId: string - itemType: 'file' | 'folder' - limit?: number - offset?: number -} - -export interface BoxUpdateCollaborationParams extends BoxBaseParams { - collaborationId: string - role: 'editor' | 'viewer' | 'previewer' | 'uploader' | 'co-owner' - status?: 'accepted' | 'pending' | 'rejected' -} - -export interface BoxDeleteCollaborationParams extends BoxBaseParams { - collaborationId: string -} - -// Shared Link Operations -export interface BoxCreateSharedLinkParams extends BoxBaseParams { - itemId: string - itemType: 'file' | 'folder' - access?: 'open' | 'company' | 'collaborators' - password?: string - unsharedAt?: string - canDownload?: boolean - canPreview?: boolean -} - -export interface BoxGetSharedLinkParams extends BoxBaseParams { - itemId: string - itemType: 'file' | 'folder' -} - -export interface BoxDeleteSharedLinkParams extends BoxBaseParams { - itemId: string - itemType: 'file' | 'folder' -} - -// Comment Operations -export interface BoxCreateCommentParams extends BoxBaseParams { - fileId: string - message: string -} - -export interface BoxListCommentsParams extends BoxBaseParams { - fileId: string - limit?: number - offset?: number -} - -export interface BoxUpdateCommentParams extends BoxBaseParams { - commentId: string - message: string -} - -export interface BoxDeleteCommentParams extends BoxBaseParams { - commentId: string -} - -// Task Operations -export interface BoxCreateTaskParams extends BoxBaseParams { - fileId: string - message?: string - dueAt?: string - action?: 'review' | 'complete' -} - -export interface BoxGetTaskParams extends BoxBaseParams { - taskId: string -} - -export interface BoxUpdateTaskParams extends BoxBaseParams { - taskId: string - message?: string - dueAt?: string -} - -export interface BoxDeleteTaskParams extends BoxBaseParams { - taskId: string -} - -// File Version Operations -export interface BoxListFileVersionsParams extends BoxBaseParams { - fileId: string - limit?: number - offset?: number -} - -export interface BoxPromoteFileVersionParams extends BoxBaseParams { - fileId: string - versionId: string -} - -// User Operations -export interface BoxGetCurrentUserParams extends BoxBaseParams {} - -// ===== Response Types ===== - -export interface BoxGetFileResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxUploadFileResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxDownloadFileResponse extends ToolResponse { - output: { - content?: string - fileName?: string - mimeType?: string - } -} - -export interface BoxUpdateFileResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxCopyFileResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxDeleteFileResponse extends ToolResponse { - output: { - success?: boolean - fileId?: string - } -} - -export interface BoxRestoreFileResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxGetFolderResponse extends ToolResponse { - output: { - folder?: BoxFolder - } -} - -export interface BoxListFolderItemsResponse extends ToolResponse { - output: { - items?: (BoxFile | BoxFolder)[] - totalCount?: number - offset?: number - limit?: number - } -} - -export interface BoxCreateFolderResponse extends ToolResponse { - output: { - folder?: BoxFolder - } -} - -export interface BoxUpdateFolderResponse extends ToolResponse { - output: { - folder?: BoxFolder - } -} - -export interface BoxCopyFolderResponse extends ToolResponse { - output: { - folder?: BoxFolder - } -} - -export interface BoxDeleteFolderResponse extends ToolResponse { - output: { - success?: boolean - folderId?: string - } -} - -export interface BoxSearchResponse extends ToolResponse { - output: { - results?: BoxSearchResult[] - totalCount?: number - offset?: number - limit?: number - } -} - -export interface BoxCreateCollaborationResponse extends ToolResponse { - output: { - collaboration?: BoxCollaboration - } -} - -export interface BoxListCollaborationsResponse extends ToolResponse { - output: { - collaborations?: BoxCollaboration[] - totalCount?: number - } -} - -export interface BoxUpdateCollaborationResponse extends ToolResponse { - output: { - collaboration?: BoxCollaboration - } -} - -export interface BoxDeleteCollaborationResponse extends ToolResponse { - output: { - success?: boolean - collaborationId?: string - } -} - -export interface BoxCreateSharedLinkResponse extends ToolResponse { - output: { - sharedLink?: BoxSharedLink - itemId?: string - itemType?: string - } -} - -export interface BoxGetSharedLinkResponse extends ToolResponse { - output: { - sharedLink?: BoxSharedLink | null - itemId?: string - itemType?: string - } -} - -export interface BoxDeleteSharedLinkResponse extends ToolResponse { - output: { - success?: boolean - itemId?: string - itemType?: string - } -} - -export interface BoxCreateCommentResponse extends ToolResponse { - output: { - comment?: BoxComment - } -} - -export interface BoxListCommentsResponse extends ToolResponse { - output: { - comments?: BoxComment[] - totalCount?: number - } -} - -export interface BoxUpdateCommentResponse extends ToolResponse { - output: { - comment?: BoxComment - } -} - -export interface BoxDeleteCommentResponse extends ToolResponse { - output: { - success?: boolean - commentId?: string - } -} - -export interface BoxCreateTaskResponse extends ToolResponse { - output: { - task?: BoxTask - } -} - -export interface BoxGetTaskResponse extends ToolResponse { - output: { - task?: BoxTask - } -} - -export interface BoxUpdateTaskResponse extends ToolResponse { - output: { - task?: BoxTask - } -} - -export interface BoxDeleteTaskResponse extends ToolResponse { - output: { - success?: boolean - taskId?: string - } -} - -export interface BoxListFileVersionsResponse extends ToolResponse { - output: { - versions?: BoxFileVersion[] - totalCount?: number - } -} - -export interface BoxPromoteFileVersionResponse extends ToolResponse { - output: { - file?: BoxFile - } -} - -export interface BoxGetCurrentUserResponse extends ToolResponse { - output: { - user?: BoxUser & { - space_used?: number - space_amount?: number - max_upload_size?: number - } - } -} - -// Union type for all Box responses -export type BoxResponse = - | BoxGetFileResponse - | BoxUploadFileResponse - | BoxDownloadFileResponse - | BoxUpdateFileResponse - | BoxCopyFileResponse - | BoxDeleteFileResponse - | BoxRestoreFileResponse - | BoxGetFolderResponse - | BoxListFolderItemsResponse - | BoxCreateFolderResponse - | BoxUpdateFolderResponse - | BoxCopyFolderResponse - | BoxDeleteFolderResponse - | BoxSearchResponse - | BoxCreateCollaborationResponse - | BoxListCollaborationsResponse - | BoxUpdateCollaborationResponse - | BoxDeleteCollaborationResponse - | BoxCreateSharedLinkResponse - | BoxGetSharedLinkResponse - | BoxDeleteSharedLinkResponse - | BoxCreateCommentResponse - | BoxListCommentsResponse - | BoxUpdateCommentResponse - | BoxDeleteCommentResponse - | BoxCreateTaskResponse - | BoxGetTaskResponse - | BoxUpdateTaskResponse - | BoxDeleteTaskResponse - | BoxListFileVersionsResponse - | BoxPromoteFileVersionResponse - | BoxGetCurrentUserResponse diff --git a/apps/sim/tools/box/update_file.ts b/apps/sim/tools/box/update_file.ts deleted file mode 100644 index 621c752efc9..00000000000 --- a/apps/sim/tools/box/update_file.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { BoxUpdateFileParams, BoxUpdateFileResponse } from '@/tools/box/types' -import type { ToolConfig } from '@/tools/types' - -export const boxUpdateFileTool: ToolConfig = { - id: 'box_update_file', - name: 'Box Update File', - description: 'Update file information in Box (rename, move, or update description)', - version: '1.0.0', - - oauth: { - required: true, - provider: 'box', - }, - - params: { - fileId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the file to update', - }, - name: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New name for the file', - }, - description: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New description for the file', - }, - parentId: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'ID of the new parent folder (to move the file)', - }, - tags: { - type: 'array', - required: false, - visibility: 'user-or-llm', - description: 'Array of tags to set on the file', - items: { type: 'string' }, - }, - }, - - request: { - url: (params) => `https://api.box.com/2.0/files/${params.fileId}`, - method: 'PUT', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for Box API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params) => { - const body: Record = {} - if (params.name) { - body.name = params.name - } - if (params.description !== undefined) { - body.description = params.description - } - if (params.parentId) { - body.parent = { id: params.parentId } - } - if (params.tags) { - body.tags = params.tags - } - return body - }, - }, - - transformResponse: async (response) => { - const data = await response.json() - - if (!response.ok) { - return { - success: false, - error: data.message || data.error_description || 'Failed to update file', - output: {}, - } - } - - return { - success: true, - output: { - file: data, - }, - } - }, - - outputs: { - file: { - type: 'object', - description: 'The updated file', - properties: { - id: { type: 'string', description: 'File ID' }, - name: { type: 'string', description: 'File name' }, - description: { type: 'string', description: 'File description' }, - parent: { type: 'object', description: 'Parent folder' }, - modified_at: { type: 'string', description: 'Last modification timestamp' }, - }, - }, - }, -} diff --git a/apps/sim/tools/datadog/create_event.ts b/apps/sim/tools/datadog/create_event.ts index 2d71cedbfae..35737f1c152 100644 --- a/apps/sim/tools/datadog/create_event.ts +++ b/apps/sim/tools/datadog/create_event.ts @@ -70,12 +70,6 @@ export const createEventTool: ToolConfig visibility: 'user-only', description: 'Datadog API key', }, - applicationKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Datadog Application key', - }, site: { type: 'string', required: false, diff --git a/apps/sim/tools/datadog/send_logs.ts b/apps/sim/tools/datadog/send_logs.ts index dd480d928ff..1dd5055675e 100644 --- a/apps/sim/tools/datadog/send_logs.ts +++ b/apps/sim/tools/datadog/send_logs.ts @@ -21,12 +21,6 @@ export const sendLogsTool: ToolConfig = { visibility: 'user-only', description: 'Datadog API key', }, - applicationKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Datadog Application key', - }, site: { type: 'string', required: false, diff --git a/apps/sim/tools/datadog/submit_metrics.ts b/apps/sim/tools/datadog/submit_metrics.ts index f2c3f492257..7b4df3cf99b 100644 --- a/apps/sim/tools/datadog/submit_metrics.ts +++ b/apps/sim/tools/datadog/submit_metrics.ts @@ -22,12 +22,6 @@ export const submitMetricsTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/create_issue.ts b/apps/sim/tools/gitlab/create_issue.ts index 2078d67ada9..417f614bfd0 100644 --- a/apps/sim/tools/gitlab/create_issue.ts +++ b/apps/sim/tools/gitlab/create_issue.ts @@ -8,12 +8,12 @@ export const gitlabCreateIssueTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = { title: params.title, diff --git a/apps/sim/tools/gitlab/create_issue_note.ts b/apps/sim/tools/gitlab/create_issue_note.ts index 2e175a1118c..10a1620e48a 100644 --- a/apps/sim/tools/gitlab/create_issue_note.ts +++ b/apps/sim/tools/gitlab/create_issue_note.ts @@ -10,12 +10,12 @@ export const gitlabCreateIssueNoteTool: ToolConfig< description: 'Add a comment to a GitLab issue', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -39,15 +39,10 @@ export const gitlabCreateIssueNoteTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}/notes` }, method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => ({ body: params.body, }), diff --git a/apps/sim/tools/gitlab/create_merge_request.ts b/apps/sim/tools/gitlab/create_merge_request.ts index e58b09fcddf..45e45d30e8e 100644 --- a/apps/sim/tools/gitlab/create_merge_request.ts +++ b/apps/sim/tools/gitlab/create_merge_request.ts @@ -13,12 +13,12 @@ export const gitlabCreateMergeRequestTool: ToolConfig< description: 'Create a new merge request in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -82,15 +82,10 @@ export const gitlabCreateMergeRequestTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests` }, method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = { source_branch: params.sourceBranch, diff --git a/apps/sim/tools/gitlab/create_merge_request_note.ts b/apps/sim/tools/gitlab/create_merge_request_note.ts index b5778d20eeb..6938807737a 100644 --- a/apps/sim/tools/gitlab/create_merge_request_note.ts +++ b/apps/sim/tools/gitlab/create_merge_request_note.ts @@ -13,12 +13,12 @@ export const gitlabCreateMergeRequestNoteTool: ToolConfig< description: 'Add a comment to a GitLab merge request', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -42,15 +42,10 @@ export const gitlabCreateMergeRequestNoteTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/notes` }, method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => ({ body: params.body, }), diff --git a/apps/sim/tools/gitlab/create_pipeline.ts b/apps/sim/tools/gitlab/create_pipeline.ts index 2214331e103..3213e976fff 100644 --- a/apps/sim/tools/gitlab/create_pipeline.ts +++ b/apps/sim/tools/gitlab/create_pipeline.ts @@ -10,12 +10,12 @@ export const gitlabCreatePipelineTool: ToolConfig< description: 'Trigger a new pipeline in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -40,15 +40,10 @@ export const gitlabCreatePipelineTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/pipeline` }, method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = { ref: params.ref, diff --git a/apps/sim/tools/gitlab/delete_issue.ts b/apps/sim/tools/gitlab/delete_issue.ts index c56907e3cbc..0787321c4d8 100644 --- a/apps/sim/tools/gitlab/delete_issue.ts +++ b/apps/sim/tools/gitlab/delete_issue.ts @@ -8,12 +8,12 @@ export const gitlabDeleteIssueTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/get_issue.ts b/apps/sim/tools/gitlab/get_issue.ts index 7b0547f32d5..acbe21e38e9 100644 --- a/apps/sim/tools/gitlab/get_issue.ts +++ b/apps/sim/tools/gitlab/get_issue.ts @@ -7,12 +7,12 @@ export const gitlabGetIssueTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/get_merge_request.ts b/apps/sim/tools/gitlab/get_merge_request.ts index fe5ecd6caaf..7b136961bb2 100644 --- a/apps/sim/tools/gitlab/get_merge_request.ts +++ b/apps/sim/tools/gitlab/get_merge_request.ts @@ -13,12 +13,12 @@ export const gitlabGetMergeRequestTool: ToolConfig< description: 'Get details of a specific GitLab merge request', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -37,14 +37,9 @@ export const gitlabGetMergeRequestTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}` }, method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/get_pipeline.ts b/apps/sim/tools/gitlab/get_pipeline.ts index a4d9c0c5213..e1ae7a69fa8 100644 --- a/apps/sim/tools/gitlab/get_pipeline.ts +++ b/apps/sim/tools/gitlab/get_pipeline.ts @@ -8,12 +8,12 @@ export const gitlabGetPipelineTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/get_project.ts b/apps/sim/tools/gitlab/get_project.ts index 84b13e53925..cabfcefd8e2 100644 --- a/apps/sim/tools/gitlab/get_project.ts +++ b/apps/sim/tools/gitlab/get_project.ts @@ -7,12 +7,12 @@ export const gitlabGetProjectTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/list_issues.ts b/apps/sim/tools/gitlab/list_issues.ts index e8112e0322c..83760197110 100644 --- a/apps/sim/tools/gitlab/list_issues.ts +++ b/apps/sim/tools/gitlab/list_issues.ts @@ -7,12 +7,12 @@ export const gitlabListIssuesTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/list_merge_requests.ts b/apps/sim/tools/gitlab/list_merge_requests.ts index 322c7e0341a..d40dbb15662 100644 --- a/apps/sim/tools/gitlab/list_merge_requests.ts +++ b/apps/sim/tools/gitlab/list_merge_requests.ts @@ -13,12 +13,12 @@ export const gitlabListMergeRequestsTool: ToolConfig< description: 'List merge requests in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -84,14 +84,9 @@ export const gitlabListMergeRequestsTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests${query ? `?${query}` : ''}` }, method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/list_pipelines.ts b/apps/sim/tools/gitlab/list_pipelines.ts index 7e4812bce21..71c5071255b 100644 --- a/apps/sim/tools/gitlab/list_pipelines.ts +++ b/apps/sim/tools/gitlab/list_pipelines.ts @@ -10,12 +10,12 @@ export const gitlabListPipelinesTool: ToolConfig< description: 'List pipelines in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -70,14 +70,9 @@ export const gitlabListPipelinesTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines${query ? `?${query}` : ''}` }, method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/list_projects.ts b/apps/sim/tools/gitlab/list_projects.ts index 3eeec1515ae..0840ba3b282 100644 --- a/apps/sim/tools/gitlab/list_projects.ts +++ b/apps/sim/tools/gitlab/list_projects.ts @@ -10,12 +10,12 @@ export const gitlabListProjectsTool: ToolConfig< description: 'List GitLab projects accessible to the authenticated user', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, owned: { type: 'boolean', required: false, @@ -74,14 +74,9 @@ export const gitlabListProjectsTool: ToolConfig< return `https://gitlab.com/api/v4/projects${query ? `?${query}` : ''}` }, method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/merge_merge_request.ts b/apps/sim/tools/gitlab/merge_merge_request.ts index 41fe7998e45..f16e0136108 100644 --- a/apps/sim/tools/gitlab/merge_merge_request.ts +++ b/apps/sim/tools/gitlab/merge_merge_request.ts @@ -13,12 +13,12 @@ export const gitlabMergeMergeRequestTool: ToolConfig< description: 'Merge a merge request in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -62,15 +62,10 @@ export const gitlabMergeMergeRequestTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}/merge` }, method: 'PUT', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/gitlab/retry_pipeline.ts b/apps/sim/tools/gitlab/retry_pipeline.ts index 6d65918d36c..bc24688b08b 100644 --- a/apps/sim/tools/gitlab/retry_pipeline.ts +++ b/apps/sim/tools/gitlab/retry_pipeline.ts @@ -10,12 +10,12 @@ export const gitlabRetryPipelineTool: ToolConfig< description: 'Retry a failed GitLab pipeline', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -34,14 +34,9 @@ export const gitlabRetryPipelineTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/pipelines/${params.pipelineId}/retry` }, method: 'POST', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), }, transformResponse: async (response) => { diff --git a/apps/sim/tools/gitlab/types.ts b/apps/sim/tools/gitlab/types.ts index f7d113f8967..60b535533b8 100644 --- a/apps/sim/tools/gitlab/types.ts +++ b/apps/sim/tools/gitlab/types.ts @@ -193,7 +193,7 @@ export interface GitLabMilestone { // ===== Common Parameters ===== interface GitLabBaseParams { - accessToken?: string + accessToken: string } // ===== Project Parameters ===== diff --git a/apps/sim/tools/gitlab/update_issue.ts b/apps/sim/tools/gitlab/update_issue.ts index 9f81f10251d..699664077eb 100644 --- a/apps/sim/tools/gitlab/update_issue.ts +++ b/apps/sim/tools/gitlab/update_issue.ts @@ -8,12 +8,12 @@ export const gitlabUpdateIssueTool: ToolConfig { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/gitlab/update_merge_request.ts b/apps/sim/tools/gitlab/update_merge_request.ts index a646f7f4a0d..9fd3a028ae9 100644 --- a/apps/sim/tools/gitlab/update_merge_request.ts +++ b/apps/sim/tools/gitlab/update_merge_request.ts @@ -13,12 +13,12 @@ export const gitlabUpdateMergeRequestTool: ToolConfig< description: 'Update an existing merge request in a GitLab project', version: '1.0.0', - oauth: { - required: true, - provider: 'gitlab', - }, - params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, projectId: { type: 'string', required: true, @@ -87,15 +87,10 @@ export const gitlabUpdateMergeRequestTool: ToolConfig< return `https://gitlab.com/api/v4/projects/${encodedId}/merge_requests/${params.mergeRequestIid}` }, method: 'PUT', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Missing access token for GitLab API request') - } - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/kalshi/get_balance.ts b/apps/sim/tools/kalshi/get_balance.ts new file mode 100644 index 00000000000..02796350453 --- /dev/null +++ b/apps/sim/tools/kalshi/get_balance.ts @@ -0,0 +1,89 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiAuthParams } from './types' +import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetBalanceParams extends KalshiAuthParams {} + +export interface KalshiGetBalanceResponse { + success: boolean + output: { + balance: number // In cents + portfolioValue?: number // In cents + balanceDollars: number // Converted to dollars + portfolioValueDollars?: number // Converted to dollars + metadata: { + operation: 'get_balance' + } + success: boolean + } +} + +export const kalshiGetBalanceTool: ToolConfig = { + id: 'kalshi_get_balance', + name: 'Get Balance from Kalshi', + description: 'Retrieve your account balance and portfolio value from Kalshi', + version: '1.0.0', + + params: { + keyId: { + type: 'string', + required: true, + description: 'Your Kalshi API Key ID', + }, + privateKey: { + type: 'string', + required: true, + description: 'Your RSA Private Key (PEM format)', + }, + }, + + request: { + url: () => buildKalshiUrl('/portfolio/balance'), + method: 'GET', + headers: (params) => { + const path = '/trade-api/v2/portfolio/balance' + return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path) + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_balance') + } + + const balance = data.balance || 0 + const portfolioValue = data.portfolio_value + + return { + success: true, + output: { + balance, + portfolioValue, + balanceDollars: balance / 100, + portfolioValueDollars: portfolioValue ? portfolioValue / 100 : undefined, + metadata: { + operation: 'get_balance' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Balance data and metadata', + properties: { + balance: { type: 'number', description: 'Account balance in cents' }, + portfolioValue: { type: 'number', description: 'Portfolio value in cents' }, + balanceDollars: { type: 'number', description: 'Account balance in dollars' }, + portfolioValueDollars: { type: 'number', description: 'Portfolio value in dollars' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_event.ts b/apps/sim/tools/kalshi/get_event.ts new file mode 100644 index 00000000000..148c1284634 --- /dev/null +++ b/apps/sim/tools/kalshi/get_event.ts @@ -0,0 +1,87 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiEvent } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetEventParams { + eventTicker: string // Event ticker + withNestedMarkets?: string // 'true' or 'false' +} + +export interface KalshiGetEventResponse { + success: boolean + output: { + event: KalshiEvent + metadata: { + operation: 'get_event' + } + success: boolean + } +} + +export const kalshiGetEventTool: ToolConfig = { + id: 'kalshi_get_event', + name: 'Get Event from Kalshi', + description: 'Retrieve details of a specific event by ticker', + version: '1.0.0', + + params: { + eventTicker: { + type: 'string', + required: true, + description: 'The event ticker', + }, + withNestedMarkets: { + type: 'string', + required: false, + description: 'Include nested markets in response (true/false)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.withNestedMarkets) + queryParams.append('with_nested_markets', params.withNestedMarkets) + + const query = queryParams.toString() + const url = buildKalshiUrl(`/events/${params.eventTicker}`) + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_event') + } + + return { + success: true, + output: { + event: data.event, + metadata: { + operation: 'get_event' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Event data and metadata', + properties: { + event: { type: 'object', description: 'Event object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_events.ts b/apps/sim/tools/kalshi/get_events.ts new file mode 100644 index 00000000000..6a2344a6c2b --- /dev/null +++ b/apps/sim/tools/kalshi/get_events.ts @@ -0,0 +1,116 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiEvent, KalshiPaginationParams, KalshiPagingInfo } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetEventsParams extends KalshiPaginationParams { + status?: string // open, closed, settled + seriesTicker?: string + withNestedMarkets?: string // 'true' or 'false' +} + +export interface KalshiGetEventsResponse { + success: boolean + output: { + events: KalshiEvent[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_events' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetEventsTool: ToolConfig = { + id: 'kalshi_get_events', + name: 'Get Events from Kalshi', + description: 'Retrieve a list of events from Kalshi with optional filtering', + version: '1.0.0', + + params: { + status: { + type: 'string', + required: false, + description: 'Filter by status (open, closed, settled)', + }, + seriesTicker: { + type: 'string', + required: false, + description: 'Filter by series ticker', + }, + withNestedMarkets: { + type: 'string', + required: false, + description: 'Include nested markets in response (true/false)', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-200, default: 200)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.status) queryParams.append('status', params.status) + if (params.seriesTicker) queryParams.append('series_ticker', params.seriesTicker) + if (params.withNestedMarkets) + queryParams.append('with_nested_markets', params.withNestedMarkets) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/events') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_events') + } + + const events = data.events || [] + + return { + success: true, + output: { + events, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_events' as const, + totalReturned: events.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Events data and metadata', + properties: { + events: { type: 'array', description: 'Array of event objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_market.ts b/apps/sim/tools/kalshi/get_market.ts new file mode 100644 index 00000000000..a2de39ec771 --- /dev/null +++ b/apps/sim/tools/kalshi/get_market.ts @@ -0,0 +1,73 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiMarket } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetMarketParams { + ticker: string // Market ticker +} + +export interface KalshiGetMarketResponse { + success: boolean + output: { + market: KalshiMarket + metadata: { + operation: 'get_market' + } + success: boolean + } +} + +export const kalshiGetMarketTool: ToolConfig = { + id: 'kalshi_get_market', + name: 'Get Market from Kalshi', + description: 'Retrieve details of a specific prediction market by ticker', + version: '1.0.0', + + params: { + ticker: { + type: 'string', + required: true, + description: 'The market ticker (e.g., "KXBTC-24DEC31")', + }, + }, + + request: { + url: (params) => buildKalshiUrl(`/markets/${params.ticker}`), + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_market') + } + + return { + success: true, + output: { + market: data.market, + metadata: { + operation: 'get_market' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Market data and metadata', + properties: { + market: { type: 'object', description: 'Market object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_markets.ts b/apps/sim/tools/kalshi/get_markets.ts new file mode 100644 index 00000000000..cb80345711f --- /dev/null +++ b/apps/sim/tools/kalshi/get_markets.ts @@ -0,0 +1,115 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiMarket, KalshiPaginationParams, KalshiPagingInfo } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetMarketsParams extends KalshiPaginationParams { + status?: string // unopened, open, closed, settled + seriesTicker?: string + eventTicker?: string +} + +export interface KalshiGetMarketsResponse { + success: boolean + output: { + markets: KalshiMarket[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_markets' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetMarketsTool: ToolConfig = { + id: 'kalshi_get_markets', + name: 'Get Markets from Kalshi', + description: 'Retrieve a list of prediction markets from Kalshi with optional filtering', + version: '1.0.0', + + params: { + status: { + type: 'string', + required: false, + description: 'Filter by status (unopened, open, closed, settled)', + }, + seriesTicker: { + type: 'string', + required: false, + description: 'Filter by series ticker', + }, + eventTicker: { + type: 'string', + required: false, + description: 'Filter by event ticker', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-1000, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.status) queryParams.append('status', params.status) + if (params.seriesTicker) queryParams.append('series_ticker', params.seriesTicker) + if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/markets') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_markets') + } + + const markets = data.markets || [] + + return { + success: true, + output: { + markets, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_markets' as const, + totalReturned: markets.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Markets data and metadata', + properties: { + markets: { type: 'array', description: 'Array of market objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_orders.ts b/apps/sim/tools/kalshi/get_orders.ts new file mode 100644 index 00000000000..3f5b3b041f6 --- /dev/null +++ b/apps/sim/tools/kalshi/get_orders.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' +import type { + KalshiAuthParams, + KalshiOrder, + KalshiPaginationParams, + KalshiPagingInfo, +} from './types' +import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetOrdersParams extends KalshiAuthParams, KalshiPaginationParams { + ticker?: string + eventTicker?: string + status?: string // resting, canceled, executed +} + +export interface KalshiGetOrdersResponse { + success: boolean + output: { + orders: KalshiOrder[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_orders' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetOrdersTool: ToolConfig = { + id: 'kalshi_get_orders', + name: 'Get Orders from Kalshi', + description: 'Retrieve your orders from Kalshi with optional filtering', + version: '1.0.0', + + params: { + keyId: { + type: 'string', + required: true, + description: 'Your Kalshi API Key ID', + }, + privateKey: { + type: 'string', + required: true, + description: 'Your RSA Private Key (PEM format)', + }, + ticker: { + type: 'string', + required: false, + description: 'Filter by market ticker', + }, + eventTicker: { + type: 'string', + required: false, + description: 'Filter by event ticker (max 10 comma-separated)', + }, + status: { + type: 'string', + required: false, + description: 'Filter by status (resting, canceled, executed)', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-200, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.ticker) queryParams.append('ticker', params.ticker) + if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker) + if (params.status) queryParams.append('status', params.status) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/portfolio/orders') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: (params) => { + const path = '/trade-api/v2/portfolio/orders' + return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path) + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_orders') + } + + const orders = data.orders || [] + + return { + success: true, + output: { + orders, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_orders' as const, + totalReturned: orders.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Orders data and metadata', + properties: { + orders: { type: 'array', description: 'Array of order objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_positions.ts b/apps/sim/tools/kalshi/get_positions.ts new file mode 100644 index 00000000000..80114bfe9b0 --- /dev/null +++ b/apps/sim/tools/kalshi/get_positions.ts @@ -0,0 +1,134 @@ +import type { ToolConfig } from '@/tools/types' +import type { + KalshiAuthParams, + KalshiPaginationParams, + KalshiPagingInfo, + KalshiPosition, +} from './types' +import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetPositionsParams extends KalshiAuthParams, KalshiPaginationParams { + ticker?: string + eventTicker?: string + settlementStatus?: string // all, unsettled, settled +} + +export interface KalshiGetPositionsResponse { + success: boolean + output: { + positions: KalshiPosition[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_positions' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetPositionsTool: ToolConfig< + KalshiGetPositionsParams, + KalshiGetPositionsResponse +> = { + id: 'kalshi_get_positions', + name: 'Get Positions from Kalshi', + description: 'Retrieve your open positions from Kalshi', + version: '1.0.0', + + params: { + keyId: { + type: 'string', + required: true, + description: 'Your Kalshi API Key ID', + }, + privateKey: { + type: 'string', + required: true, + description: 'Your RSA Private Key (PEM format)', + }, + ticker: { + type: 'string', + required: false, + description: 'Filter by market ticker', + }, + eventTicker: { + type: 'string', + required: false, + description: 'Filter by event ticker (max 10 comma-separated)', + }, + settlementStatus: { + type: 'string', + required: false, + description: 'Filter by settlement status (all, unsettled, settled). Default: unsettled', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-1000, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.ticker) queryParams.append('ticker', params.ticker) + if (params.eventTicker) queryParams.append('event_ticker', params.eventTicker) + if (params.settlementStatus) queryParams.append('settlement_status', params.settlementStatus) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/portfolio/positions') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: (params) => { + const path = '/trade-api/v2/portfolio/positions' + return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path) + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_positions') + } + + const positions = data.market_positions || data.positions || [] + + return { + success: true, + output: { + positions, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_positions' as const, + totalReturned: positions.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Positions data and metadata', + properties: { + positions: { type: 'array', description: 'Array of position objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/index.ts b/apps/sim/tools/kalshi/index.ts new file mode 100644 index 00000000000..64b371b47d5 --- /dev/null +++ b/apps/sim/tools/kalshi/index.ts @@ -0,0 +1,7 @@ +export { kalshiGetBalanceTool } from './get_balance' +export { kalshiGetEventTool } from './get_event' +export { kalshiGetEventsTool } from './get_events' +export { kalshiGetMarketTool } from './get_market' +export { kalshiGetMarketsTool } from './get_markets' +export { kalshiGetOrdersTool } from './get_orders' +export { kalshiGetPositionsTool } from './get_positions' diff --git a/apps/sim/tools/kalshi/types.ts b/apps/sim/tools/kalshi/types.ts new file mode 100644 index 00000000000..c591d7bc49a --- /dev/null +++ b/apps/sim/tools/kalshi/types.ts @@ -0,0 +1,170 @@ +import crypto from 'crypto' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('Kalshi') + +// Base URL for Kalshi API +export const KALSHI_BASE_URL = 'https://api.elections.kalshi.com/trade-api/v2' + +// Base params for authenticated endpoints +export interface KalshiAuthParams { + keyId: string // API Key ID + privateKey: string // RSA Private Key (PEM format) +} + +// Pagination params +export interface KalshiPaginationParams { + limit?: string // 1-1000, default 100 + cursor?: string // Pagination cursor +} + +// Pagination info in response +export interface KalshiPagingInfo { + cursor?: string | null +} + +// Generic response type +export interface KalshiResponse { + success: boolean + output: T & { + paging?: KalshiPagingInfo + metadata: { + operation: string + [key: string]: any + } + success: boolean + } +} + +// Market type +export interface KalshiMarket { + ticker: string + event_ticker: string + market_type: string + title: string + subtitle?: string + yes_sub_title?: string + no_sub_title?: string + open_time: string + close_time: string + expiration_time: string + status: string + yes_bid: number + yes_ask: number + no_bid: number + no_ask: number + last_price: number + previous_yes_bid?: number + previous_yes_ask?: number + previous_price?: number + volume: number + volume_24h: number + liquidity?: number + open_interest?: number + result?: string + cap_strike?: number + floor_strike?: number +} + +// Event type +export interface KalshiEvent { + event_ticker: string + series_ticker: string + sub_title?: string + title: string + mutually_exclusive: boolean + category: string + markets?: KalshiMarket[] + strike_date?: string + status?: string +} + +// Balance type +export interface KalshiBalance { + balance: number // In cents + portfolio_value?: number // In cents +} + +// Position type +export interface KalshiPosition { + ticker: string + event_ticker: string + event_title?: string + market_title?: string + position: number + market_exposure?: number + realized_pnl?: number + total_traded?: number + resting_orders_count?: number +} + +// Order type +export interface KalshiOrder { + order_id: string + ticker: string + event_ticker: string + status: string + side: string + type: string + yes_price?: number + no_price?: number + action: string + count: number + remaining_count: number + created_time: string + expiration_time?: string + place_count?: number + decrease_count?: number + maker_fill_count?: number + taker_fill_count?: number + taker_fees?: number +} + +// Helper function to build Kalshi API URLs +export function buildKalshiUrl(path: string): string { + return `${KALSHI_BASE_URL}${path}` +} + +// RSA signature generation for authenticated requests +export function generateKalshiSignature( + privateKey: string, + timestamp: string, + method: string, + path: string +): string { + // Sign: timestamp + method + path (without query params) + // Strip query params from path for signing + const pathWithoutQuery = path.split('?')[0] + const message = timestamp + method.toUpperCase() + pathWithoutQuery + + const sign = crypto.createSign('RSA-SHA256') + sign.update(message) + return sign.sign(privateKey, 'base64') +} + +// Build auth headers for authenticated requests +export function buildKalshiAuthHeaders( + keyId: string, + privateKey: string, + method: string, + path: string +): Record { + const timestamp = Date.now().toString() + const signature = generateKalshiSignature(privateKey, timestamp, method, path) + + return { + 'KALSHI-ACCESS-KEY': keyId, + 'KALSHI-ACCESS-TIMESTAMP': timestamp, + 'KALSHI-ACCESS-SIGNATURE': signature, + 'Content-Type': 'application/json', + } +} + +// Helper function for consistent error handling +export function handleKalshiError(data: any, status: number, operation: string): never { + logger.error(`Kalshi API request failed for ${operation}`, { data, status }) + + const errorMessage = + data.error?.message || data.error || data.message || data.detail || 'Unknown error' + throw new Error(`Kalshi ${operation} failed: ${errorMessage}`) +} diff --git a/apps/sim/tools/polymarket/get_event.ts b/apps/sim/tools/polymarket/get_event.ts new file mode 100644 index 00000000000..7beba868735 --- /dev/null +++ b/apps/sim/tools/polymarket/get_event.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketEvent } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetEventParams { + eventId?: string // Event ID + slug?: string // Event slug (alternative to ID) +} + +export interface PolymarketGetEventResponse { + success: boolean + output: { + event: PolymarketEvent + metadata: { + operation: 'get_event' + } + success: boolean + } +} + +export const polymarketGetEventTool: ToolConfig< + PolymarketGetEventParams, + PolymarketGetEventResponse +> = { + id: 'polymarket_get_event', + name: 'Get Event from Polymarket', + description: 'Retrieve details of a specific event by ID or slug', + version: '1.0.0', + + params: { + eventId: { + type: 'string', + required: false, + description: 'The event ID. Required if slug is not provided.', + }, + slug: { + type: 'string', + required: false, + description: + 'The event slug (e.g., "2024-presidential-election"). Required if eventId is not provided.', + }, + }, + + request: { + url: (params) => { + if (params.slug) { + return buildGammaUrl(`/events/slug/${params.slug}`) + } + return buildGammaUrl(`/events/${params.eventId}`) + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_event') + } + + return { + success: true, + output: { + event: data, + metadata: { + operation: 'get_event' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Event data and metadata', + properties: { + event: { type: 'object', description: 'Event object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_events.ts b/apps/sim/tools/polymarket/get_events.ts new file mode 100644 index 00000000000..c54be4d4e46 --- /dev/null +++ b/apps/sim/tools/polymarket/get_events.ts @@ -0,0 +1,121 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketEvent, PolymarketPaginationParams } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetEventsParams extends PolymarketPaginationParams { + closed?: string // 'true' or 'false' - filter for closed/active events + order?: string // sort field + ascending?: string // 'true' or 'false' - sort direction + tagId?: string // filter by tag ID +} + +export interface PolymarketGetEventsResponse { + success: boolean + output: { + events: PolymarketEvent[] + metadata: { + operation: 'get_events' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetEventsTool: ToolConfig< + PolymarketGetEventsParams, + PolymarketGetEventsResponse +> = { + id: 'polymarket_get_events', + name: 'Get Events from Polymarket', + description: 'Retrieve a list of events from Polymarket with optional filtering', + version: '1.0.0', + + params: { + closed: { + type: 'string', + required: false, + description: 'Filter by closed status (true/false). Use false for active events only.', + }, + order: { + type: 'string', + required: false, + description: 'Sort field (e.g., id, volume)', + }, + ascending: { + type: 'string', + required: false, + description: 'Sort direction (true for ascending, false for descending)', + }, + tagId: { + type: 'string', + required: false, + description: 'Filter by tag ID', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.closed) queryParams.append('closed', params.closed) + if (params.order) queryParams.append('order', params.order) + if (params.ascending) queryParams.append('ascending', params.ascending) + if (params.tagId) queryParams.append('tag_id', params.tagId) + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + const query = queryParams.toString() + const url = buildGammaUrl('/events') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_events') + } + + // Response is an array of events + const events = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + events, + metadata: { + operation: 'get_events' as const, + totalReturned: events.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Events data and metadata', + properties: { + events: { type: 'array', description: 'Array of event objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_market.ts b/apps/sim/tools/polymarket/get_market.ts new file mode 100644 index 00000000000..4d674e25acf --- /dev/null +++ b/apps/sim/tools/polymarket/get_market.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketMarket } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetMarketParams { + marketId?: string // Market ID + slug?: string // Market slug (alternative to ID) +} + +export interface PolymarketGetMarketResponse { + success: boolean + output: { + market: PolymarketMarket + metadata: { + operation: 'get_market' + } + success: boolean + } +} + +export const polymarketGetMarketTool: ToolConfig< + PolymarketGetMarketParams, + PolymarketGetMarketResponse +> = { + id: 'polymarket_get_market', + name: 'Get Market from Polymarket', + description: 'Retrieve details of a specific prediction market by ID or slug', + version: '1.0.0', + + params: { + marketId: { + type: 'string', + required: false, + description: 'The market ID. Required if slug is not provided.', + }, + slug: { + type: 'string', + required: false, + description: + 'The market slug (e.g., "will-trump-win"). Required if marketId is not provided.', + }, + }, + + request: { + url: (params) => { + if (params.slug) { + return buildGammaUrl(`/markets/slug/${params.slug}`) + } + return buildGammaUrl(`/markets/${params.marketId}`) + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_market') + } + + return { + success: true, + output: { + market: data, + metadata: { + operation: 'get_market' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Market data and metadata', + properties: { + market: { type: 'object', description: 'Market object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_markets.ts b/apps/sim/tools/polymarket/get_markets.ts new file mode 100644 index 00000000000..209992e2703 --- /dev/null +++ b/apps/sim/tools/polymarket/get_markets.ts @@ -0,0 +1,121 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketMarket, PolymarketPaginationParams } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetMarketsParams extends PolymarketPaginationParams { + closed?: string // 'true' or 'false' - filter for closed/active markets + order?: string // sort field (e.g., 'id', 'volume', 'liquidity') + ascending?: string // 'true' or 'false' - sort direction + tagId?: string // filter by tag ID +} + +export interface PolymarketGetMarketsResponse { + success: boolean + output: { + markets: PolymarketMarket[] + metadata: { + operation: 'get_markets' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetMarketsTool: ToolConfig< + PolymarketGetMarketsParams, + PolymarketGetMarketsResponse +> = { + id: 'polymarket_get_markets', + name: 'Get Markets from Polymarket', + description: 'Retrieve a list of prediction markets from Polymarket with optional filtering', + version: '1.0.0', + + params: { + closed: { + type: 'string', + required: false, + description: 'Filter by closed status (true/false). Use false for active markets only.', + }, + order: { + type: 'string', + required: false, + description: 'Sort field (e.g., id, volume, liquidity)', + }, + ascending: { + type: 'string', + required: false, + description: 'Sort direction (true for ascending, false for descending)', + }, + tagId: { + type: 'string', + required: false, + description: 'Filter by tag ID', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.closed) queryParams.append('closed', params.closed) + if (params.order) queryParams.append('order', params.order) + if (params.ascending) queryParams.append('ascending', params.ascending) + if (params.tagId) queryParams.append('tag_id', params.tagId) + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + const query = queryParams.toString() + const url = buildGammaUrl('/markets') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_markets') + } + + // Response is an array of markets + const markets = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + markets, + metadata: { + operation: 'get_markets' as const, + totalReturned: markets.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Markets data and metadata', + properties: { + markets: { type: 'array', description: 'Array of market objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_midpoint.ts b/apps/sim/tools/polymarket/get_midpoint.ts new file mode 100644 index 00000000000..d39e6e85b53 --- /dev/null +++ b/apps/sim/tools/polymarket/get_midpoint.ts @@ -0,0 +1,79 @@ +import type { ToolConfig } from '@/tools/types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetMidpointParams { + tokenId: string // The token ID (CLOB token ID from market) +} + +export interface PolymarketGetMidpointResponse { + success: boolean + output: { + midpoint: string + metadata: { + operation: 'get_midpoint' + } + success: boolean + } +} + +export const polymarketGetMidpointTool: ToolConfig< + PolymarketGetMidpointParams, + PolymarketGetMidpointResponse +> = { + id: 'polymarket_get_midpoint', + name: 'Get Midpoint Price from Polymarket', + description: 'Retrieve the midpoint price for a specific token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + return `${buildClobUrl('/midpoint')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_midpoint') + } + + return { + success: true, + output: { + midpoint: data.mid || data.midpoint || data, + metadata: { + operation: 'get_midpoint' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Midpoint price data and metadata', + properties: { + midpoint: { type: 'string', description: 'Midpoint price' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_orderbook.ts b/apps/sim/tools/polymarket/get_orderbook.ts new file mode 100644 index 00000000000..707c230fc61 --- /dev/null +++ b/apps/sim/tools/polymarket/get_orderbook.ts @@ -0,0 +1,80 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketOrderBook } from './types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetOrderbookParams { + tokenId: string // The token ID (CLOB token ID from market) +} + +export interface PolymarketGetOrderbookResponse { + success: boolean + output: { + orderbook: PolymarketOrderBook + metadata: { + operation: 'get_orderbook' + } + success: boolean + } +} + +export const polymarketGetOrderbookTool: ToolConfig< + PolymarketGetOrderbookParams, + PolymarketGetOrderbookResponse +> = { + id: 'polymarket_get_orderbook', + name: 'Get Orderbook from Polymarket', + description: 'Retrieve the order book summary for a specific token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + return `${buildClobUrl('/book')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_orderbook') + } + + return { + success: true, + output: { + orderbook: data, + metadata: { + operation: 'get_orderbook' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Orderbook data and metadata', + properties: { + orderbook: { type: 'object', description: 'Order book with bids and asks' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_price.ts b/apps/sim/tools/polymarket/get_price.ts new file mode 100644 index 00000000000..3dbc37bfa90 --- /dev/null +++ b/apps/sim/tools/polymarket/get_price.ts @@ -0,0 +1,89 @@ +import type { ToolConfig } from '@/tools/types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetPriceParams { + tokenId: string // The token ID (CLOB token ID from market) + side: string // 'buy' or 'sell' +} + +export interface PolymarketGetPriceResponse { + success: boolean + output: { + price: string + side: string + metadata: { + operation: 'get_price' + } + success: boolean + } +} + +export const polymarketGetPriceTool: ToolConfig< + PolymarketGetPriceParams, + PolymarketGetPriceResponse +> = { + id: 'polymarket_get_price', + name: 'Get Price from Polymarket', + description: 'Retrieve the market price for a specific token and side', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + side: { + type: 'string', + required: true, + description: 'Order side: buy or sell', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + queryParams.append('side', params.side.toUpperCase()) + return `${buildClobUrl('/price')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_price') + } + + return { + success: true, + output: { + price: data.price || data, + side: data.side || '', + metadata: { + operation: 'get_price' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Price data and metadata', + properties: { + price: { type: 'string', description: 'Market price' }, + side: { type: 'string', description: 'Order side' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_price_history.ts b/apps/sim/tools/polymarket/get_price_history.ts new file mode 100644 index 00000000000..a7fb4c4cba8 --- /dev/null +++ b/apps/sim/tools/polymarket/get_price_history.ts @@ -0,0 +1,114 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPriceHistoryEntry } from './types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetPriceHistoryParams { + tokenId: string // The token ID (CLOB token ID from market) + interval?: string // Duration: 1m, 1h, 6h, 1d, 1w, max (mutually exclusive with startTs/endTs) + fidelity?: number // Data resolution in minutes + startTs?: number // Start timestamp (Unix seconds UTC) + endTs?: number // End timestamp (Unix seconds UTC) +} + +export interface PolymarketGetPriceHistoryResponse { + success: boolean + output: { + history: PolymarketPriceHistoryEntry[] + metadata: { + operation: 'get_price_history' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetPriceHistoryTool: ToolConfig< + PolymarketGetPriceHistoryParams, + PolymarketGetPriceHistoryResponse +> = { + id: 'polymarket_get_price_history', + name: 'Get Price History from Polymarket', + description: 'Retrieve historical price data for a specific market token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + interval: { + type: 'string', + required: false, + description: + 'Duration ending at current time (1m, 1h, 6h, 1d, 1w, max). Mutually exclusive with startTs/endTs.', + }, + fidelity: { + type: 'number', + required: false, + description: 'Data resolution in minutes (e.g., 60 for hourly)', + }, + startTs: { + type: 'number', + required: false, + description: 'Start timestamp (Unix seconds UTC)', + }, + endTs: { + type: 'number', + required: false, + description: 'End timestamp (Unix seconds UTC)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('market', params.tokenId) + if (params.interval) queryParams.append('interval', params.interval) + if (params.fidelity !== undefined) queryParams.append('fidelity', String(params.fidelity)) + if (params.startTs !== undefined) queryParams.append('startTs', String(params.startTs)) + if (params.endTs !== undefined) queryParams.append('endTs', String(params.endTs)) + return `${buildClobUrl('/prices-history')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_price_history') + } + + // Response is typically { history: [...] } or just an array + const history = data.history || (Array.isArray(data) ? data : []) + + return { + success: true, + output: { + history, + metadata: { + operation: 'get_price_history' as const, + totalReturned: history.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Price history data and metadata', + properties: { + history: { type: 'array', description: 'Array of price history entries' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/index.ts b/apps/sim/tools/polymarket/index.ts new file mode 100644 index 00000000000..a0263a89d71 --- /dev/null +++ b/apps/sim/tools/polymarket/index.ts @@ -0,0 +1,9 @@ +export * from './get_event' +export * from './get_events' +export * from './get_market' +export * from './get_markets' +export * from './get_midpoint' +export * from './get_orderbook' +export * from './get_price' +export * from './get_price_history' +export * from './types' diff --git a/apps/sim/tools/polymarket/types.ts b/apps/sim/tools/polymarket/types.ts new file mode 100644 index 00000000000..dc7ed4b0563 --- /dev/null +++ b/apps/sim/tools/polymarket/types.ts @@ -0,0 +1,134 @@ +// Polymarket API Types and Helpers + +// Base URLs for different Polymarket APIs +export const POLYMARKET_GAMMA_URL = 'https://gamma-api.polymarket.com' +export const POLYMARKET_CLOB_URL = 'https://clob.polymarket.com' + +// Helper to build Gamma API URL +export function buildGammaUrl(path: string): string { + return `${POLYMARKET_GAMMA_URL}${path}` +} + +// Helper to build CLOB API URL +export function buildClobUrl(path: string): string { + return `${POLYMARKET_CLOB_URL}${path}` +} + +// Common pagination parameters +export interface PolymarketPaginationParams { + limit?: string + offset?: string +} + +// Paging info in responses +export interface PolymarketPagingInfo { + limit: number + offset: number + count: number +} + +// Market structure +export interface PolymarketMarket { + id: string + question: string + conditionId: string + slug: string + resolutionSource: string + endDate: string + liquidity: string + startDate: string + image: string + icon: string + description: string + outcomes: string + outcomePrices: string + volume: string + active: boolean + closed: boolean + marketMakerAddress: string + createdAt: string + updatedAt: string + new: boolean + featured: boolean + submitted_by: string + archived: boolean + resolvedBy: string + restricted: boolean + groupItemTitle: string + groupItemThreshold: string + questionID: string + enableOrderBook: boolean + orderPriceMinTickSize: number + orderMinSize: number + volumeNum: number + liquidityNum: number + clobTokenIds: string[] + acceptingOrders: boolean + negRisk: boolean +} + +// Event structure +export interface PolymarketEvent { + id: string + ticker: string + slug: string + title: string + description: string + startDate: string + creationDate: string + endDate: string + image: string + icon: string + active: boolean + closed: boolean + archived: boolean + new: boolean + featured: boolean + restricted: boolean + liquidity: number + volume: number + openInterest: number + commentCount: number + markets: PolymarketMarket[] +} + +// Tag structure +export interface PolymarketTag { + id: string + label: string + slug: string +} + +// Order book entry +export interface PolymarketOrderBookEntry { + price: string + size: string +} + +// Order book structure +export interface PolymarketOrderBook { + market: string + asset_id: string + hash: string + timestamp: string + bids: PolymarketOrderBookEntry[] + asks: PolymarketOrderBookEntry[] +} + +// Price structure +export interface PolymarketPrice { + price: string + side: string +} + +// Price history entry +export interface PolymarketPriceHistoryEntry { + t: number // timestamp + p: number // price +} + +// Error handler for Polymarket API responses +export function handlePolymarketError(data: any, status: number, operation: string): never { + const errorMessage = data?.message || data?.error || `Unknown error during ${operation}` + throw new Error(`Polymarket API error (${status}): ${errorMessage}`) +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0658f845538..38579e71b19 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -51,20 +51,6 @@ import { asanaSearchTasksTool, asanaUpdateTaskTool, } from '@/tools/asana' -import { - boxCopyFileTool, - boxCreateCollaborationTool, - boxCreateFolderTool, - boxCreateSharedLinkTool, - boxDeleteFileTool, - boxDeleteFolderTool, - boxDownloadFileTool, - boxGetCurrentUserTool, - boxGetFileTool, - boxListFolderTool, - boxSearchTool, - boxUpdateFileTool, -} from '@/tools/box' import { browserUseRunTaskTool } from '@/tools/browser_use' import { calendlyCancelEventTool, @@ -456,6 +442,15 @@ import { jiraUpdateWorklogTool, jiraWriteTool, } from '@/tools/jira' +import { + kalshiGetBalanceTool, + kalshiGetEventsTool, + kalshiGetEventTool, + kalshiGetMarketsTool, + kalshiGetMarketTool, + kalshiGetOrdersTool, + kalshiGetPositionsTool, +} from '@/tools/kalshi' import { knowledgeCreateDocumentTool, knowledgeSearchTool, @@ -748,6 +743,16 @@ import { pipedriveUpdateDealTool, pipedriveUpdateLeadTool, } from '@/tools/pipedrive' +import { + polymarketGetEventsTool, + polymarketGetEventTool, + polymarketGetMarketsTool, + polymarketGetMarketTool, + polymarketGetMidpointTool, + polymarketGetOrderbookTool, + polymarketGetPriceHistoryTool, + polymarketGetPriceTool, +} from '@/tools/polymarket' import { deleteTool as postgresDeleteTool, executeTool as postgresExecuteTool, @@ -1239,18 +1244,6 @@ export const tools: Record = { asana_search_tasks: asanaSearchTasksTool, asana_add_comment: asanaAddCommentTool, browser_use_run_task: browserUseRunTaskTool, - box_get_file: boxGetFileTool, - box_list_folder: boxListFolderTool, - box_create_folder: boxCreateFolderTool, - box_delete_file: boxDeleteFileTool, - box_delete_folder: boxDeleteFolderTool, - box_copy_file: boxCopyFileTool, - box_update_file: boxUpdateFileTool, - box_search: boxSearchTool, - box_download_file: boxDownloadFileTool, - box_get_current_user: boxGetCurrentUserTool, - box_create_shared_link: boxCreateSharedLinkTool, - box_create_collaboration: boxCreateCollaborationTool, openai_embeddings: openAIEmbeddings, http_request: httpRequest, huggingface_chat: huggingfaceChatTool, @@ -1352,6 +1345,21 @@ export const tools: Record = { jira_delete_issue_link: jiraDeleteIssueLinkTool, jira_add_watcher: jiraAddWatcherTool, jira_remove_watcher: jiraRemoveWatcherTool, + kalshi_get_markets: kalshiGetMarketsTool, + kalshi_get_market: kalshiGetMarketTool, + kalshi_get_events: kalshiGetEventsTool, + kalshi_get_event: kalshiGetEventTool, + kalshi_get_balance: kalshiGetBalanceTool, + kalshi_get_positions: kalshiGetPositionsTool, + kalshi_get_orders: kalshiGetOrdersTool, + polymarket_get_markets: polymarketGetMarketsTool, + polymarket_get_market: polymarketGetMarketTool, + polymarket_get_events: polymarketGetEventsTool, + polymarket_get_event: polymarketGetEventTool, + polymarket_get_orderbook: polymarketGetOrderbookTool, + polymarket_get_price: polymarketGetPriceTool, + polymarket_get_midpoint: polymarketGetMidpointTool, + polymarket_get_price_history: polymarketGetPriceHistoryTool, slack_message: slackMessageTool, slack_message_reader: slackMessageReaderTool, slack_canvas: slackCanvasTool, diff --git a/apps/sim/tools/shopify/adjust_inventory.ts b/apps/sim/tools/shopify/adjust_inventory.ts index 27b40c7b662..b80db6aae43 100644 --- a/apps/sim/tools/shopify/adjust_inventory.ts +++ b/apps/sim/tools/shopify/adjust_inventory.ts @@ -43,7 +43,8 @@ export const shopifyAdjustInventoryTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/cancel_order.ts b/apps/sim/tools/shopify/cancel_order.ts index b83156d8fb3..30893da11ab 100644 --- a/apps/sim/tools/shopify/cancel_order.ts +++ b/apps/sim/tools/shopify/cancel_order.ts @@ -58,7 +58,8 @@ export const shopifyCancelOrderTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/create_customer.ts b/apps/sim/tools/shopify/create_customer.ts index a3e94db9169..62c9b1e503e 100644 --- a/apps/sim/tools/shopify/create_customer.ts +++ b/apps/sim/tools/shopify/create_customer.ts @@ -67,7 +67,8 @@ export const shopifyCreateCustomerTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/create_product.ts b/apps/sim/tools/shopify/create_product.ts index c39e1b570c8..9bbc92fa5d6 100644 --- a/apps/sim/tools/shopify/create_product.ts +++ b/apps/sim/tools/shopify/create_product.ts @@ -61,7 +61,8 @@ export const shopifyCreateProductTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/delete_customer.ts b/apps/sim/tools/shopify/delete_customer.ts index 657bd37eeef..994a26326d1 100644 --- a/apps/sim/tools/shopify/delete_customer.ts +++ b/apps/sim/tools/shopify/delete_customer.ts @@ -31,7 +31,8 @@ export const shopifyDeleteCustomerTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/delete_product.ts b/apps/sim/tools/shopify/delete_product.ts index 751b57c6e02..7b0bfed161a 100644 --- a/apps/sim/tools/shopify/delete_product.ts +++ b/apps/sim/tools/shopify/delete_product.ts @@ -31,7 +31,8 @@ export const shopifyDeleteProductTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/get_customer.ts b/apps/sim/tools/shopify/get_customer.ts index 3c1fc5f6c82..4eefd4a94b7 100644 --- a/apps/sim/tools/shopify/get_customer.ts +++ b/apps/sim/tools/shopify/get_customer.ts @@ -29,7 +29,8 @@ export const shopifyGetCustomerTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/get_inventory_level.ts b/apps/sim/tools/shopify/get_inventory_level.ts index ff9da7560cc..6ad8d58a579 100644 --- a/apps/sim/tools/shopify/get_inventory_level.ts +++ b/apps/sim/tools/shopify/get_inventory_level.ts @@ -37,7 +37,8 @@ export const shopifyGetInventoryLevelTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/get_order.ts b/apps/sim/tools/shopify/get_order.ts index 52d1ac04141..103c160a9e5 100644 --- a/apps/sim/tools/shopify/get_order.ts +++ b/apps/sim/tools/shopify/get_order.ts @@ -28,7 +28,8 @@ export const shopifyGetOrderTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/get_product.ts b/apps/sim/tools/shopify/get_product.ts index 50782f0d3d0..98d2fc92230 100644 --- a/apps/sim/tools/shopify/get_product.ts +++ b/apps/sim/tools/shopify/get_product.ts @@ -28,7 +28,8 @@ export const shopifyGetProductTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/list_customers.ts b/apps/sim/tools/shopify/list_customers.ts index 03481872442..6ab9c6e70a0 100644 --- a/apps/sim/tools/shopify/list_customers.ts +++ b/apps/sim/tools/shopify/list_customers.ts @@ -38,7 +38,8 @@ export const shopifyListCustomersTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/list_orders.ts b/apps/sim/tools/shopify/list_orders.ts index 6643489f2a7..033a8687d1f 100644 --- a/apps/sim/tools/shopify/list_orders.ts +++ b/apps/sim/tools/shopify/list_orders.ts @@ -35,7 +35,8 @@ export const shopifyListOrdersTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/list_products.ts b/apps/sim/tools/shopify/list_products.ts index 48ac9baae04..4e96217dbf5 100644 --- a/apps/sim/tools/shopify/list_products.ts +++ b/apps/sim/tools/shopify/list_products.ts @@ -38,7 +38,8 @@ export const shopifyListProductsTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/types.ts b/apps/sim/tools/shopify/types.ts index 678ebbd35c9..266f0f9c8d1 100644 --- a/apps/sim/tools/shopify/types.ts +++ b/apps/sim/tools/shopify/types.ts @@ -178,6 +178,7 @@ export interface ShopifyInventoryItem { export interface ShopifyBaseParams { accessToken: string shopDomain: string + idToken?: string // Shop domain from OAuth, used as fallback } // Product Tool Params diff --git a/apps/sim/tools/shopify/update_customer.ts b/apps/sim/tools/shopify/update_customer.ts index 622ca98129a..3a06fd2fa9f 100644 --- a/apps/sim/tools/shopify/update_customer.ts +++ b/apps/sim/tools/shopify/update_customer.ts @@ -67,7 +67,8 @@ export const shopifyUpdateCustomerTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/update_order.ts b/apps/sim/tools/shopify/update_order.ts index 904c95f2af3..13cd6bccf51 100644 --- a/apps/sim/tools/shopify/update_order.ts +++ b/apps/sim/tools/shopify/update_order.ts @@ -46,7 +46,8 @@ export const shopifyUpdateOrderTool: ToolConfig `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/shopify/update_product.ts b/apps/sim/tools/shopify/update_product.ts index d744da14d53..b817cbc81ff 100644 --- a/apps/sim/tools/shopify/update_product.ts +++ b/apps/sim/tools/shopify/update_product.ts @@ -67,7 +67,8 @@ export const shopifyUpdateProductTool: ToolConfig< }, request: { - url: (params) => `https://${params.shopDomain}/admin/api/2024-10/graphql.json`, + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, method: 'POST', headers: (params) => { if (!params.accessToken) { diff --git a/apps/sim/tools/zendesk/get_tickets.ts b/apps/sim/tools/zendesk/get_tickets.ts index 97846c68b8b..ca651eaafe1 100644 --- a/apps/sim/tools/zendesk/get_tickets.ts +++ b/apps/sim/tools/zendesk/get_tickets.ts @@ -120,12 +120,35 @@ export const zendeskGetTicketsTool: ToolConfig { + const hasFilters = + params.status || + params.priority || + params.type || + params.assigneeId || + params.organizationId + + if (hasFilters) { + // Use Search API for filtering - the /tickets endpoint doesn't support filter params + // Build search query using Zendesk search syntax + const searchTerms: string[] = ['type:ticket'] + if (params.status) searchTerms.push(`status:${params.status}`) + if (params.priority) searchTerms.push(`priority:${params.priority}`) + if (params.type) searchTerms.push(`ticket_type:${params.type}`) + if (params.assigneeId) searchTerms.push(`assignee_id:${params.assigneeId}`) + if (params.organizationId) searchTerms.push(`organization_id:${params.organizationId}`) + + const queryParams = new URLSearchParams() + queryParams.append('query', searchTerms.join(' ')) + if (params.sortBy) queryParams.append('sort_by', params.sortBy) + if (params.sortOrder) queryParams.append('sort_order', params.sortOrder) + if (params.page) queryParams.append('page', params.page) + if (params.perPage) queryParams.append('per_page', params.perPage) + + return `${buildZendeskUrl(params.subdomain, '/search')}?${queryParams.toString()}` + } + + // No filters - use the simple /tickets endpoint const queryParams = new URLSearchParams() - if (params.status) queryParams.append('status', params.status) - if (params.priority) queryParams.append('priority', params.priority) - if (params.type) queryParams.append('type', params.type) - if (params.assigneeId) queryParams.append('assignee_id', params.assigneeId) - if (params.organizationId) queryParams.append('organization_id', params.organizationId) if (params.sortBy) queryParams.append('sort_by', params.sortBy) if (params.sortOrder) queryParams.append('sort_order', params.sortOrder) if (params.page) queryParams.append('page', params.page) @@ -154,7 +177,8 @@ export const zendeskGetTicketsTool: ToolConfig Date: Wed, 3 Dec 2025 14:52:45 -0800 Subject: [PATCH 03/11] fix search modal bg instead of bgColor, added polymarket and kalshi new endpoints --- apps/docs/components/icons.tsx | 2 +- apps/docs/components/ui/icon-mapping.ts | 4 +- apps/docs/content/docs/en/tools/kalshi.mdx | 140 +++++++++++- .../docs/content/docs/en/tools/polymarket.mdx | 164 +++++++++++++- .../templates/components/template-card.tsx | 4 +- .../search-modal/search-modal.tsx | 4 +- apps/sim/blocks/blocks/ahrefs.ts | 4 +- apps/sim/blocks/blocks/datadog.ts | 150 +++++++------ apps/sim/blocks/blocks/kalshi.ts | 206 +++++++++++++++++- apps/sim/blocks/blocks/polymarket.ts | 104 ++++++++- apps/sim/components/icons.tsx | 2 +- apps/sim/lib/uploads/utils/file-utils.ts | 19 +- .../serializer/tests/dual-validation.test.ts | 4 +- apps/sim/tools/ahrefs/referring_domains.ts | 2 +- apps/sim/tools/datadog/list_monitors.ts | 36 ++- apps/sim/tools/kalshi/get_candlesticks.ts | 120 ++++++++++ apps/sim/tools/kalshi/get_exchange_status.ts | 75 +++++++ apps/sim/tools/kalshi/get_fills.ts | 138 ++++++++++++ apps/sim/tools/kalshi/get_orderbook.ts | 93 ++++++++ apps/sim/tools/kalshi/get_series.ts | 101 +++++++++ apps/sim/tools/kalshi/get_series_by_ticker.ts | 82 +++++++ apps/sim/tools/kalshi/get_trades.ts | 115 ++++++++++ apps/sim/tools/kalshi/index.ts | 7 + apps/sim/tools/kalshi/types.ts | 61 ++++++ .../tools/polymarket/get_last_trade_price.ts | 81 +++++++ apps/sim/tools/polymarket/get_positions.ts | 95 ++++++++ apps/sim/tools/polymarket/get_series.ts | 92 ++++++++ apps/sim/tools/polymarket/get_series_by_id.ts | 78 +++++++ apps/sim/tools/polymarket/get_spread.ts | 82 +++++++ apps/sim/tools/polymarket/get_tags.ts | 90 ++++++++ apps/sim/tools/polymarket/get_tick_size.ts | 81 +++++++ apps/sim/tools/polymarket/get_trades.ts | 107 +++++++++ apps/sim/tools/polymarket/index.ts | 9 + apps/sim/tools/polymarket/search.ts | 99 +++++++++ apps/sim/tools/polymarket/types.ts | 50 +++++ apps/sim/tools/registry.ts | 32 +++ apps/sim/tools/utils.test.ts | 14 +- apps/sim/tools/utils.ts | 2 +- 38 files changed, 2443 insertions(+), 106 deletions(-) create mode 100644 apps/sim/tools/kalshi/get_candlesticks.ts create mode 100644 apps/sim/tools/kalshi/get_exchange_status.ts create mode 100644 apps/sim/tools/kalshi/get_fills.ts create mode 100644 apps/sim/tools/kalshi/get_orderbook.ts create mode 100644 apps/sim/tools/kalshi/get_series.ts create mode 100644 apps/sim/tools/kalshi/get_series_by_ticker.ts create mode 100644 apps/sim/tools/kalshi/get_trades.ts create mode 100644 apps/sim/tools/polymarket/get_last_trade_price.ts create mode 100644 apps/sim/tools/polymarket/get_positions.ts create mode 100644 apps/sim/tools/polymarket/get_series.ts create mode 100644 apps/sim/tools/polymarket/get_series_by_id.ts create mode 100644 apps/sim/tools/polymarket/get_spread.ts create mode 100644 apps/sim/tools/polymarket/get_tags.ts create mode 100644 apps/sim/tools/polymarket/get_tick_size.ts create mode 100644 apps/sim/tools/polymarket/get_trades.ts create mode 100644 apps/sim/tools/polymarket/search.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4837d5c53b4..6564a6d21c0 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3986,7 +3986,7 @@ export function WordpressIcon(props: SVGProps) { ) } -export function AhRefsIcon(props: SVGProps) { +export function AhrefsIcon(props: SVGProps) { return ( = { apollo: ApolloIcon, apify: ApifyIcon, airtable: AirtableIcon, - ahrefs: AhRefsIcon, + ahrefs: AhrefsIcon, } diff --git a/apps/docs/content/docs/en/tools/kalshi.mdx b/apps/docs/content/docs/en/tools/kalshi.mdx index 6248de43872..0003dcea0d6 100644 --- a/apps/docs/content/docs/en/tools/kalshi.mdx +++ b/apps/docs/content/docs/en/tools/kalshi.mdx @@ -10,7 +10,6 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="#09C285" /> - {/* MANUAL-CONTENT-START:intro */} [Kalshi](https://kalshi.com) is a federally regulated exchange that enables users to trade directly on the outcomes of future events, known as prediction markets. Kalshi offers an open API, giving developers access to real-time market and event data, as well as account and trading information for automated or data-driven workflows. @@ -26,9 +25,10 @@ Key features of the Kalshi integration include: By using the documented API tools, you can seamlessly incorporate Kalshi's rich event and financial market data into your AI workflows, dashboards, and trading automations. {/* MANUAL-CONTENT-END */} + ## Usage Instructions -Integrate Kalshi prediction markets into the workflow. Can get markets, get market, get events, get event, get balance, get positions, get orders. +Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, and exchange status. @@ -175,6 +175,142 @@ Retrieve your orders from Kalshi with optional filtering | `success` | boolean | Operation success status | | `output` | object | Orders data and metadata | +### `kalshi_get_orderbook` + +Retrieve the orderbook (bids and asks) for a specific market + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `ticker` | string | Yes | Market ticker \(e.g., KXBTC-24DEC31\) | +| `depth` | number | No | Number of price levels to return per side | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Orderbook data and metadata | + +### `kalshi_get_trades` + +Retrieve recent trades across all markets or for a specific market + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `ticker` | string | No | Filter by market ticker | +| `minTs` | number | No | Minimum timestamp \(Unix milliseconds\) | +| `maxTs` | number | No | Maximum timestamp \(Unix milliseconds\) | +| `limit` | string | No | Number of results \(1-1000, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Trades data and metadata | + +### `kalshi_get_candlesticks` + +Retrieve OHLC candlestick data for a specific market + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `seriesTicker` | string | Yes | Series ticker | +| `ticker` | string | Yes | Market ticker \(e.g., KXBTC-24DEC31\) | +| `startTs` | number | No | Start timestamp \(Unix milliseconds\) | +| `endTs` | number | No | End timestamp \(Unix milliseconds\) | +| `periodInterval` | number | No | Period interval: 1 \(1min\), 60 \(1hour\), or 1440 \(1day\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Candlestick data and metadata | + +### `kalshi_get_fills` + +Retrieve your portfolio + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `keyId` | string | Yes | Your Kalshi API Key ID | +| `privateKey` | string | Yes | Your RSA Private Key \(PEM format\) | +| `ticker` | string | No | Filter by market ticker | +| `orderId` | string | No | Filter by order ID | +| `minTs` | number | No | Minimum timestamp \(Unix milliseconds\) | +| `maxTs` | number | No | Maximum timestamp \(Unix milliseconds\) | +| `limit` | string | No | Number of results \(1-1000, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Fills data and metadata | + +### `kalshi_get_series` + +Retrieve a list of market series templates from Kalshi + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `status` | string | No | Filter by status | +| `limit` | string | No | Number of results \(1-1000, default: 100\) | +| `cursor` | string | No | Pagination cursor for next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Series data and metadata | + +### `kalshi_get_series_by_ticker` + +Retrieve details of a specific market series by ticker + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `seriesTicker` | string | Yes | Series ticker | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Series data and metadata | + +### `kalshi_get_exchange_status` + +Retrieve the current status of the Kalshi exchange (trading and exchange activity) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Exchange status data and metadata | + ## Notes diff --git a/apps/docs/content/docs/en/tools/polymarket.mdx b/apps/docs/content/docs/en/tools/polymarket.mdx index d3f9501d8b5..2ce9bb98641 100644 --- a/apps/docs/content/docs/en/tools/polymarket.mdx +++ b/apps/docs/content/docs/en/tools/polymarket.mdx @@ -26,9 +26,10 @@ Key features of the Polymarket integration include: By using these documented API endpoints, you can seamlessly integrate Polymarket’s rich on-chain prediction market data into your own AI workflows, dashboards, research tools, and trading automations. {/* MANUAL-CONTENT-END */} + ## Usage Instructions -Integrate Polymarket prediction markets into the workflow. Can get markets, get market, get events, get event, get orderbook, get price, get midpoint, and get price history. +Integrate Polymarket prediction markets into the workflow. Can get markets, market, events, event, tags, series, orderbook, price, midpoint, price history, last trade price, spread, tick size, positions, trades, and search. @@ -114,6 +115,78 @@ Retrieve details of a specific event by ID or slug | `success` | boolean | Operation success status | | `output` | object | Event data and metadata | +### `polymarket_get_tags` + +Retrieve available tags for filtering markets from Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Tags data and metadata | + +### `polymarket_search` + +Search for markets, events, and profiles on Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query term | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Search results and metadata | + +### `polymarket_get_series` + +Retrieve series (related market groups) from Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Series data and metadata | + +### `polymarket_get_series_by_id` + +Retrieve a specific series (related market group) by ID from Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `seriesId` | string | Yes | The series ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Series data and metadata | + ### `polymarket_get_orderbook` Retrieve the order book summary for a specific token @@ -187,6 +260,95 @@ Retrieve historical price data for a specific market token | `success` | boolean | Operation success status | | `output` | object | Price history data and metadata | +### `polymarket_get_last_trade_price` + +Retrieve the last trade price for a specific token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Last trade price and metadata | + +### `polymarket_get_spread` + +Retrieve the bid-ask spread for a specific token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Spread data and metadata | + +### `polymarket_get_tick_size` + +Retrieve the minimum tick size for a specific token + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `tokenId` | string | Yes | The CLOB token ID \(from market clobTokenIds\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Tick size and metadata | + +### `polymarket_get_positions` + +Retrieve user positions from Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user` | string | Yes | User wallet address | +| `market` | string | No | Optional market ID to filter positions | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Positions data and metadata | + +### `polymarket_get_trades` + +Retrieve trade history from Polymarket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user` | string | No | User wallet address to filter trades | +| `market` | string | No | Market ID to filter trades | +| `limit` | string | No | Number of results per page \(recommended: 25-50\) | +| `offset` | string | No | Pagination offset \(skip this many results\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Trades data and metadata | + ## Notes diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 0bd1686eff5..f6fe4ead2ab 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -232,7 +232,7 @@ function TemplateCardInner({ key={index} className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]' style={{ - backgroundColor: blockConfig.bgColor || 'gray', + background: blockConfig.bgColor || 'gray', marginLeft: index > 0 ? '-4px' : '0', }} > @@ -257,7 +257,7 @@ function TemplateCardInner({ key={index} className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]' style={{ - backgroundColor: blockConfig.bgColor || 'gray', + background: blockConfig.bgColor || 'gray', marginLeft: index > 0 ? '-4px' : '0', }} > diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/search-modal/search-modal.tsx index 25cce42b24f..a7cdba45cc7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/search-modal/search-modal.tsx @@ -564,9 +564,7 @@ export function SearchModal({
= { docsLink: 'https://docs.ahrefs.com/docs/api/reference/introduction', category: 'tools', bgColor: '#E0E0E0', - icon: AhRefsIcon, + icon: AhrefsIcon, subBlocks: [ { id: 'operation', diff --git a/apps/sim/blocks/blocks/datadog.ts b/apps/sim/blocks/blocks/datadog.ts index 32f1134c5e0..d0da31eb65d 100644 --- a/apps/sim/blocks/blocks/datadog.ts +++ b/apps/sim/blocks/blocks/datadog.ts @@ -437,75 +437,99 @@ export const DatadogBlock: BlockConfig = { 'datadog_cancel_downtime', ], config: { - tool: (params) => { - // Map the input field IDs to the expected tool parameter names - const mappedParams = { ...params } - - // Handle monitor operations - if (params.monitorQuery) { - mappedParams.query = params.monitorQuery - } - if (params.monitorTags) { - mappedParams.tags = params.monitorTags - } - if (params.monitorPriority) { - mappedParams.priority = Number(params.monitorPriority) - } - if (params.muteMonitorId) { - mappedParams.monitorId = params.muteMonitorId + tool: (params) => params.operation, + params: (params) => { + // Base params that are always needed + const baseParams: Record = { + apiKey: params.apiKey, + applicationKey: params.applicationKey, + site: params.site, } - // Handle log operations - if (params.logQuery) { - mappedParams.query = params.logQuery - } - if (params.logFrom) { - mappedParams.from = params.logFrom - } - if (params.logTo) { - mappedParams.to = params.logTo - } - if (params.logLimit) { - mappedParams.limit = Number(params.logLimit) - } + // Only include params relevant to each operation + switch (params.operation) { + case 'datadog_submit_metrics': + return { ...baseParams, series: params.series } - // Handle downtime operations - if (params.downtimeScope) { - mappedParams.scope = params.downtimeScope - } - if (params.downtimeMessage) { - mappedParams.message = params.downtimeMessage - } - if (params.downtimeStart) { - mappedParams.start = Number(params.downtimeStart) - } - if (params.downtimeEnd) { - mappedParams.end = Number(params.downtimeEnd) - } - if (params.downtimeMonitorId) { - mappedParams.monitorId = params.downtimeMonitorId - } + case 'datadog_query_timeseries': + return { + ...baseParams, + query: params.query, + from: params.from ? Number(params.from) : undefined, + to: params.to ? Number(params.to) : undefined, + } - // Handle list monitors - if (params.listMonitorName) { - mappedParams.name = params.listMonitorName - } - if (params.listMonitorTags) { - mappedParams.tags = params.listMonitorTags - } + case 'datadog_create_event': + return { + ...baseParams, + title: params.title, + text: params.text, + alertType: params.alertType, + priority: params.priority, + tags: params.tags, + } - // Convert numeric fields - if (params.from) { - mappedParams.from = Number(params.from) - } - if (params.to) { - mappedParams.to = Number(params.to) - } - if (params.end) { - mappedParams.end = Number(params.end) - } + case 'datadog_create_monitor': + return { + ...baseParams, + name: params.name, + type: params.type, + query: params.monitorQuery, + message: params.message, + tags: params.monitorTags, + priority: params.monitorPriority ? Number(params.monitorPriority) : undefined, + options: params.options, + } + + case 'datadog_get_monitor': + return { ...baseParams, monitorId: params.monitorId } + + case 'datadog_list_monitors': + return { + ...baseParams, + name: params.listMonitorName || undefined, + tags: params.listMonitorTags || undefined, + } + + case 'datadog_mute_monitor': + return { + ...baseParams, + monitorId: params.muteMonitorId, + scope: params.scope, + end: params.end ? Number(params.end) : undefined, + } - return params.operation + case 'datadog_query_logs': + return { + ...baseParams, + query: params.logQuery, + from: params.logFrom, + to: params.logTo, + limit: params.logLimit ? Number(params.logLimit) : undefined, + } + + case 'datadog_send_logs': + return { ...baseParams, logs: params.logs } + + case 'datadog_create_downtime': + return { + ...baseParams, + scope: params.downtimeScope, + message: params.downtimeMessage, + start: params.downtimeStart ? Number(params.downtimeStart) : undefined, + end: params.downtimeEnd ? Number(params.downtimeEnd) : undefined, + monitorId: params.downtimeMonitorId, + } + + case 'datadog_list_downtimes': + return { ...baseParams, currentOnly: params.currentOnly } + + case 'datadog_cancel_downtime': + return { ...baseParams, downtimeId: params.downtimeId } + + default: + return baseParams + } }, }, }, diff --git a/apps/sim/blocks/blocks/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts index 0623b1c6cbe..630fc73c066 100644 --- a/apps/sim/blocks/blocks/kalshi.ts +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -7,7 +7,7 @@ export const KalshiBlock: BlockConfig = { name: 'Kalshi', description: 'Access prediction markets data from Kalshi', longDescription: - 'Integrate Kalshi prediction markets into the workflow. Can get markets, get market, get events, get event, get balance, get positions, get orders.', + 'Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, and exchange status.', docsLink: 'https://docs.sim.ai/tools/kalshi', authMode: AuthMode.ApiKey, category: 'tools', @@ -26,6 +26,13 @@ export const KalshiBlock: BlockConfig = { { label: 'Get Balance', id: 'get_balance' }, { label: 'Get Positions', id: 'get_positions' }, { label: 'Get Orders', id: 'get_orders' }, + { label: 'Get Orderbook', id: 'get_orderbook' }, + { label: 'Get Trades', id: 'get_trades' }, + { label: 'Get Candlesticks', id: 'get_candlesticks' }, + { label: 'Get Fills', id: 'get_fills' }, + { label: 'Get Series', id: 'get_series' }, + { label: 'Get Series by Ticker', id: 'get_series_by_ticker' }, + { label: 'Get Exchange Status', id: 'get_exchange_status' }, ], value: () => 'get_markets', }, @@ -35,7 +42,10 @@ export const KalshiBlock: BlockConfig = { title: 'API Key ID', type: 'short-input', placeholder: 'Your Kalshi API Key ID', - condition: { field: 'operation', value: ['get_balance', 'get_positions', 'get_orders'] }, + condition: { + field: 'operation', + value: ['get_balance', 'get_positions', 'get_orders', 'get_fills'], + }, required: true, }, { @@ -44,7 +54,10 @@ export const KalshiBlock: BlockConfig = { type: 'long-input', password: true, placeholder: 'Your RSA Private Key (PEM format)', - condition: { field: 'operation', value: ['get_balance', 'get_positions', 'get_orders'] }, + condition: { + field: 'operation', + value: ['get_balance', 'get_positions', 'get_orders', 'get_fills'], + }, required: true, }, // Get Markets fields @@ -89,7 +102,7 @@ export const KalshiBlock: BlockConfig = { type: 'short-input', placeholder: 'Market ticker (e.g., KXBTC-24DEC31)', required: true, - condition: { field: 'operation', value: ['get_market'] }, + condition: { field: 'operation', value: ['get_market', 'get_orderbook'] }, }, // Ticker filter for get_orders and get_positions - OPTIONAL { @@ -135,6 +148,111 @@ export const KalshiBlock: BlockConfig = { ], condition: { field: 'operation', value: ['get_orders'] }, }, + // Get Orderbook fields + { + id: 'depth', + title: 'Depth', + type: 'short-input', + placeholder: 'Number of price levels per side', + condition: { field: 'operation', value: ['get_orderbook'] }, + }, + // Get Trades fields + { + id: 'tickerTrades', + title: 'Market Ticker', + type: 'short-input', + placeholder: 'Filter by market ticker (optional)', + condition: { field: 'operation', value: ['get_trades'] }, + }, + { + id: 'minTs', + title: 'Min Timestamp', + type: 'short-input', + placeholder: 'Minimum timestamp (Unix milliseconds)', + condition: { field: 'operation', value: ['get_trades', 'get_fills'] }, + }, + { + id: 'maxTs', + title: 'Max Timestamp', + type: 'short-input', + placeholder: 'Maximum timestamp (Unix milliseconds)', + condition: { field: 'operation', value: ['get_trades', 'get_fills'] }, + }, + // Get Candlesticks fields + { + id: 'seriesTickerCandlesticks', + title: 'Series Ticker', + type: 'short-input', + placeholder: 'Series ticker', + required: true, + condition: { field: 'operation', value: ['get_candlesticks'] }, + }, + { + id: 'tickerCandlesticks', + title: 'Market Ticker', + type: 'short-input', + placeholder: 'Market ticker (e.g., KXBTC-24DEC31)', + required: true, + condition: { field: 'operation', value: ['get_candlesticks'] }, + }, + { + id: 'startTs', + title: 'Start Timestamp', + type: 'short-input', + placeholder: 'Start timestamp (Unix milliseconds)', + condition: { field: 'operation', value: ['get_candlesticks'] }, + }, + { + id: 'endTs', + title: 'End Timestamp', + type: 'short-input', + placeholder: 'End timestamp (Unix milliseconds)', + condition: { field: 'operation', value: ['get_candlesticks'] }, + }, + { + id: 'periodInterval', + title: 'Period Interval', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: '1 minute', id: '1' }, + { label: '1 hour', id: '60' }, + { label: '1 day', id: '1440' }, + ], + condition: { field: 'operation', value: ['get_candlesticks'] }, + }, + // Get Fills fields + { + id: 'tickerFills', + title: 'Market Ticker', + type: 'short-input', + placeholder: 'Filter by market ticker (optional)', + condition: { field: 'operation', value: ['get_fills'] }, + }, + { + id: 'orderId', + title: 'Order ID', + type: 'short-input', + placeholder: 'Filter by order ID (optional)', + condition: { field: 'operation', value: ['get_fills'] }, + }, + // Get Series fields + { + id: 'statusSeries', + title: 'Status', + type: 'short-input', + placeholder: 'Filter by status (optional)', + condition: { field: 'operation', value: ['get_series'] }, + }, + // Get Series by Ticker fields + { + id: 'seriesTickerGet', + title: 'Series Ticker', + type: 'short-input', + placeholder: 'Series ticker', + required: true, + condition: { field: 'operation', value: ['get_series_by_ticker'] }, + }, // Pagination fields { id: 'limit', @@ -143,7 +261,15 @@ export const KalshiBlock: BlockConfig = { placeholder: 'Number of results (1-1000, default: 100)', condition: { field: 'operation', - value: ['get_markets', 'get_events', 'get_positions', 'get_orders'], + value: [ + 'get_markets', + 'get_events', + 'get_positions', + 'get_orders', + 'get_trades', + 'get_fills', + 'get_series', + ], }, }, { @@ -153,7 +279,15 @@ export const KalshiBlock: BlockConfig = { placeholder: 'Pagination cursor', condition: { field: 'operation', - value: ['get_markets', 'get_events', 'get_positions', 'get_orders'], + value: [ + 'get_markets', + 'get_events', + 'get_positions', + 'get_orders', + 'get_trades', + 'get_fills', + 'get_series', + ], }, }, ], @@ -166,6 +300,13 @@ export const KalshiBlock: BlockConfig = { 'kalshi_get_balance', 'kalshi_get_positions', 'kalshi_get_orders', + 'kalshi_get_orderbook', + 'kalshi_get_trades', + 'kalshi_get_candlesticks', + 'kalshi_get_fills', + 'kalshi_get_series', + 'kalshi_get_series_by_ticker', + 'kalshi_get_exchange_status', ], config: { tool: (params) => { @@ -184,12 +325,37 @@ export const KalshiBlock: BlockConfig = { return 'kalshi_get_positions' case 'get_orders': return 'kalshi_get_orders' + case 'get_orderbook': + return 'kalshi_get_orderbook' + case 'get_trades': + return 'kalshi_get_trades' + case 'get_candlesticks': + return 'kalshi_get_candlesticks' + case 'get_fills': + return 'kalshi_get_fills' + case 'get_series': + return 'kalshi_get_series' + case 'get_series_by_ticker': + return 'kalshi_get_series_by_ticker' + case 'get_exchange_status': + return 'kalshi_get_exchange_status' default: return 'kalshi_get_markets' } }, params: (params) => { - const { operation, orderStatus, tickerFilter, ...rest } = params + const { + operation, + orderStatus, + tickerFilter, + tickerTrades, + tickerFills, + tickerCandlesticks, + seriesTickerCandlesticks, + seriesTickerGet, + statusSeries, + ...rest + } = params const cleanParams: Record = {} // Map orderStatus to status for get_orders @@ -202,6 +368,32 @@ export const KalshiBlock: BlockConfig = { cleanParams.ticker = tickerFilter } + // Map tickerTrades to ticker for get_trades + if (operation === 'get_trades' && tickerTrades) { + cleanParams.ticker = tickerTrades + } + + // Map tickerFills to ticker for get_fills + if (operation === 'get_fills' && tickerFills) { + cleanParams.ticker = tickerFills + } + + // Map fields for get_candlesticks + if (operation === 'get_candlesticks') { + if (seriesTickerCandlesticks) cleanParams.seriesTicker = seriesTickerCandlesticks + if (tickerCandlesticks) cleanParams.ticker = tickerCandlesticks + } + + // Map seriesTickerGet to seriesTicker for get_series_by_ticker + if (operation === 'get_series_by_ticker' && seriesTickerGet) { + cleanParams.seriesTicker = seriesTickerGet + } + + // Map statusSeries to status for get_series + if (operation === 'get_series' && statusSeries) { + cleanParams.status = statusSeries + } + Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { cleanParams[key] = value diff --git a/apps/sim/blocks/blocks/polymarket.ts b/apps/sim/blocks/blocks/polymarket.ts index 334331720ed..57baa363ccd 100644 --- a/apps/sim/blocks/blocks/polymarket.ts +++ b/apps/sim/blocks/blocks/polymarket.ts @@ -6,7 +6,7 @@ export const PolymarketBlock: BlockConfig = { name: 'Polymarket', description: 'Access prediction markets data from Polymarket', longDescription: - 'Integrate Polymarket prediction markets into the workflow. Can get markets, get market, get events, get event, get orderbook, get price, get midpoint, and get price history.', + 'Integrate Polymarket prediction markets into the workflow. Can get markets, market, events, event, tags, series, orderbook, price, midpoint, price history, last trade price, spread, tick size, positions, trades, and search.', docsLink: 'https://docs.sim.ai/tools/polymarket', category: 'tools', bgColor: '#4C82FB', @@ -21,10 +21,19 @@ export const PolymarketBlock: BlockConfig = { { label: 'Get Market', id: 'get_market' }, { label: 'Get Events', id: 'get_events' }, { label: 'Get Event', id: 'get_event' }, + { label: 'Get Tags', id: 'get_tags' }, + { label: 'Search', id: 'search' }, + { label: 'Get Series', id: 'get_series' }, + { label: 'Get Series by ID', id: 'get_series_by_id' }, { label: 'Get Orderbook', id: 'get_orderbook' }, { label: 'Get Price', id: 'get_price' }, { label: 'Get Midpoint', id: 'get_midpoint' }, { label: 'Get Price History', id: 'get_price_history' }, + { label: 'Get Last Trade Price', id: 'get_last_trade_price' }, + { label: 'Get Spread', id: 'get_spread' }, + { label: 'Get Tick Size', id: 'get_tick_size' }, + { label: 'Get Positions', id: 'get_positions' }, + { label: 'Get Trades', id: 'get_trades' }, ], value: () => 'get_markets', }, @@ -58,6 +67,48 @@ export const PolymarketBlock: BlockConfig = { placeholder: 'Event slug (required if no ID)', condition: { field: 'operation', value: ['get_event'] }, }, + // Series ID for get_series_by_id + { + id: 'seriesId', + title: 'Series ID', + type: 'short-input', + placeholder: 'Series ID', + required: true, + condition: { field: 'operation', value: ['get_series_by_id'] }, + }, + // Search query + { + id: 'query', + title: 'Search Query', + type: 'short-input', + placeholder: 'Search term', + required: true, + condition: { field: 'operation', value: ['search'] }, + }, + // User wallet address for Data API operations + { + id: 'user', + title: 'User Wallet Address', + type: 'short-input', + placeholder: 'Wallet address', + required: true, + condition: { field: 'operation', value: ['get_positions'] }, + }, + { + id: 'user', + title: 'User Wallet Address', + type: 'short-input', + placeholder: 'Wallet address (optional filter)', + condition: { field: 'operation', value: ['get_trades'] }, + }, + // Market filter for positions and trades + { + id: 'market', + title: 'Market ID', + type: 'short-input', + placeholder: 'Market ID (optional filter)', + condition: { field: 'operation', value: ['get_positions', 'get_trades'] }, + }, // Token ID for CLOB operations { id: 'tokenId', @@ -67,7 +118,15 @@ export const PolymarketBlock: BlockConfig = { required: true, condition: { field: 'operation', - value: ['get_orderbook', 'get_price', 'get_midpoint', 'get_price_history'], + value: [ + 'get_orderbook', + 'get_price', + 'get_midpoint', + 'get_price_history', + 'get_last_trade_price', + 'get_spread', + 'get_tick_size', + ], }, }, // Side for price query @@ -161,14 +220,20 @@ export const PolymarketBlock: BlockConfig = { title: 'Limit', type: 'short-input', placeholder: 'Number of results (recommended: 25-50)', - condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + condition: { + field: 'operation', + value: ['get_markets', 'get_events', 'get_tags', 'search', 'get_series', 'get_trades'], + }, }, { id: 'offset', title: 'Offset', type: 'short-input', placeholder: 'Pagination offset', - condition: { field: 'operation', value: ['get_markets', 'get_events'] }, + condition: { + field: 'operation', + value: ['get_markets', 'get_events', 'get_tags', 'search', 'get_series', 'get_trades'], + }, }, ], tools: { @@ -177,10 +242,19 @@ export const PolymarketBlock: BlockConfig = { 'polymarket_get_market', 'polymarket_get_events', 'polymarket_get_event', + 'polymarket_get_tags', + 'polymarket_search', + 'polymarket_get_series', + 'polymarket_get_series_by_id', 'polymarket_get_orderbook', 'polymarket_get_price', 'polymarket_get_midpoint', 'polymarket_get_price_history', + 'polymarket_get_last_trade_price', + 'polymarket_get_spread', + 'polymarket_get_tick_size', + 'polymarket_get_positions', + 'polymarket_get_trades', ], config: { tool: (params) => { @@ -193,6 +267,14 @@ export const PolymarketBlock: BlockConfig = { return 'polymarket_get_events' case 'get_event': return 'polymarket_get_event' + case 'get_tags': + return 'polymarket_get_tags' + case 'search': + return 'polymarket_search' + case 'get_series': + return 'polymarket_get_series' + case 'get_series_by_id': + return 'polymarket_get_series_by_id' case 'get_orderbook': return 'polymarket_get_orderbook' case 'get_price': @@ -201,6 +283,16 @@ export const PolymarketBlock: BlockConfig = { return 'polymarket_get_midpoint' case 'get_price_history': return 'polymarket_get_price_history' + case 'get_last_trade_price': + return 'polymarket_get_last_trade_price' + case 'get_spread': + return 'polymarket_get_spread' + case 'get_tick_size': + return 'polymarket_get_tick_size' + case 'get_positions': + return 'polymarket_get_positions' + case 'get_trades': + return 'polymarket_get_trades' default: return 'polymarket_get_markets' } @@ -245,6 +337,10 @@ export const PolymarketBlock: BlockConfig = { marketSlug: { type: 'string', description: 'Market slug' }, eventId: { type: 'string', description: 'Event ID' }, eventSlug: { type: 'string', description: 'Event slug' }, + seriesId: { type: 'string', description: 'Series ID' }, + query: { type: 'string', description: 'Search query' }, + user: { type: 'string', description: 'User wallet address' }, + market: { type: 'string', description: 'Market ID filter' }, tokenId: { type: 'string', description: 'CLOB Token ID' }, side: { type: 'string', description: 'Order side (buy/sell)' }, interval: { type: 'string', description: 'Price history interval' }, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index dc9f6bab2ec..573d1b44901 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3902,7 +3902,7 @@ export function WordpressIcon(props: SVGProps) { ) } -export function AhRefsIcon(props: SVGProps) { +export function AhrefsIcon(props: SVGProps) { return ( { } as any, mergedParams ) - }).toThrow('"Url" is required for Jina Reader') + }).toThrow('Url is required for Jina Reader') } ) @@ -314,7 +314,7 @@ describe('Validation Integration Tests', () => { } as any, mergedParams ) - }).toThrow('"Subreddit" is required for Reddit Posts') + }).toThrow('Subreddit is required for Reddit Posts') }) it.concurrent('complete success: all required fields provided correctly', () => { diff --git a/apps/sim/tools/ahrefs/referring_domains.ts b/apps/sim/tools/ahrefs/referring_domains.ts index 87dce46c4eb..fb1b6ff028d 100644 --- a/apps/sim/tools/ahrefs/referring_domains.ts +++ b/apps/sim/tools/ahrefs/referring_domains.ts @@ -56,7 +56,7 @@ export const referringDomainsTool: ToolConfig< request: { url: (params) => { - const url = new URL('https://api.ahrefs.com/v3/site-explorer/referring-domains') + const url = new URL('https://api.ahrefs.com/v3/site-explorer/refdomains') url.searchParams.set('target', params.target) // Date is required - default to today if not provided const date = params.date || new Date().toISOString().split('T')[0] diff --git a/apps/sim/tools/datadog/list_monitors.ts b/apps/sim/tools/datadog/list_monitors.ts index a9619273bbd..53cb4e0ed8a 100644 --- a/apps/sim/tools/datadog/list_monitors.ts +++ b/apps/sim/tools/datadog/list_monitors.ts @@ -84,7 +84,18 @@ export const listMonitorsTool: ToolConfig ({ @@ -106,8 +117,27 @@ export const listMonitorsTool: ToolConfig ({ + const text = await response.text() + let data: any + try { + data = JSON.parse(text) + } catch (e) { + return { + success: false, + output: { monitors: [] }, + error: `Failed to parse response: ${text.substring(0, 200)}`, + } + } + + if (!Array.isArray(data)) { + return { + success: false, + output: { monitors: [] }, + error: `Expected array but got: ${typeof data} - ${JSON.stringify(data).substring(0, 200)}`, + } + } + + const monitors = data.map((m: any) => ({ id: m.id, name: m.name, type: m.type, diff --git a/apps/sim/tools/kalshi/get_candlesticks.ts b/apps/sim/tools/kalshi/get_candlesticks.ts new file mode 100644 index 00000000000..206be1d24e2 --- /dev/null +++ b/apps/sim/tools/kalshi/get_candlesticks.ts @@ -0,0 +1,120 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiCandlestick } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetCandlesticksParams { + seriesTicker: string + ticker: string + startTs?: number + endTs?: number + periodInterval?: number // 1, 60, or 1440 (1min, 1hour, 1day) +} + +export interface KalshiGetCandlesticksResponse { + success: boolean + output: { + candlesticks: KalshiCandlestick[] + metadata: { + operation: 'get_candlesticks' + seriesTicker: string + ticker: string + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetCandlesticksTool: ToolConfig< + KalshiGetCandlesticksParams, + KalshiGetCandlesticksResponse +> = { + id: 'kalshi_get_candlesticks', + name: 'Get Market Candlesticks from Kalshi', + description: 'Retrieve OHLC candlestick data for a specific market', + version: '1.0.0', + + params: { + seriesTicker: { + type: 'string', + required: true, + description: 'Series ticker', + }, + ticker: { + type: 'string', + required: true, + description: 'Market ticker (e.g., KXBTC-24DEC31)', + }, + startTs: { + type: 'number', + required: false, + description: 'Start timestamp (Unix milliseconds)', + }, + endTs: { + type: 'number', + required: false, + description: 'End timestamp (Unix milliseconds)', + }, + periodInterval: { + type: 'number', + required: false, + description: 'Period interval: 1 (1min), 60 (1hour), or 1440 (1day)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.startTs !== undefined) queryParams.append('start_ts', params.startTs.toString()) + if (params.endTs !== undefined) queryParams.append('end_ts', params.endTs.toString()) + if (params.periodInterval !== undefined) + queryParams.append('period_interval', params.periodInterval.toString()) + + const query = queryParams.toString() + const url = buildKalshiUrl( + `/series/${params.seriesTicker}/markets/${params.ticker}/candlesticks` + ) + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_candlesticks') + } + + const candlesticks = data.candlesticks || [] + + return { + success: true, + output: { + candlesticks, + metadata: { + operation: 'get_candlesticks' as const, + seriesTicker: data.series_ticker || '', + ticker: data.ticker || '', + totalReturned: candlesticks.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Candlestick data and metadata', + properties: { + candlesticks: { type: 'array', description: 'Array of OHLC candlestick objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_exchange_status.ts b/apps/sim/tools/kalshi/get_exchange_status.ts new file mode 100644 index 00000000000..b8091cc7b05 --- /dev/null +++ b/apps/sim/tools/kalshi/get_exchange_status.ts @@ -0,0 +1,75 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiExchangeStatus } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export type KalshiGetExchangeStatusParams = Record + +export interface KalshiGetExchangeStatusResponse { + success: boolean + output: { + exchangeStatus: KalshiExchangeStatus + metadata: { + operation: 'get_exchange_status' + } + success: boolean + } +} + +export const kalshiGetExchangeStatusTool: ToolConfig< + KalshiGetExchangeStatusParams, + KalshiGetExchangeStatusResponse +> = { + id: 'kalshi_get_exchange_status', + name: 'Get Exchange Status from Kalshi', + description: 'Retrieve the current status of the Kalshi exchange (trading and exchange activity)', + version: '1.0.0', + + params: {}, + + request: { + url: () => { + return buildKalshiUrl('/exchange/status') + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_exchange_status') + } + + const exchangeStatus = { + trading_active: data.trading_active ?? false, + exchange_active: data.exchange_active ?? false, + } + + return { + success: true, + output: { + exchangeStatus, + metadata: { + operation: 'get_exchange_status' as const, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Exchange status data and metadata', + properties: { + exchangeStatus: { type: 'object', description: 'Exchange status object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_fills.ts b/apps/sim/tools/kalshi/get_fills.ts new file mode 100644 index 00000000000..defd87e5d8d --- /dev/null +++ b/apps/sim/tools/kalshi/get_fills.ts @@ -0,0 +1,138 @@ +import type { ToolConfig } from '@/tools/types' +import type { + KalshiAuthParams, + KalshiFill, + KalshiPaginationParams, + KalshiPagingInfo, +} from './types' +import { buildKalshiAuthHeaders, buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetFillsParams extends KalshiAuthParams, KalshiPaginationParams { + ticker?: string + orderId?: string + minTs?: number + maxTs?: number +} + +export interface KalshiGetFillsResponse { + success: boolean + output: { + fills: KalshiFill[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_fills' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetFillsTool: ToolConfig = { + id: 'kalshi_get_fills', + name: 'Get Fills from Kalshi', + description: "Retrieve your portfolio's fills/trades from Kalshi", + version: '1.0.0', + + params: { + keyId: { + type: 'string', + required: true, + description: 'Your Kalshi API Key ID', + }, + privateKey: { + type: 'string', + required: true, + description: 'Your RSA Private Key (PEM format)', + }, + ticker: { + type: 'string', + required: false, + description: 'Filter by market ticker', + }, + orderId: { + type: 'string', + required: false, + description: 'Filter by order ID', + }, + minTs: { + type: 'number', + required: false, + description: 'Minimum timestamp (Unix milliseconds)', + }, + maxTs: { + type: 'number', + required: false, + description: 'Maximum timestamp (Unix milliseconds)', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-1000, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.ticker) queryParams.append('ticker', params.ticker) + if (params.orderId) queryParams.append('order_id', params.orderId) + if (params.minTs !== undefined) queryParams.append('min_ts', params.minTs.toString()) + if (params.maxTs !== undefined) queryParams.append('max_ts', params.maxTs.toString()) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/portfolio/fills') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: (params) => { + const path = '/trade-api/v2/portfolio/fills' + return buildKalshiAuthHeaders(params.keyId, params.privateKey, 'GET', path) + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_fills') + } + + const fills = data.fills || [] + + return { + success: true, + output: { + fills, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_fills' as const, + totalReturned: fills.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Fills data and metadata', + properties: { + fills: { type: 'array', description: 'Array of fill/trade objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_orderbook.ts b/apps/sim/tools/kalshi/get_orderbook.ts new file mode 100644 index 00000000000..496f95f9bd3 --- /dev/null +++ b/apps/sim/tools/kalshi/get_orderbook.ts @@ -0,0 +1,93 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiOrderbook } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetOrderbookParams { + ticker: string + depth?: number +} + +export interface KalshiGetOrderbookResponse { + success: boolean + output: { + orderbook: KalshiOrderbook + metadata: { + operation: 'get_orderbook' + ticker: string + } + success: boolean + } +} + +export const kalshiGetOrderbookTool: ToolConfig< + KalshiGetOrderbookParams, + KalshiGetOrderbookResponse +> = { + id: 'kalshi_get_orderbook', + name: 'Get Market Orderbook from Kalshi', + description: 'Retrieve the orderbook (bids and asks) for a specific market', + version: '1.0.0', + + params: { + ticker: { + type: 'string', + required: true, + description: 'Market ticker (e.g., KXBTC-24DEC31)', + }, + depth: { + type: 'number', + required: false, + description: 'Number of price levels to return per side', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.depth !== undefined) queryParams.append('depth', params.depth.toString()) + + const query = queryParams.toString() + const url = buildKalshiUrl(`/markets/${params.ticker}/orderbook`) + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_orderbook') + } + + const orderbook = data.orderbook || { yes: [], no: [] } + + return { + success: true, + output: { + orderbook, + metadata: { + operation: 'get_orderbook' as const, + ticker: data.ticker || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Orderbook data and metadata', + properties: { + orderbook: { type: 'object', description: 'Orderbook with yes/no bids and asks' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_series.ts b/apps/sim/tools/kalshi/get_series.ts new file mode 100644 index 00000000000..5d9de9c2d41 --- /dev/null +++ b/apps/sim/tools/kalshi/get_series.ts @@ -0,0 +1,101 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiPaginationParams, KalshiPagingInfo, KalshiSeries } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetSeriesParams extends KalshiPaginationParams { + status?: string +} + +export interface KalshiGetSeriesResponse { + success: boolean + output: { + series: KalshiSeries[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_series' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetSeriesTool: ToolConfig = { + id: 'kalshi_get_series', + name: 'Get Series from Kalshi', + description: 'Retrieve a list of market series templates from Kalshi', + version: '1.0.0', + + params: { + status: { + type: 'string', + required: false, + description: 'Filter by status', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-1000, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.status) queryParams.append('status', params.status) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/series') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_series') + } + + const series = data.series || [] + + return { + success: true, + output: { + series, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_series' as const, + totalReturned: series.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Series data and metadata', + properties: { + series: { type: 'array', description: 'Array of series objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_series_by_ticker.ts b/apps/sim/tools/kalshi/get_series_by_ticker.ts new file mode 100644 index 00000000000..f6567046e9e --- /dev/null +++ b/apps/sim/tools/kalshi/get_series_by_ticker.ts @@ -0,0 +1,82 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiSeries } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetSeriesByTickerParams { + seriesTicker: string +} + +export interface KalshiGetSeriesByTickerResponse { + success: boolean + output: { + series: KalshiSeries + metadata: { + operation: 'get_series_by_ticker' + ticker: string + } + success: boolean + } +} + +export const kalshiGetSeriesByTickerTool: ToolConfig< + KalshiGetSeriesByTickerParams, + KalshiGetSeriesByTickerResponse +> = { + id: 'kalshi_get_series_by_ticker', + name: 'Get Series by Ticker from Kalshi', + description: 'Retrieve details of a specific market series by ticker', + version: '1.0.0', + + params: { + seriesTicker: { + type: 'string', + required: true, + description: 'Series ticker', + }, + }, + + request: { + url: (params) => { + return buildKalshiUrl(`/series/${params.seriesTicker}`) + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_series_by_ticker') + } + + const series = data.series || data + + return { + success: true, + output: { + series, + metadata: { + operation: 'get_series_by_ticker' as const, + ticker: series.ticker || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Series data and metadata', + properties: { + series: { type: 'object', description: 'Series object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/get_trades.ts b/apps/sim/tools/kalshi/get_trades.ts new file mode 100644 index 00000000000..89118ffb570 --- /dev/null +++ b/apps/sim/tools/kalshi/get_trades.ts @@ -0,0 +1,115 @@ +import type { ToolConfig } from '@/tools/types' +import type { KalshiPaginationParams, KalshiPagingInfo, KalshiTrade } from './types' +import { buildKalshiUrl, handleKalshiError } from './types' + +export interface KalshiGetTradesParams extends KalshiPaginationParams { + ticker?: string + minTs?: number + maxTs?: number +} + +export interface KalshiGetTradesResponse { + success: boolean + output: { + trades: KalshiTrade[] + paging?: KalshiPagingInfo + metadata: { + operation: 'get_trades' + totalReturned: number + } + success: boolean + } +} + +export const kalshiGetTradesTool: ToolConfig = { + id: 'kalshi_get_trades', + name: 'Get Trades from Kalshi', + description: 'Retrieve recent trades across all markets or for a specific market', + version: '1.0.0', + + params: { + ticker: { + type: 'string', + required: false, + description: 'Filter by market ticker', + }, + minTs: { + type: 'number', + required: false, + description: 'Minimum timestamp (Unix milliseconds)', + }, + maxTs: { + type: 'number', + required: false, + description: 'Maximum timestamp (Unix milliseconds)', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results (1-1000, default: 100)', + }, + cursor: { + type: 'string', + required: false, + description: 'Pagination cursor for next page', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.ticker) queryParams.append('ticker', params.ticker) + if (params.minTs !== undefined) queryParams.append('min_ts', params.minTs.toString()) + if (params.maxTs !== undefined) queryParams.append('max_ts', params.maxTs.toString()) + if (params.limit) queryParams.append('limit', params.limit) + if (params.cursor) queryParams.append('cursor', params.cursor) + + const query = queryParams.toString() + const url = buildKalshiUrl('/markets/trades') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handleKalshiError(data, response.status, 'get_trades') + } + + const trades = data.trades || [] + + return { + success: true, + output: { + trades, + paging: { + cursor: data.cursor || null, + }, + metadata: { + operation: 'get_trades' as const, + totalReturned: trades.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Trades data and metadata', + properties: { + trades: { type: 'array', description: 'Array of trade objects' }, + paging: { type: 'object', description: 'Pagination information' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/kalshi/index.ts b/apps/sim/tools/kalshi/index.ts index 64b371b47d5..d1cf2c83bb3 100644 --- a/apps/sim/tools/kalshi/index.ts +++ b/apps/sim/tools/kalshi/index.ts @@ -1,7 +1,14 @@ export { kalshiGetBalanceTool } from './get_balance' +export { kalshiGetCandlesticksTool } from './get_candlesticks' export { kalshiGetEventTool } from './get_event' export { kalshiGetEventsTool } from './get_events' +export { kalshiGetExchangeStatusTool } from './get_exchange_status' +export { kalshiGetFillsTool } from './get_fills' export { kalshiGetMarketTool } from './get_market' export { kalshiGetMarketsTool } from './get_markets' +export { kalshiGetOrderbookTool } from './get_orderbook' export { kalshiGetOrdersTool } from './get_orders' export { kalshiGetPositionsTool } from './get_positions' +export { kalshiGetSeriesTool } from './get_series' +export { kalshiGetSeriesByTickerTool } from './get_series_by_ticker' +export { kalshiGetTradesTool } from './get_trades' diff --git a/apps/sim/tools/kalshi/types.ts b/apps/sim/tools/kalshi/types.ts index c591d7bc49a..a2c18a980e1 100644 --- a/apps/sim/tools/kalshi/types.ts +++ b/apps/sim/tools/kalshi/types.ts @@ -120,6 +120,67 @@ export interface KalshiOrder { taker_fees?: number } +// Orderbook type +export interface KalshiOrderbookLevel { + price: number + quantity: number +} + +export interface KalshiOrderbook { + yes: KalshiOrderbookLevel[] + no: KalshiOrderbookLevel[] +} + +// Trade type +export interface KalshiTrade { + ticker: string + yes_price: number + no_price: number + count: number + created_time: string + taker_side: string +} + +// Candlestick type +export interface KalshiCandlestick { + open_time: string + close_time: string + open: number + high: number + low: number + close: number + volume: number +} + +// Fill type +export interface KalshiFill { + created_time: string + ticker: string + is_taker: boolean + side: string + yes_price: number + no_price: number + count: number + order_id: string + trade_id: string +} + +// Series type +export interface KalshiSeries { + ticker: string + title: string + frequency: string + category: string + tags?: string[] + status?: string +} + +// Exchange status type +export interface KalshiExchangeStatus { + trading_active: boolean + exchange_active: boolean +} + // Helper function to build Kalshi API URLs export function buildKalshiUrl(path: string): string { return `${KALSHI_BASE_URL}${path}` diff --git a/apps/sim/tools/polymarket/get_last_trade_price.ts b/apps/sim/tools/polymarket/get_last_trade_price.ts new file mode 100644 index 00000000000..451d9a03ef3 --- /dev/null +++ b/apps/sim/tools/polymarket/get_last_trade_price.ts @@ -0,0 +1,81 @@ +import type { ToolConfig } from '@/tools/types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetLastTradePriceParams { + tokenId: string // The token ID (CLOB token ID from market) +} + +export interface PolymarketGetLastTradePriceResponse { + success: boolean + output: { + price: string + metadata: { + operation: 'get_last_trade_price' + tokenId: string + } + success: boolean + } +} + +export const polymarketGetLastTradePriceTool: ToolConfig< + PolymarketGetLastTradePriceParams, + PolymarketGetLastTradePriceResponse +> = { + id: 'polymarket_get_last_trade_price', + name: 'Get Last Trade Price from Polymarket', + description: 'Retrieve the last trade price for a specific token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + return `${buildClobUrl('/last-trade-price')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_last_trade_price') + } + + return { + success: true, + output: { + price: typeof data === 'string' ? data : data.price || '', + metadata: { + operation: 'get_last_trade_price' as const, + tokenId: params?.tokenId || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Last trade price and metadata', + properties: { + price: { type: 'string', description: 'Last trade price' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_positions.ts b/apps/sim/tools/polymarket/get_positions.ts new file mode 100644 index 00000000000..1e4c17fcfb6 --- /dev/null +++ b/apps/sim/tools/polymarket/get_positions.ts @@ -0,0 +1,95 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPosition } from './types' +import { buildDataUrl, handlePolymarketError } from './types' + +export interface PolymarketGetPositionsParams { + user: string // Wallet address (required) + market?: string // Optional market filter +} + +export interface PolymarketGetPositionsResponse { + success: boolean + output: { + positions: PolymarketPosition[] + metadata: { + operation: 'get_positions' + user: string + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetPositionsTool: ToolConfig< + PolymarketGetPositionsParams, + PolymarketGetPositionsResponse +> = { + id: 'polymarket_get_positions', + name: 'Get Positions from Polymarket', + description: 'Retrieve user positions from Polymarket', + version: '1.0.0', + + params: { + user: { + type: 'string', + required: true, + description: 'User wallet address', + }, + market: { + type: 'string', + required: false, + description: 'Optional market ID to filter positions', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('user', params.user) + if (params.market) queryParams.append('market', params.market) + + return `${buildDataUrl('/positions')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_positions') + } + + // Response is an array of positions + const positions = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + positions, + metadata: { + operation: 'get_positions' as const, + user: params?.user || '', + totalReturned: positions.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Positions data and metadata', + properties: { + positions: { type: 'array', description: 'Array of position objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_series.ts b/apps/sim/tools/polymarket/get_series.ts new file mode 100644 index 00000000000..f1c9d61f9af --- /dev/null +++ b/apps/sim/tools/polymarket/get_series.ts @@ -0,0 +1,92 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPaginationParams, PolymarketSeries } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetSeriesParams extends PolymarketPaginationParams {} + +export interface PolymarketGetSeriesResponse { + success: boolean + output: { + series: PolymarketSeries[] + metadata: { + operation: 'get_series' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetSeriesTool: ToolConfig< + PolymarketGetSeriesParams, + PolymarketGetSeriesResponse +> = { + id: 'polymarket_get_series', + name: 'Get Series from Polymarket', + description: 'Retrieve series (related market groups) from Polymarket', + version: '1.0.0', + + params: { + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + const query = queryParams.toString() + const url = buildGammaUrl('/series') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_series') + } + + // Response is an array of series + const series = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + series, + metadata: { + operation: 'get_series' as const, + totalReturned: series.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Series data and metadata', + properties: { + series: { type: 'array', description: 'Array of series objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_series_by_id.ts b/apps/sim/tools/polymarket/get_series_by_id.ts new file mode 100644 index 00000000000..8a925856368 --- /dev/null +++ b/apps/sim/tools/polymarket/get_series_by_id.ts @@ -0,0 +1,78 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketSeries } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetSeriesByIdParams { + seriesId: string // Series ID (required) +} + +export interface PolymarketGetSeriesByIdResponse { + success: boolean + output: { + series: PolymarketSeries + metadata: { + operation: 'get_series_by_id' + seriesId: string + } + success: boolean + } +} + +export const polymarketGetSeriesByIdTool: ToolConfig< + PolymarketGetSeriesByIdParams, + PolymarketGetSeriesByIdResponse +> = { + id: 'polymarket_get_series_by_id', + name: 'Get Series by ID from Polymarket', + description: 'Retrieve a specific series (related market group) by ID from Polymarket', + version: '1.0.0', + + params: { + seriesId: { + type: 'string', + required: true, + description: 'The series ID', + }, + }, + + request: { + url: (params) => buildGammaUrl(`/series/${params.seriesId}`), + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_series_by_id') + } + + return { + success: true, + output: { + series: data, + metadata: { + operation: 'get_series_by_id' as const, + seriesId: params?.seriesId || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Series data and metadata', + properties: { + series: { type: 'object', description: 'Series object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_spread.ts b/apps/sim/tools/polymarket/get_spread.ts new file mode 100644 index 00000000000..940925584ba --- /dev/null +++ b/apps/sim/tools/polymarket/get_spread.ts @@ -0,0 +1,82 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketSpread } from './types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetSpreadParams { + tokenId: string // The token ID (CLOB token ID from market) +} + +export interface PolymarketGetSpreadResponse { + success: boolean + output: { + spread: PolymarketSpread + metadata: { + operation: 'get_spread' + tokenId: string + } + success: boolean + } +} + +export const polymarketGetSpreadTool: ToolConfig< + PolymarketGetSpreadParams, + PolymarketGetSpreadResponse +> = { + id: 'polymarket_get_spread', + name: 'Get Spread from Polymarket', + description: 'Retrieve the bid-ask spread for a specific token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + return `${buildClobUrl('/spread')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_spread') + } + + return { + success: true, + output: { + spread: data, + metadata: { + operation: 'get_spread' as const, + tokenId: params?.tokenId || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Spread data and metadata', + properties: { + spread: { type: 'object', description: 'Bid-ask spread object' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_tags.ts b/apps/sim/tools/polymarket/get_tags.ts new file mode 100644 index 00000000000..93032e4b221 --- /dev/null +++ b/apps/sim/tools/polymarket/get_tags.ts @@ -0,0 +1,90 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPaginationParams, PolymarketTag } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketGetTagsParams extends PolymarketPaginationParams {} + +export interface PolymarketGetTagsResponse { + success: boolean + output: { + tags: PolymarketTag[] + metadata: { + operation: 'get_tags' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetTagsTool: ToolConfig = + { + id: 'polymarket_get_tags', + name: 'Get Tags from Polymarket', + description: 'Retrieve available tags for filtering markets from Polymarket', + version: '1.0.0', + + params: { + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + const query = queryParams.toString() + const url = buildGammaUrl('/tags') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_tags') + } + + // Response is an array of tags + const tags = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + tags, + metadata: { + operation: 'get_tags' as const, + totalReturned: tags.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Tags data and metadata', + properties: { + tags: { type: 'array', description: 'Array of tag objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, + } diff --git a/apps/sim/tools/polymarket/get_tick_size.ts b/apps/sim/tools/polymarket/get_tick_size.ts new file mode 100644 index 00000000000..79d8f9a1983 --- /dev/null +++ b/apps/sim/tools/polymarket/get_tick_size.ts @@ -0,0 +1,81 @@ +import type { ToolConfig } from '@/tools/types' +import { buildClobUrl, handlePolymarketError } from './types' + +export interface PolymarketGetTickSizeParams { + tokenId: string // The token ID (CLOB token ID from market) +} + +export interface PolymarketGetTickSizeResponse { + success: boolean + output: { + tickSize: string + metadata: { + operation: 'get_tick_size' + tokenId: string + } + success: boolean + } +} + +export const polymarketGetTickSizeTool: ToolConfig< + PolymarketGetTickSizeParams, + PolymarketGetTickSizeResponse +> = { + id: 'polymarket_get_tick_size', + name: 'Get Tick Size from Polymarket', + description: 'Retrieve the minimum tick size for a specific token', + version: '1.0.0', + + params: { + tokenId: { + type: 'string', + required: true, + description: 'The CLOB token ID (from market clobTokenIds)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('token_id', params.tokenId) + return `${buildClobUrl('/tick-size')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_tick_size') + } + + return { + success: true, + output: { + tickSize: typeof data === 'string' ? data : data.tick_size || data.tickSize || '', + metadata: { + operation: 'get_tick_size' as const, + tokenId: params?.tokenId || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Tick size and metadata', + properties: { + tickSize: { type: 'string', description: 'Minimum tick size' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/get_trades.ts b/apps/sim/tools/polymarket/get_trades.ts new file mode 100644 index 00000000000..3eb371b04cc --- /dev/null +++ b/apps/sim/tools/polymarket/get_trades.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPaginationParams, PolymarketTrade } from './types' +import { buildDataUrl, handlePolymarketError } from './types' + +export interface PolymarketGetTradesParams extends PolymarketPaginationParams { + user?: string // Optional user wallet address + market?: string // Optional market filter +} + +export interface PolymarketGetTradesResponse { + success: boolean + output: { + trades: PolymarketTrade[] + metadata: { + operation: 'get_trades' + totalReturned: number + } + success: boolean + } +} + +export const polymarketGetTradesTool: ToolConfig< + PolymarketGetTradesParams, + PolymarketGetTradesResponse +> = { + id: 'polymarket_get_trades', + name: 'Get Trades from Polymarket', + description: 'Retrieve trade history from Polymarket', + version: '1.0.0', + + params: { + user: { + type: 'string', + required: false, + description: 'User wallet address to filter trades', + }, + market: { + type: 'string', + required: false, + description: 'Market ID to filter trades', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + if (params.user) queryParams.append('user', params.user) + if (params.market) queryParams.append('market', params.market) + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + const query = queryParams.toString() + const url = buildDataUrl('/trades') + return query ? `${url}?${query}` : url + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'get_trades') + } + + // Response is an array of trades + const trades = Array.isArray(data) ? data : [] + + return { + success: true, + output: { + trades, + metadata: { + operation: 'get_trades' as const, + totalReturned: trades.length, + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Trades data and metadata', + properties: { + trades: { type: 'array', description: 'Array of trade objects' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/index.ts b/apps/sim/tools/polymarket/index.ts index a0263a89d71..a52e9ac3788 100644 --- a/apps/sim/tools/polymarket/index.ts +++ b/apps/sim/tools/polymarket/index.ts @@ -1,9 +1,18 @@ export * from './get_event' export * from './get_events' +export * from './get_last_trade_price' export * from './get_market' export * from './get_markets' export * from './get_midpoint' export * from './get_orderbook' +export * from './get_positions' export * from './get_price' export * from './get_price_history' +export * from './get_series' +export * from './get_series_by_id' +export * from './get_spread' +export * from './get_tags' +export * from './get_tick_size' +export * from './get_trades' +export * from './search' export * from './types' diff --git a/apps/sim/tools/polymarket/search.ts b/apps/sim/tools/polymarket/search.ts new file mode 100644 index 00000000000..99c32a604c9 --- /dev/null +++ b/apps/sim/tools/polymarket/search.ts @@ -0,0 +1,99 @@ +import type { ToolConfig } from '@/tools/types' +import type { PolymarketPaginationParams, PolymarketSearchResult } from './types' +import { buildGammaUrl, handlePolymarketError } from './types' + +export interface PolymarketSearchParams extends PolymarketPaginationParams { + query: string // Search term (required) +} + +export interface PolymarketSearchResponse { + success: boolean + output: { + results: PolymarketSearchResult + metadata: { + operation: 'search' + query: string + } + success: boolean + } +} + +export const polymarketSearchTool: ToolConfig = { + id: 'polymarket_search', + name: 'Search Polymarket', + description: 'Search for markets, events, and profiles on Polymarket', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + description: 'Search query term', + }, + limit: { + type: 'string', + required: false, + description: 'Number of results per page (recommended: 25-50)', + }, + offset: { + type: 'string', + required: false, + description: 'Pagination offset (skip this many results)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('query', params.query) + if (params.limit) queryParams.append('limit', params.limit) + if (params.offset) queryParams.append('offset', params.offset) + + return `${buildGammaUrl('/public-search')}?${queryParams.toString()}` + }, + method: 'GET', + headers: () => ({ + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + handlePolymarketError(data, response.status, 'search') + } + + // Response contains markets, events, and profiles arrays + const results: PolymarketSearchResult = { + markets: data.markets || [], + events: data.events || [], + profiles: data.profiles || [], + } + + return { + success: true, + output: { + results, + metadata: { + operation: 'search' as const, + query: params?.query || '', + }, + success: true, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Operation success status' }, + output: { + type: 'object', + description: 'Search results and metadata', + properties: { + results: { type: 'object', description: 'Search results (markets, events, profiles)' }, + metadata: { type: 'object', description: 'Operation metadata' }, + success: { type: 'boolean', description: 'Operation success' }, + }, + }, + }, +} diff --git a/apps/sim/tools/polymarket/types.ts b/apps/sim/tools/polymarket/types.ts index dc7ed4b0563..5d16e29583a 100644 --- a/apps/sim/tools/polymarket/types.ts +++ b/apps/sim/tools/polymarket/types.ts @@ -3,6 +3,7 @@ // Base URLs for different Polymarket APIs export const POLYMARKET_GAMMA_URL = 'https://gamma-api.polymarket.com' export const POLYMARKET_CLOB_URL = 'https://clob.polymarket.com' +export const POLYMARKET_DATA_URL = 'https://data-api.polymarket.com' // Helper to build Gamma API URL export function buildGammaUrl(path: string): string { @@ -14,6 +15,11 @@ export function buildClobUrl(path: string): string { return `${POLYMARKET_CLOB_URL}${path}` } +// Helper to build Data API URL +export function buildDataUrl(path: string): string { + return `${POLYMARKET_DATA_URL}${path}` +} + // Common pagination parameters export interface PolymarketPaginationParams { limit?: string @@ -127,6 +133,50 @@ export interface PolymarketPriceHistoryEntry { p: number // price } +// Series structure +export interface PolymarketSeries { + id: string + title: string + slug: string + description: string + image: string + markets: PolymarketMarket[] +} + +// Search result structure +export interface PolymarketSearchResult { + markets: PolymarketMarket[] + events: PolymarketEvent[] + profiles: any[] // Profile structure not fully documented +} + +// Spread structure +export interface PolymarketSpread { + bid: string + ask: string +} + +// Position structure +export interface PolymarketPosition { + market: string + asset_id: string + size: string + value: string +} + +// Trade structure +export interface PolymarketTrade { + id: string + market: string + asset_id: string + side: string + size: string + price: string + timestamp: string + maker: string + taker: string +} + // Error handler for Polymarket API responses export function handlePolymarketError(data: any, status: number, operation: string): never { const errorMessage = data?.message || data?.error || `Unknown error during ${operation}` diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 38579e71b19..019fbf460af 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -444,12 +444,19 @@ import { } from '@/tools/jira' import { kalshiGetBalanceTool, + kalshiGetCandlesticksTool, kalshiGetEventsTool, kalshiGetEventTool, + kalshiGetExchangeStatusTool, + kalshiGetFillsTool, kalshiGetMarketsTool, kalshiGetMarketTool, + kalshiGetOrderbookTool, kalshiGetOrdersTool, kalshiGetPositionsTool, + kalshiGetSeriesByTickerTool, + kalshiGetSeriesTool, + kalshiGetTradesTool, } from '@/tools/kalshi' import { knowledgeCreateDocumentTool, @@ -746,12 +753,21 @@ import { import { polymarketGetEventsTool, polymarketGetEventTool, + polymarketGetLastTradePriceTool, polymarketGetMarketsTool, polymarketGetMarketTool, polymarketGetMidpointTool, polymarketGetOrderbookTool, + polymarketGetPositionsTool, polymarketGetPriceHistoryTool, polymarketGetPriceTool, + polymarketGetSeriesByIdTool, + polymarketGetSeriesTool, + polymarketGetSpreadTool, + polymarketGetTagsTool, + polymarketGetTickSizeTool, + polymarketGetTradesTool, + polymarketSearchTool, } from '@/tools/polymarket' import { deleteTool as postgresDeleteTool, @@ -1352,6 +1368,13 @@ export const tools: Record = { kalshi_get_balance: kalshiGetBalanceTool, kalshi_get_positions: kalshiGetPositionsTool, kalshi_get_orders: kalshiGetOrdersTool, + kalshi_get_orderbook: kalshiGetOrderbookTool, + kalshi_get_trades: kalshiGetTradesTool, + kalshi_get_candlesticks: kalshiGetCandlesticksTool, + kalshi_get_fills: kalshiGetFillsTool, + kalshi_get_series: kalshiGetSeriesTool, + kalshi_get_series_by_ticker: kalshiGetSeriesByTickerTool, + kalshi_get_exchange_status: kalshiGetExchangeStatusTool, polymarket_get_markets: polymarketGetMarketsTool, polymarket_get_market: polymarketGetMarketTool, polymarket_get_events: polymarketGetEventsTool, @@ -1360,6 +1383,15 @@ export const tools: Record = { polymarket_get_price: polymarketGetPriceTool, polymarket_get_midpoint: polymarketGetMidpointTool, polymarket_get_price_history: polymarketGetPriceHistoryTool, + polymarket_get_tags: polymarketGetTagsTool, + polymarket_search: polymarketSearchTool, + polymarket_get_series: polymarketGetSeriesTool, + polymarket_get_series_by_id: polymarketGetSeriesByIdTool, + polymarket_get_last_trade_price: polymarketGetLastTradePriceTool, + polymarket_get_spread: polymarketGetSpreadTool, + polymarket_get_tick_size: polymarketGetTickSizeTool, + polymarket_get_positions: polymarketGetPositionsTool, + polymarket_get_trades: polymarketGetTradesTool, slack_message: slackMessageTool, slack_message_reader: slackMessageReaderTool, slack_canvas: slackCanvasTool, diff --git a/apps/sim/tools/utils.test.ts b/apps/sim/tools/utils.test.ts index 89b6ef25c22..cd7f259a556 100644 --- a/apps/sim/tools/utils.test.ts +++ b/apps/sim/tools/utils.test.ts @@ -232,7 +232,7 @@ describe('validateRequiredParametersAfterMerge', () => { required1: 'value', // required2 is missing }) - }).toThrow('"Required2" is required for Test Tool') + }).toThrow('Required2 is required for Test Tool') }) it.concurrent('should not throw error when all required parameters are provided', () => { @@ -260,7 +260,7 @@ describe('validateRequiredParametersAfterMerge', () => { required1: null, required2: '', }) - }).toThrow('"Required1" is required for Test Tool') + }).toThrow('Required1 is required for Test Tool') }) it.concurrent( @@ -317,7 +317,7 @@ describe('validateRequiredParametersAfterMerge', () => { // userOrLlmParam missing - should cause error // userOnlyParam missing - should NOT cause error (validated earlier) }) - }).toThrow('"User Or Llm Param" is required for') + }).toThrow('User Or Llm Param is required for') }) it.concurrent('should use parameter description in error messages when available', () => { @@ -335,7 +335,7 @@ describe('validateRequiredParametersAfterMerge', () => { expect(() => { validateRequiredParametersAfterMerge('test-tool', toolWithDescriptions, {}) - }).toThrow('"Subreddit" is required for Test Tool') + }).toThrow('Subreddit is required for Test Tool') }) it.concurrent('should fall back to parameter name when no description available', () => { @@ -353,7 +353,7 @@ describe('validateRequiredParametersAfterMerge', () => { expect(() => { validateRequiredParametersAfterMerge('test-tool', toolWithoutDescription, {}) - }).toThrow('"Subreddit" is required for Test Tool') + }).toThrow('Subreddit is required for Test Tool') }) it.concurrent('should handle undefined values as missing', () => { @@ -362,7 +362,7 @@ describe('validateRequiredParametersAfterMerge', () => { required1: 'value', required2: undefined, // Explicitly undefined }) - }).toThrow('"Required2" is required for Test Tool') + }).toThrow('Required2 is required for Test Tool') }) it.concurrent('should validate all missing parameters at once', () => { @@ -387,7 +387,7 @@ describe('validateRequiredParametersAfterMerge', () => { // Should throw for the first missing parameter it encounters expect(() => { validateRequiredParametersAfterMerge('test-tool', toolWithMultipleRequired, {}) - }).toThrow('"Param1" is required for Test Tool') + }).toThrow('Param1 is required for Test Tool') }) }) diff --git a/apps/sim/tools/utils.ts b/apps/sim/tools/utils.ts index 8cf07a5301b..e24be3778a8 100644 --- a/apps/sim/tools/utils.ts +++ b/apps/sim/tools/utils.ts @@ -172,7 +172,7 @@ export function validateRequiredParametersAfterMerge( const toolName = tool.name || toolId const friendlyParamName = parameterNameMap?.[paramName] || formatParameterNameForError(paramName) - throw new Error(`"${friendlyParamName}" is required for ${toolName}`) + throw new Error(`${friendlyParamName} is required for ${toolName}`) } } } From 6cd6ac7bf4b67b7d7627b6521a9466532d3c7d46 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 15:09:18 -0800 Subject: [PATCH 04/11] split up grafana --- apps/docs/content/docs/en/tools/grafana.mdx | 556 ++++++------- apps/sim/lib/uploads/utils/validation.ts | 8 +- apps/sim/tools/dropbox/index.ts | 33 +- apps/sim/tools/grafana/alerts.ts | 768 ------------------ apps/sim/tools/grafana/annotations.ts | 491 ----------- apps/sim/tools/grafana/create_alert_rule.ts | 194 +++++ apps/sim/tools/grafana/create_annotation.ts | 138 ++++ apps/sim/tools/grafana/create_dashboard.ts | 182 +++++ apps/sim/tools/grafana/create_folder.ts | 112 +++ apps/sim/tools/grafana/dashboards.ts | 663 --------------- .../{health.ts => data_source_health.ts} | 73 -- apps/sim/tools/grafana/delete_alert_rule.ts | 74 ++ apps/sim/tools/grafana/delete_annotation.ts | 75 ++ apps/sim/tools/grafana/delete_dashboard.ts | 86 ++ apps/sim/tools/grafana/folders.ts | 225 ----- apps/sim/tools/grafana/get_alert_rule.ts | 123 +++ apps/sim/tools/grafana/get_dashboard.ts | 76 ++ .../{data_sources.ts => get_data_source.ts} | 95 --- apps/sim/tools/grafana/health_check.ts | 73 ++ apps/sim/tools/grafana/index.ts | 45 +- apps/sim/tools/grafana/list_alert_rules.ts | 101 +++ apps/sim/tools/grafana/list_annotations.ts | 161 ++++ apps/sim/tools/grafana/list_contact_points.ts | 87 ++ apps/sim/tools/grafana/list_dashboards.ts | 143 ++++ apps/sim/tools/grafana/list_data_sources.ts | 98 +++ apps/sim/tools/grafana/list_folders.ts | 110 +++ apps/sim/tools/grafana/update_alert_rule.ts | 201 +++++ apps/sim/tools/grafana/update_annotation.ts | 126 +++ apps/sim/tools/grafana/update_dashboard.ts | 185 +++++ 29 files changed, 2637 insertions(+), 2665 deletions(-) delete mode 100644 apps/sim/tools/grafana/alerts.ts delete mode 100644 apps/sim/tools/grafana/annotations.ts create mode 100644 apps/sim/tools/grafana/create_alert_rule.ts create mode 100644 apps/sim/tools/grafana/create_annotation.ts create mode 100644 apps/sim/tools/grafana/create_dashboard.ts create mode 100644 apps/sim/tools/grafana/create_folder.ts delete mode 100644 apps/sim/tools/grafana/dashboards.ts rename apps/sim/tools/grafana/{health.ts => data_source_health.ts} (53%) create mode 100644 apps/sim/tools/grafana/delete_alert_rule.ts create mode 100644 apps/sim/tools/grafana/delete_annotation.ts create mode 100644 apps/sim/tools/grafana/delete_dashboard.ts delete mode 100644 apps/sim/tools/grafana/folders.ts create mode 100644 apps/sim/tools/grafana/get_alert_rule.ts create mode 100644 apps/sim/tools/grafana/get_dashboard.ts rename apps/sim/tools/grafana/{data_sources.ts => get_data_source.ts} (53%) create mode 100644 apps/sim/tools/grafana/health_check.ts create mode 100644 apps/sim/tools/grafana/list_alert_rules.ts create mode 100644 apps/sim/tools/grafana/list_annotations.ts create mode 100644 apps/sim/tools/grafana/list_contact_points.ts create mode 100644 apps/sim/tools/grafana/list_dashboards.ts create mode 100644 apps/sim/tools/grafana/list_data_sources.ts create mode 100644 apps/sim/tools/grafana/list_folders.ts create mode 100644 apps/sim/tools/grafana/update_alert_rule.ts create mode 100644 apps/sim/tools/grafana/update_annotation.ts create mode 100644 apps/sim/tools/grafana/update_dashboard.ts diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index 2707db758fb..c54de82e663 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -40,570 +40,496 @@ Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data s ### `grafana_health_check` +Check the health status of a Grafana instance + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `commit` | string | Git commit hash of the Grafana build | +| `database` | string | Database connection status \(ok or failing\) | +| `version` | string | Grafana version number | ### `grafana_data_source_health` +Test the health of a specific data source connection. Note: This endpoint is deprecated since Grafana v9.0.0 and may not work on newer versions. + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `dataSourceId` | string | Yes | The ID or UID of the data source to check | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `status` | string | Health status \(OK or ERROR\) | +| `message` | string | Status message or error description | ### `grafana_get_dashboard` +Get a dashboard by its UID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `dashboardUid` | string | Yes | The UID of the dashboard to retrieve | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `dashboard` | json | The full dashboard JSON object | +| `meta` | json | Dashboard metadata \(version, permissions, etc.\) | ### `grafana_list_dashboards` +Search and list all dashboards + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `query` | string | No | Search query to filter dashboards by title | +| `tag` | string | No | Filter by tag \(comma-separated for multiple tags\) | +| `folderIds` | string | No | Filter by folder IDs \(comma-separated\) | +| `starred` | boolean | No | Only return starred dashboards | +| `limit` | number | No | Maximum number of dashboards to return | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `dashboards` | array | List of dashboard search results | ### `grafana_create_dashboard` +Create a new dashboard + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `title` | string | Yes | The title of the new dashboard | +| `folderUid` | string | No | The UID of the folder to create the dashboard in | +| `tags` | string | No | Comma-separated list of tags | +| `timezone` | string | No | Dashboard timezone \(e.g., browser, utc\) | +| `refresh` | string | No | Auto-refresh interval \(e.g., 5s, 1m, 5m\) | +| `panels` | string | No | JSON array of panel configurations | +| `overwrite` | boolean | No | Overwrite existing dashboard with same title | +| `message` | string | No | Commit message for the dashboard version | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | The numeric ID of the created dashboard | +| `uid` | string | The UID of the created dashboard | +| `url` | string | The URL path to the dashboard | +| `status` | string | Status of the operation \(success\) | +| `version` | number | The version number of the dashboard | +| `slug` | string | URL-friendly slug of the dashboard | ### `grafana_update_dashboard` +Update an existing dashboard + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `dashboardUid` | string | Yes | The UID of the dashboard to update | +| `title` | string | No | New title for the dashboard | +| `folderUid` | string | No | New folder UID to move the dashboard to | +| `tags` | string | No | Comma-separated list of new tags | +| `timezone` | string | No | Dashboard timezone \(e.g., browser, utc\) | +| `refresh` | string | No | Auto-refresh interval \(e.g., 5s, 1m, 5m\) | +| `panels` | string | No | JSON array of panel configurations | +| `overwrite` | boolean | No | Overwrite even if there is a version conflict | +| `message` | string | No | Commit message for this version | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | The numeric ID of the updated dashboard | +| `uid` | string | The UID of the updated dashboard | +| `url` | string | The URL path to the dashboard | +| `status` | string | Status of the operation \(success\) | +| `version` | number | The new version number of the dashboard | +| `slug` | string | URL-friendly slug of the dashboard | ### `grafana_delete_dashboard` +Delete a dashboard by its UID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `dashboardUid` | string | Yes | The UID of the dashboard to delete | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `title` | string | The title of the deleted dashboard | +| `message` | string | Confirmation message | +| `id` | number | The ID of the deleted dashboard | ### `grafana_list_alert_rules` +List all alert rules in the Grafana instance + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `rules` | array | List of alert rules | ### `grafana_get_alert_rule` +Get a specific alert rule by its UID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `alertRuleUid` | string | Yes | The UID of the alert rule to retrieve | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `uid` | string | Alert rule UID | +| `title` | string | Alert rule title | +| `condition` | string | Alert condition | +| `data` | json | Alert rule query data | +| `folderUID` | string | Parent folder UID | +| `ruleGroup` | string | Rule group name | +| `noDataState` | string | State when no data is returned | +| `execErrState` | string | State on execution error | +| `annotations` | json | Alert annotations | +| `labels` | json | Alert labels | ### `grafana_create_alert_rule` +Create a new alert rule + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `title` | string | Yes | The title of the alert rule | +| `folderUid` | string | Yes | The UID of the folder to create the alert in | +| `ruleGroup` | string | Yes | The name of the rule group | +| `condition` | string | Yes | The refId of the query or expression to use as the alert condition | +| `data` | string | Yes | JSON array of query/expression data objects | +| `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) | +| `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) | +| `execErrState` | string | No | State on execution error \(Alerting, OK\) | +| `annotations` | string | No | JSON object of annotations | +| `labels` | string | No | JSON object of labels | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `uid` | string | The UID of the created alert rule | +| `title` | string | Alert rule title | +| `folderUID` | string | Parent folder UID | +| `ruleGroup` | string | Rule group name | ### `grafana_update_alert_rule` +Update an existing alert rule + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `alertRuleUid` | string | Yes | The UID of the alert rule to update | +| `title` | string | No | New title for the alert rule | +| `folderUid` | string | No | New folder UID to move the alert to | +| `ruleGroup` | string | No | New rule group name | +| `condition` | string | No | New condition refId | +| `data` | string | No | New JSON array of query/expression data objects | +| `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) | +| `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) | +| `execErrState` | string | No | State on execution error \(Alerting, OK\) | +| `annotations` | string | No | JSON object of annotations | +| `labels` | string | No | JSON object of labels | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `uid` | string | The UID of the updated alert rule | +| `title` | string | Alert rule title | +| `folderUID` | string | Parent folder UID | +| `ruleGroup` | string | Rule group name | ### `grafana_delete_alert_rule` +Delete an alert rule by its UID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `alertRuleUid` | string | Yes | The UID of the alert rule to delete | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `message` | string | Confirmation message | ### `grafana_list_contact_points` +List all alert notification contact points + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `contactPoints` | array | List of contact points | ### `grafana_create_annotation` +Create an annotation on a dashboard or as a global annotation + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `text` | string | Yes | The text content of the annotation | +| `tags` | string | No | Comma-separated list of tags | +| `dashboardUid` | string | No | UID of the dashboard to add the annotation to \(optional for global annotations\) | +| `panelId` | number | No | ID of the panel to add the annotation to | +| `time` | number | No | Start time in epoch milliseconds \(defaults to now\) | +| `timeEnd` | number | No | End time in epoch milliseconds \(for range annotations\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | The ID of the created annotation | +| `message` | string | Confirmation message | ### `grafana_list_annotations` +Query annotations by time range, dashboard, or tags + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `from` | number | No | Start time in epoch milliseconds | +| `to` | number | No | End time in epoch milliseconds | +| `dashboardUid` | string | No | Filter by dashboard UID | +| `panelId` | number | No | Filter by panel ID | +| `tags` | string | No | Comma-separated list of tags to filter by | +| `type` | string | No | Filter by type \(alert or annotation\) | +| `limit` | number | No | Maximum number of annotations to return | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `annotations` | array | List of annotations | ### `grafana_update_annotation` +Update an existing annotation + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `annotationId` | number | Yes | The ID of the annotation to update | +| `text` | string | Yes | New text content for the annotation | +| `tags` | string | No | Comma-separated list of new tags | +| `time` | number | No | New start time in epoch milliseconds | +| `timeEnd` | number | No | New end time in epoch milliseconds | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | The ID of the updated annotation | +| `message` | string | Confirmation message | ### `grafana_delete_annotation` +Delete an annotation by its ID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `annotationId` | number | Yes | The ID of the annotation to delete | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `message` | string | Confirmation message | ### `grafana_list_data_sources` +List all data sources configured in Grafana + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `dataSources` | array | List of data sources | ### `grafana_get_data_source` +Get a data source by its ID or UID + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `dataSourceId` | string | Yes | The ID or UID of the data source to retrieve | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | Data source ID | +| `uid` | string | Data source UID | +| `name` | string | Data source name | +| `type` | string | Data source type | +| `url` | string | Data source connection URL | +| `database` | string | Database name \(if applicable\) | +| `isDefault` | boolean | Whether this is the default data source | +| `jsonData` | json | Additional data source configuration | ### `grafana_list_folders` +List all folders in Grafana + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `limit` | number | No | Maximum number of folders to return | +| `page` | number | No | Page number for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `folders` | array | List of folders | ### `grafana_create_folder` +Create a new folder in Grafana + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances | +| `title` | string | Yes | The title of the new folder | +| `uid` | string | No | Optional UID for the folder \(auto-generated if not provided\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `id` | number | The numeric ID of the created folder | +| `uid` | string | The UID of the created folder | +| `title` | string | The title of the created folder | +| `url` | string | The URL path to the folder | diff --git a/apps/sim/lib/uploads/utils/validation.ts b/apps/sim/lib/uploads/utils/validation.ts index b3f87c99d6e..46125f9b404 100644 --- a/apps/sim/lib/uploads/utils/validation.ts +++ b/apps/sim/lib/uploads/utils/validation.ts @@ -50,7 +50,13 @@ export const SUPPORTED_MIME_TYPES: Record 'application/octet-stream', ], txt: ['text/plain', 'text/x-plain', 'application/txt'], - md: ['text/markdown', 'text/x-markdown', 'text/plain', 'application/markdown'], + md: [ + 'text/markdown', + 'text/x-markdown', + 'text/plain', + 'application/markdown', + 'application/octet-stream', + ], xlsx: [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/octet-stream', diff --git a/apps/sim/tools/dropbox/index.ts b/apps/sim/tools/dropbox/index.ts index 617232fecf4..6fea8749700 100644 --- a/apps/sim/tools/dropbox/index.ts +++ b/apps/sim/tools/dropbox/index.ts @@ -1,10 +1,23 @@ -export { dropboxCopyTool } from '@/tools/dropbox/copy' -export { dropboxCreateFolderTool } from '@/tools/dropbox/create_folder' -export { dropboxCreateSharedLinkTool } from '@/tools/dropbox/create_shared_link' -export { dropboxDeleteTool } from '@/tools/dropbox/delete' -export { dropboxDownloadTool } from '@/tools/dropbox/download' -export { dropboxGetMetadataTool } from '@/tools/dropbox/get_metadata' -export { dropboxListFolderTool } from '@/tools/dropbox/list_folder' -export { dropboxMoveTool } from '@/tools/dropbox/move' -export { dropboxSearchTool } from '@/tools/dropbox/search' -export { dropboxUploadTool } from '@/tools/dropbox/upload' +import { dropboxCopyTool } from '@/tools/dropbox/copy' +import { dropboxCreateFolderTool } from '@/tools/dropbox/create_folder' +import { dropboxCreateSharedLinkTool } from '@/tools/dropbox/create_shared_link' +import { dropboxDeleteTool } from '@/tools/dropbox/delete' +import { dropboxDownloadTool } from '@/tools/dropbox/download' +import { dropboxGetMetadataTool } from '@/tools/dropbox/get_metadata' +import { dropboxListFolderTool } from '@/tools/dropbox/list_folder' +import { dropboxMoveTool } from '@/tools/dropbox/move' +import { dropboxSearchTool } from '@/tools/dropbox/search' +import { dropboxUploadTool } from '@/tools/dropbox/upload' + +export { + dropboxCopyTool, + dropboxCreateFolderTool, + dropboxCreateSharedLinkTool, + dropboxDeleteTool, + dropboxDownloadTool, + dropboxGetMetadataTool, + dropboxListFolderTool, + dropboxMoveTool, + dropboxSearchTool, + dropboxUploadTool, +} diff --git a/apps/sim/tools/grafana/alerts.ts b/apps/sim/tools/grafana/alerts.ts deleted file mode 100644 index eff8579b365..00000000000 --- a/apps/sim/tools/grafana/alerts.ts +++ /dev/null @@ -1,768 +0,0 @@ -import type { - GrafanaCreateAlertRuleParams, - GrafanaCreateAlertRuleResponse, - GrafanaDeleteAlertRuleParams, - GrafanaDeleteAlertRuleResponse, - GrafanaGetAlertRuleParams, - GrafanaGetAlertRuleResponse, - GrafanaListAlertRulesParams, - GrafanaListAlertRulesResponse, - GrafanaListContactPointsParams, - GrafanaListContactPointsResponse, - GrafanaUpdateAlertRuleParams, - GrafanaUpdateAlertRuleResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const listAlertRulesTool: ToolConfig< - GrafanaListAlertRulesParams, - GrafanaListAlertRulesResponse -> = { - id: 'grafana_list_alert_rules', - name: 'Grafana List Alert Rules', - description: 'List all alert rules in the Grafana instance', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - rules: Array.isArray(data) - ? data.map((rule: any) => ({ - uid: rule.uid, - title: rule.title, - condition: rule.condition, - data: rule.data, - updated: rule.updated, - noDataState: rule.noDataState, - execErrState: rule.execErrState, - for: rule.for, - annotations: rule.annotations || {}, - labels: rule.labels || {}, - isPaused: rule.isPaused || false, - folderUID: rule.folderUID, - ruleGroup: rule.ruleGroup, - orgId: rule.orgId, - namespace_uid: rule.namespace_uid, - namespace_id: rule.namespace_id, - provenance: rule.provenance || '', - })) - : [], - }, - } - }, - - outputs: { - rules: { - type: 'array', - description: 'List of alert rules', - items: { - type: 'object', - properties: { - uid: { type: 'string', description: 'Alert rule UID' }, - title: { type: 'string', description: 'Alert rule title' }, - condition: { type: 'string', description: 'Alert condition' }, - folderUID: { type: 'string', description: 'Parent folder UID' }, - ruleGroup: { type: 'string', description: 'Rule group name' }, - noDataState: { type: 'string', description: 'State when no data is returned' }, - execErrState: { type: 'string', description: 'State on execution error' }, - }, - }, - }, - }, -} - -export const getAlertRuleTool: ToolConfig = - { - id: 'grafana_get_alert_rule', - name: 'Grafana Get Alert Rule', - description: 'Get a specific alert rule by its UID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - alertRuleUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the alert rule to retrieve', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - uid: data.uid, - title: data.title, - condition: data.condition, - data: data.data, - updated: data.updated, - noDataState: data.noDataState, - execErrState: data.execErrState, - for: data.for, - annotations: data.annotations || {}, - labels: data.labels || {}, - isPaused: data.isPaused || false, - folderUID: data.folderUID, - ruleGroup: data.ruleGroup, - orgId: data.orgId, - namespace_uid: data.namespace_uid, - namespace_id: data.namespace_id, - provenance: data.provenance || '', - }, - } - }, - - outputs: { - uid: { - type: 'string', - description: 'Alert rule UID', - }, - title: { - type: 'string', - description: 'Alert rule title', - }, - condition: { - type: 'string', - description: 'Alert condition', - }, - data: { - type: 'json', - description: 'Alert rule query data', - }, - folderUID: { - type: 'string', - description: 'Parent folder UID', - }, - ruleGroup: { - type: 'string', - description: 'Rule group name', - }, - noDataState: { - type: 'string', - description: 'State when no data is returned', - }, - execErrState: { - type: 'string', - description: 'State on execution error', - }, - annotations: { - type: 'json', - description: 'Alert annotations', - }, - labels: { - type: 'json', - description: 'Alert labels', - }, - }, - } - -export const createAlertRuleTool: ToolConfig< - GrafanaCreateAlertRuleParams, - GrafanaCreateAlertRuleResponse -> = { - id: 'grafana_create_alert_rule', - name: 'Grafana Create Alert Rule', - description: 'Create a new alert rule', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - title: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The title of the alert rule', - }, - folderUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the folder to create the alert in', - }, - ruleGroup: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the rule group', - }, - condition: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The refId of the query or expression to use as the alert condition', - }, - data: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'JSON array of query/expression data objects', - }, - forDuration: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Duration to wait before firing (e.g., 5m, 1h)', - }, - noDataState: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'State when no data is returned (NoData, Alerting, OK)', - }, - execErrState: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'State on execution error (Alerting, OK)', - }, - annotations: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON object of annotations', - }, - labels: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON object of labels', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, - method: 'POST', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - let dataArray: any[] = [] - try { - dataArray = JSON.parse(params.data) - } catch { - throw new Error('Invalid JSON for data parameter') - } - - const body: Record = { - title: params.title, - folderUID: params.folderUid, - ruleGroup: params.ruleGroup, - condition: params.condition, - data: dataArray, - for: params.forDuration || '5m', - noDataState: params.noDataState || 'NoData', - execErrState: params.execErrState || 'Alerting', - } - - if (params.annotations) { - try { - body.annotations = JSON.parse(params.annotations) - } catch { - body.annotations = {} - } - } - - if (params.labels) { - try { - body.labels = JSON.parse(params.labels) - } catch { - body.labels = {} - } - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - uid: data.uid, - title: data.title, - condition: data.condition, - data: data.data, - updated: data.updated, - noDataState: data.noDataState, - execErrState: data.execErrState, - for: data.for, - annotations: data.annotations || {}, - labels: data.labels || {}, - isPaused: data.isPaused || false, - folderUID: data.folderUID, - ruleGroup: data.ruleGroup, - orgId: data.orgId, - namespace_uid: data.namespace_uid, - namespace_id: data.namespace_id, - provenance: data.provenance || '', - }, - } - }, - - outputs: { - uid: { - type: 'string', - description: 'The UID of the created alert rule', - }, - title: { - type: 'string', - description: 'Alert rule title', - }, - folderUID: { - type: 'string', - description: 'Parent folder UID', - }, - ruleGroup: { - type: 'string', - description: 'Rule group name', - }, - }, -} - -export const updateAlertRuleTool: ToolConfig< - GrafanaUpdateAlertRuleParams, - GrafanaUpdateAlertRuleResponse -> = { - id: 'grafana_update_alert_rule', - name: 'Grafana Update Alert Rule', - description: 'Update an existing alert rule', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - alertRuleUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the alert rule to update', - }, - title: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New title for the alert rule', - }, - folderUid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New folder UID to move the alert to', - }, - ruleGroup: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New rule group name', - }, - condition: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New condition refId', - }, - data: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New JSON array of query/expression data objects', - }, - forDuration: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Duration to wait before firing (e.g., 5m, 1h)', - }, - noDataState: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'State when no data is returned (NoData, Alerting, OK)', - }, - execErrState: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'State on execution error (Alerting, OK)', - }, - annotations: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON object of annotations', - }, - labels: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON object of labels', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, - method: 'PUT', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const body: Record = {} - - if (params.title) body.title = params.title - if (params.folderUid) body.folderUID = params.folderUid - if (params.ruleGroup) body.ruleGroup = params.ruleGroup - if (params.condition) body.condition = params.condition - if (params.forDuration) body.for = params.forDuration - if (params.noDataState) body.noDataState = params.noDataState - if (params.execErrState) body.execErrState = params.execErrState - - if (params.data) { - try { - body.data = JSON.parse(params.data) - } catch { - // Skip if invalid JSON - } - } - - if (params.annotations) { - try { - body.annotations = JSON.parse(params.annotations) - } catch { - // Skip if invalid JSON - } - } - - if (params.labels) { - try { - body.labels = JSON.parse(params.labels) - } catch { - // Skip if invalid JSON - } - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - uid: data.uid, - title: data.title, - condition: data.condition, - data: data.data, - updated: data.updated, - noDataState: data.noDataState, - execErrState: data.execErrState, - for: data.for, - annotations: data.annotations || {}, - labels: data.labels || {}, - isPaused: data.isPaused || false, - folderUID: data.folderUID, - ruleGroup: data.ruleGroup, - orgId: data.orgId, - namespace_uid: data.namespace_uid, - namespace_id: data.namespace_id, - provenance: data.provenance || '', - }, - } - }, - - outputs: { - uid: { - type: 'string', - description: 'The UID of the updated alert rule', - }, - title: { - type: 'string', - description: 'Alert rule title', - }, - folderUID: { - type: 'string', - description: 'Parent folder UID', - }, - ruleGroup: { - type: 'string', - description: 'Rule group name', - }, - }, -} - -export const deleteAlertRuleTool: ToolConfig< - GrafanaDeleteAlertRuleParams, - GrafanaDeleteAlertRuleResponse -> = { - id: 'grafana_delete_alert_rule', - name: 'Grafana Delete Alert Rule', - description: 'Delete an alert rule by its UID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - alertRuleUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the alert rule to delete', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, - method: 'DELETE', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async () => { - return { - success: true, - output: { - message: 'Alert rule deleted successfully', - }, - } - }, - - outputs: { - message: { - type: 'string', - description: 'Confirmation message', - }, - }, -} - -export const listContactPointsTool: ToolConfig< - GrafanaListContactPointsParams, - GrafanaListContactPointsResponse -> = { - id: 'grafana_list_contact_points', - name: 'Grafana List Contact Points', - description: 'List all alert notification contact points', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - contactPoints: Array.isArray(data) - ? data.map((cp: any) => ({ - uid: cp.uid, - name: cp.name, - type: cp.type, - settings: cp.settings || {}, - disableResolveMessage: cp.disableResolveMessage || false, - provenance: cp.provenance || '', - })) - : [], - }, - } - }, - - outputs: { - contactPoints: { - type: 'array', - description: 'List of contact points', - items: { - type: 'object', - properties: { - uid: { type: 'string', description: 'Contact point UID' }, - name: { type: 'string', description: 'Contact point name' }, - type: { type: 'string', description: 'Notification type (email, slack, etc.)' }, - settings: { type: 'object', description: 'Type-specific settings' }, - }, - }, - }, - }, -} diff --git a/apps/sim/tools/grafana/annotations.ts b/apps/sim/tools/grafana/annotations.ts deleted file mode 100644 index 3c7688107c6..00000000000 --- a/apps/sim/tools/grafana/annotations.ts +++ /dev/null @@ -1,491 +0,0 @@ -import type { - GrafanaCreateAnnotationParams, - GrafanaCreateAnnotationResponse, - GrafanaDeleteAnnotationParams, - GrafanaDeleteAnnotationResponse, - GrafanaListAnnotationsParams, - GrafanaListAnnotationsResponse, - GrafanaUpdateAnnotationParams, - GrafanaUpdateAnnotationResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const createAnnotationTool: ToolConfig< - GrafanaCreateAnnotationParams, - GrafanaCreateAnnotationResponse -> = { - id: 'grafana_create_annotation', - name: 'Grafana Create Annotation', - description: 'Create an annotation on a dashboard or as a global annotation', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - text: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The text content of the annotation', - }, - tags: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of tags', - }, - dashboardUid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'UID of the dashboard to add the annotation to (optional for global annotations)', - }, - panelId: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'ID of the panel to add the annotation to', - }, - time: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Start time in epoch milliseconds (defaults to now)', - }, - timeEnd: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'End time in epoch milliseconds (for range annotations)', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations`, - method: 'POST', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const body: Record = { - text: params.text, - time: params.time || Date.now(), - } - - if (params.tags) { - body.tags = params.tags - .split(',') - .map((t) => t.trim()) - .filter((t) => t) - } - - if (params.dashboardUid) { - body.dashboardUID = params.dashboardUid - } - - if (params.panelId) { - body.panelId = params.panelId - } - - if (params.timeEnd) { - body.timeEnd = params.timeEnd - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - id: data.id, - message: data.message || 'Annotation created successfully', - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'The ID of the created annotation', - }, - message: { - type: 'string', - description: 'Confirmation message', - }, - }, -} - -export const listAnnotationsTool: ToolConfig< - GrafanaListAnnotationsParams, - GrafanaListAnnotationsResponse -> = { - id: 'grafana_list_annotations', - name: 'Grafana List Annotations', - description: 'Query annotations by time range, dashboard, or tags', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - from: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Start time in epoch milliseconds', - }, - to: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'End time in epoch milliseconds', - }, - dashboardUid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filter by dashboard UID', - }, - panelId: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'Filter by panel ID', - }, - tags: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of tags to filter by', - }, - type: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Filter by type (alert or annotation)', - }, - limit: { - type: 'number', - required: false, - visibility: 'user-only', - description: 'Maximum number of annotations to return', - }, - }, - - request: { - url: (params) => { - const baseUrl = params.baseUrl.replace(/\/$/, '') - const searchParams = new URLSearchParams() - - if (params.from) searchParams.set('from', String(params.from)) - if (params.to) searchParams.set('to', String(params.to)) - if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid) - if (params.panelId) searchParams.set('panelId', String(params.panelId)) - if (params.tags) { - params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim())) - } - if (params.type) searchParams.set('type', params.type) - if (params.limit) searchParams.set('limit', String(params.limit)) - - const queryString = searchParams.toString() - return `${baseUrl}/api/annotations${queryString ? `?${queryString}` : ''}` - }, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - annotations: Array.isArray(data) - ? data.map((a: any) => ({ - id: a.id, - alertId: a.alertId, - alertName: a.alertName, - dashboardId: a.dashboardId, - dashboardUID: a.dashboardUID, - panelId: a.panelId, - userId: a.userId, - newState: a.newState, - prevState: a.prevState, - created: a.created, - updated: a.updated, - time: a.time, - timeEnd: a.timeEnd, - text: a.text, - tags: a.tags || [], - login: a.login, - email: a.email, - avatarUrl: a.avatarUrl, - data: a.data, - })) - : [], - }, - } - }, - - outputs: { - annotations: { - type: 'array', - description: 'List of annotations', - items: { - type: 'object', - properties: { - id: { type: 'number', description: 'Annotation ID' }, - text: { type: 'string', description: 'Annotation text' }, - tags: { type: 'array', description: 'Annotation tags' }, - time: { type: 'number', description: 'Start time in epoch ms' }, - timeEnd: { type: 'number', description: 'End time in epoch ms' }, - dashboardUID: { type: 'string', description: 'Dashboard UID' }, - panelId: { type: 'number', description: 'Panel ID' }, - }, - }, - }, - }, -} - -export const updateAnnotationTool: ToolConfig< - GrafanaUpdateAnnotationParams, - GrafanaUpdateAnnotationResponse -> = { - id: 'grafana_update_annotation', - name: 'Grafana Update Annotation', - description: 'Update an existing annotation', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - annotationId: { - type: 'number', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the annotation to update', - }, - text: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'New text content for the annotation', - }, - tags: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of new tags', - }, - time: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'New start time in epoch milliseconds', - }, - timeEnd: { - type: 'number', - required: false, - visibility: 'user-or-llm', - description: 'New end time in epoch milliseconds', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, - method: 'PATCH', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const body: Record = { - text: params.text, - } - - if (params.tags) { - body.tags = params.tags - .split(',') - .map((t) => t.trim()) - .filter((t) => t) - } - - if (params.time) { - body.time = params.time - } - - if (params.timeEnd) { - body.timeEnd = params.timeEnd - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - id: data.id || 0, - message: data.message || 'Annotation updated successfully', - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'The ID of the updated annotation', - }, - message: { - type: 'string', - description: 'Confirmation message', - }, - }, -} - -export const deleteAnnotationTool: ToolConfig< - GrafanaDeleteAnnotationParams, - GrafanaDeleteAnnotationResponse -> = { - id: 'grafana_delete_annotation', - name: 'Grafana Delete Annotation', - description: 'Delete an annotation by its ID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - annotationId: { - type: 'number', - required: true, - visibility: 'user-or-llm', - description: 'The ID of the annotation to delete', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, - method: 'DELETE', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - message: data.message || 'Annotation deleted successfully', - }, - } - }, - - outputs: { - message: { - type: 'string', - description: 'Confirmation message', - }, - }, -} diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts new file mode 100644 index 00000000000..24029f0939b --- /dev/null +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -0,0 +1,194 @@ +import type { + GrafanaCreateAlertRuleParams, + GrafanaCreateAlertRuleResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createAlertRuleTool: ToolConfig< + GrafanaCreateAlertRuleParams, + GrafanaCreateAlertRuleResponse +> = { + id: 'grafana_create_alert_rule', + name: 'Grafana Create Alert Rule', + description: 'Create a new alert rule', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the alert rule', + }, + folderUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the folder to create the alert in', + }, + ruleGroup: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the rule group', + }, + condition: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The refId of the query or expression to use as the alert condition', + }, + data: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'JSON array of query/expression data objects', + }, + forDuration: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Duration to wait before firing (e.g., 5m, 1h)', + }, + noDataState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State when no data is returned (NoData, Alerting, OK)', + }, + execErrState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State on execution error (Alerting, OK)', + }, + annotations: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of annotations', + }, + labels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of labels', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + let dataArray: any[] = [] + try { + dataArray = JSON.parse(params.data) + } catch { + throw new Error('Invalid JSON for data parameter') + } + + const body: Record = { + title: params.title, + folderUID: params.folderUid, + ruleGroup: params.ruleGroup, + condition: params.condition, + data: dataArray, + for: params.forDuration || '5m', + noDataState: params.noDataState || 'NoData', + execErrState: params.execErrState || 'Alerting', + } + + if (params.annotations) { + try { + body.annotations = JSON.parse(params.annotations) + } catch { + body.annotations = {} + } + } + + if (params.labels) { + try { + body.labels = JSON.parse(params.labels) + } catch { + body.labels = {} + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'The UID of the created alert rule', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + }, +} diff --git a/apps/sim/tools/grafana/create_annotation.ts b/apps/sim/tools/grafana/create_annotation.ts new file mode 100644 index 00000000000..e0e846c5d31 --- /dev/null +++ b/apps/sim/tools/grafana/create_annotation.ts @@ -0,0 +1,138 @@ +import type { + GrafanaCreateAnnotationParams, + GrafanaCreateAnnotationResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createAnnotationTool: ToolConfig< + GrafanaCreateAnnotationParams, + GrafanaCreateAnnotationResponse +> = { + id: 'grafana_create_annotation', + name: 'Grafana Create Annotation', + description: 'Create an annotation on a dashboard or as a global annotation', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text content of the annotation', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags', + }, + dashboardUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'UID of the dashboard to add the annotation to (optional for global annotations)', + }, + panelId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'ID of the panel to add the annotation to', + }, + time: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time in epoch milliseconds (defaults to now)', + }, + timeEnd: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time in epoch milliseconds (for range annotations)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + text: params.text, + time: params.time || Date.now(), + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + + if (params.dashboardUid) { + body.dashboardUID = params.dashboardUid + } + + if (params.panelId) { + body.panelId = params.panelId + } + + if (params.timeEnd) { + body.timeEnd = params.timeEnd + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + message: data.message || 'Annotation created successfully', + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The ID of the created annotation', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/grafana/create_dashboard.ts b/apps/sim/tools/grafana/create_dashboard.ts new file mode 100644 index 00000000000..8286ca9ac38 --- /dev/null +++ b/apps/sim/tools/grafana/create_dashboard.ts @@ -0,0 +1,182 @@ +import type { + GrafanaCreateDashboardParams, + GrafanaCreateDashboardResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createDashboardTool: ToolConfig< + GrafanaCreateDashboardParams, + GrafanaCreateDashboardResponse +> = { + id: 'grafana_create_dashboard', + name: 'Grafana Create Dashboard', + description: 'Create a new dashboard', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new dashboard', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The UID of the folder to create the dashboard in', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Dashboard timezone (e.g., browser, utc)', + }, + refresh: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', + }, + panels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of panel configurations', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Overwrite existing dashboard with same title', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Commit message for the dashboard version', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const dashboard: Record = { + title: params.title, + tags: params.tags + ? params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + : [], + timezone: params.timezone || 'browser', + schemaVersion: 39, + version: 0, + refresh: params.refresh || '', + } + + if (params.panels) { + try { + dashboard.panels = JSON.parse(params.panels) + } catch { + dashboard.panels = [] + } + } else { + dashboard.panels = [] + } + + const body: Record = { + dashboard, + overwrite: params.overwrite || false, + } + + if (params.folderUid) { + body.folderUid = params.folderUid + } + + if (params.message) { + body.message = params.message + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + url: data.url, + status: data.status, + version: data.version, + slug: data.slug, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the created dashboard', + }, + uid: { + type: 'string', + description: 'The UID of the created dashboard', + }, + url: { + type: 'string', + description: 'The URL path to the dashboard', + }, + status: { + type: 'string', + description: 'Status of the operation (success)', + }, + version: { + type: 'number', + description: 'The version number of the dashboard', + }, + slug: { + type: 'string', + description: 'URL-friendly slug of the dashboard', + }, + }, +} diff --git a/apps/sim/tools/grafana/create_folder.ts b/apps/sim/tools/grafana/create_folder.ts new file mode 100644 index 00000000000..c4a289c7644 --- /dev/null +++ b/apps/sim/tools/grafana/create_folder.ts @@ -0,0 +1,112 @@ +import type { GrafanaCreateFolderParams, GrafanaCreateFolderResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createFolderTool: ToolConfig = + { + id: 'grafana_create_folder', + name: 'Grafana Create Folder', + description: 'Create a new folder in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new folder', + }, + uid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional UID for the folder (auto-generated if not provided)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + title: params.title, + } + + if (params.uid) { + body.uid = params.uid + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + title: data.title, + url: data.url, + hasAcl: data.hasAcl || false, + canSave: data.canSave || false, + canEdit: data.canEdit || false, + canAdmin: data.canAdmin || false, + canDelete: data.canDelete || false, + createdBy: data.createdBy || '', + created: data.created || '', + updatedBy: data.updatedBy || '', + updated: data.updated || '', + version: data.version || 0, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the created folder', + }, + uid: { + type: 'string', + description: 'The UID of the created folder', + }, + title: { + type: 'string', + description: 'The title of the created folder', + }, + url: { + type: 'string', + description: 'The URL path to the folder', + }, + }, + } diff --git a/apps/sim/tools/grafana/dashboards.ts b/apps/sim/tools/grafana/dashboards.ts deleted file mode 100644 index dcb98cf8614..00000000000 --- a/apps/sim/tools/grafana/dashboards.ts +++ /dev/null @@ -1,663 +0,0 @@ -import type { - GrafanaCreateDashboardParams, - GrafanaCreateDashboardResponse, - GrafanaDeleteDashboardParams, - GrafanaDeleteDashboardResponse, - GrafanaGetDashboardParams, - GrafanaGetDashboardResponse, - GrafanaListDashboardsParams, - GrafanaListDashboardsResponse, - GrafanaUpdateDashboardParams, - GrafanaUpdateDashboardResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const getDashboardTool: ToolConfig = - { - id: 'grafana_get_dashboard', - name: 'Grafana Get Dashboard', - description: 'Get a dashboard by its UID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - dashboardUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the dashboard to retrieve', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - dashboard: data.dashboard, - meta: data.meta, - }, - } - }, - - outputs: { - dashboard: { - type: 'json', - description: 'The full dashboard JSON object', - }, - meta: { - type: 'json', - description: 'Dashboard metadata (version, permissions, etc.)', - }, - }, - } - -export const listDashboardsTool: ToolConfig< - GrafanaListDashboardsParams, - GrafanaListDashboardsResponse -> = { - id: 'grafana_list_dashboards', - name: 'Grafana List Dashboards', - description: 'Search and list all dashboards', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - query: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Search query to filter dashboards by title', - }, - tag: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Filter by tag (comma-separated for multiple tags)', - }, - folderIds: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Filter by folder IDs (comma-separated)', - }, - starred: { - type: 'boolean', - required: false, - visibility: 'user-only', - description: 'Only return starred dashboards', - }, - limit: { - type: 'number', - required: false, - visibility: 'user-only', - description: 'Maximum number of dashboards to return', - }, - }, - - request: { - url: (params) => { - const baseUrl = params.baseUrl.replace(/\/$/, '') - const searchParams = new URLSearchParams() - searchParams.set('type', 'dash-db') - - if (params.query) searchParams.set('query', params.query) - if (params.tag) { - params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim())) - } - if (params.folderIds) { - params.folderIds.split(',').forEach((id) => searchParams.append('folderIds', id.trim())) - } - if (params.starred) searchParams.set('starred', 'true') - if (params.limit) searchParams.set('limit', String(params.limit)) - - return `${baseUrl}/api/search?${searchParams.toString()}` - }, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - dashboards: Array.isArray(data) - ? data.map((d: any) => ({ - id: d.id, - uid: d.uid, - title: d.title, - uri: d.uri, - url: d.url, - slug: d.slug, - type: d.type, - tags: d.tags || [], - isStarred: d.isStarred || false, - folderId: d.folderId, - folderUid: d.folderUid, - folderTitle: d.folderTitle, - folderUrl: d.folderUrl, - sortMeta: d.sortMeta, - })) - : [], - }, - } - }, - - outputs: { - dashboards: { - type: 'array', - description: 'List of dashboard search results', - items: { - type: 'object', - properties: { - id: { type: 'number', description: 'Dashboard ID' }, - uid: { type: 'string', description: 'Dashboard UID' }, - title: { type: 'string', description: 'Dashboard title' }, - url: { type: 'string', description: 'Dashboard URL path' }, - tags: { type: 'array', description: 'Dashboard tags' }, - folderTitle: { type: 'string', description: 'Parent folder title' }, - }, - }, - }, - }, -} - -export const createDashboardTool: ToolConfig< - GrafanaCreateDashboardParams, - GrafanaCreateDashboardResponse -> = { - id: 'grafana_create_dashboard', - name: 'Grafana Create Dashboard', - description: 'Create a new dashboard', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - title: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The title of the new dashboard', - }, - folderUid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'The UID of the folder to create the dashboard in', - }, - tags: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of tags', - }, - timezone: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Dashboard timezone (e.g., browser, utc)', - }, - refresh: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', - }, - panels: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON array of panel configurations', - }, - overwrite: { - type: 'boolean', - required: false, - visibility: 'user-only', - description: 'Overwrite existing dashboard with same title', - }, - message: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Commit message for the dashboard version', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, - method: 'POST', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const dashboard: Record = { - title: params.title, - tags: params.tags - ? params.tags - .split(',') - .map((t) => t.trim()) - .filter((t) => t) - : [], - timezone: params.timezone || 'browser', - schemaVersion: 39, - version: 0, - refresh: params.refresh || '', - } - - if (params.panels) { - try { - dashboard.panels = JSON.parse(params.panels) - } catch { - dashboard.panels = [] - } - } else { - dashboard.panels = [] - } - - const body: Record = { - dashboard, - overwrite: params.overwrite || false, - } - - if (params.folderUid) { - body.folderUid = params.folderUid - } - - if (params.message) { - body.message = params.message - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - id: data.id, - uid: data.uid, - url: data.url, - status: data.status, - version: data.version, - slug: data.slug, - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'The numeric ID of the created dashboard', - }, - uid: { - type: 'string', - description: 'The UID of the created dashboard', - }, - url: { - type: 'string', - description: 'The URL path to the dashboard', - }, - status: { - type: 'string', - description: 'Status of the operation (success)', - }, - version: { - type: 'number', - description: 'The version number of the dashboard', - }, - slug: { - type: 'string', - description: 'URL-friendly slug of the dashboard', - }, - }, -} - -export const updateDashboardTool: ToolConfig< - GrafanaUpdateDashboardParams, - GrafanaUpdateDashboardResponse -> = { - id: 'grafana_update_dashboard', - name: 'Grafana Update Dashboard', - description: 'Update an existing dashboard', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - dashboardUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the dashboard to update', - }, - title: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New title for the dashboard', - }, - folderUid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'New folder UID to move the dashboard to', - }, - tags: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Comma-separated list of new tags', - }, - timezone: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Dashboard timezone (e.g., browser, utc)', - }, - refresh: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', - }, - panels: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON array of panel configurations', - }, - overwrite: { - type: 'boolean', - required: false, - visibility: 'user-only', - description: 'Overwrite even if there is a version conflict', - }, - message: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Commit message for this version', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, - method: 'POST', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const dashboard: Record = { - uid: params.dashboardUid, - } - - if (params.title) dashboard.title = params.title - if (params.tags) { - dashboard.tags = params.tags - .split(',') - .map((t) => t.trim()) - .filter((t) => t) - } - if (params.timezone) dashboard.timezone = params.timezone - if (params.refresh) dashboard.refresh = params.refresh - if (params.panels) { - try { - dashboard.panels = JSON.parse(params.panels) - } catch { - // Keep existing panels if parse fails - } - } - - const body: Record = { - dashboard, - overwrite: params.overwrite !== false, - } - - if (params.folderUid) { - body.folderUid = params.folderUid - } - - if (params.message) { - body.message = params.message - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - id: data.id, - uid: data.uid, - url: data.url, - status: data.status, - version: data.version, - slug: data.slug, - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'The numeric ID of the updated dashboard', - }, - uid: { - type: 'string', - description: 'The UID of the updated dashboard', - }, - url: { - type: 'string', - description: 'The URL path to the dashboard', - }, - status: { - type: 'string', - description: 'Status of the operation (success)', - }, - version: { - type: 'number', - description: 'The new version number of the dashboard', - }, - slug: { - type: 'string', - description: 'URL-friendly slug of the dashboard', - }, - }, -} - -export const deleteDashboardTool: ToolConfig< - GrafanaDeleteDashboardParams, - GrafanaDeleteDashboardResponse -> = { - id: 'grafana_delete_dashboard', - name: 'Grafana Delete Dashboard', - description: 'Delete a dashboard by its UID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - dashboardUid: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The UID of the dashboard to delete', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, - method: 'DELETE', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - title: data.title || '', - message: data.message || 'Dashboard deleted', - id: data.id || 0, - }, - } - }, - - outputs: { - title: { - type: 'string', - description: 'The title of the deleted dashboard', - }, - message: { - type: 'string', - description: 'Confirmation message', - }, - id: { - type: 'number', - description: 'The ID of the deleted dashboard', - }, - }, -} diff --git a/apps/sim/tools/grafana/health.ts b/apps/sim/tools/grafana/data_source_health.ts similarity index 53% rename from apps/sim/tools/grafana/health.ts rename to apps/sim/tools/grafana/data_source_health.ts index 4769aa3276b..7ce8fb18595 100644 --- a/apps/sim/tools/grafana/health.ts +++ b/apps/sim/tools/grafana/data_source_health.ts @@ -1,82 +1,9 @@ import type { GrafanaDataSourceHealthParams, GrafanaDataSourceHealthResponse, - GrafanaHealthCheckParams, - GrafanaHealthCheckResponse, } from '@/tools/grafana/types' import type { ToolConfig } from '@/tools/types' -export const healthCheckTool: ToolConfig = { - id: 'grafana_health_check', - name: 'Grafana Health Check', - description: 'Check the health status of a Grafana instance', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - commit: data.commit || '', - database: data.database || 'ok', - version: data.version || '', - }, - } - }, - - outputs: { - commit: { - type: 'string', - description: 'Git commit hash of the Grafana build', - }, - database: { - type: 'string', - description: 'Database connection status (ok or failing)', - }, - version: { - type: 'string', - description: 'Grafana version number', - }, - }, -} - export const dataSourceHealthTool: ToolConfig< GrafanaDataSourceHealthParams, GrafanaDataSourceHealthResponse diff --git a/apps/sim/tools/grafana/delete_alert_rule.ts b/apps/sim/tools/grafana/delete_alert_rule.ts new file mode 100644 index 00000000000..a5b4cfff5dd --- /dev/null +++ b/apps/sim/tools/grafana/delete_alert_rule.ts @@ -0,0 +1,74 @@ +import type { + GrafanaDeleteAlertRuleParams, + GrafanaDeleteAlertRuleResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteAlertRuleTool: ToolConfig< + GrafanaDeleteAlertRuleParams, + GrafanaDeleteAlertRuleResponse +> = { + id: 'grafana_delete_alert_rule', + name: 'Grafana Delete Alert Rule', + description: 'Delete an alert rule by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to delete', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async () => { + return { + success: true, + output: { + message: 'Alert rule deleted successfully', + }, + } + }, + + outputs: { + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/grafana/delete_annotation.ts b/apps/sim/tools/grafana/delete_annotation.ts new file mode 100644 index 00000000000..109d6da9d63 --- /dev/null +++ b/apps/sim/tools/grafana/delete_annotation.ts @@ -0,0 +1,75 @@ +import type { + GrafanaDeleteAnnotationParams, + GrafanaDeleteAnnotationResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteAnnotationTool: ToolConfig< + GrafanaDeleteAnnotationParams, + GrafanaDeleteAnnotationResponse +> = { + id: 'grafana_delete_annotation', + name: 'Grafana Delete Annotation', + description: 'Delete an annotation by its ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + annotationId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the annotation to delete', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + message: data.message || 'Annotation deleted successfully', + }, + } + }, + + outputs: { + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/grafana/delete_dashboard.ts b/apps/sim/tools/grafana/delete_dashboard.ts new file mode 100644 index 00000000000..8b70ef847af --- /dev/null +++ b/apps/sim/tools/grafana/delete_dashboard.ts @@ -0,0 +1,86 @@ +import type { + GrafanaDeleteDashboardParams, + GrafanaDeleteDashboardResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteDashboardTool: ToolConfig< + GrafanaDeleteDashboardParams, + GrafanaDeleteDashboardResponse +> = { + id: 'grafana_delete_dashboard', + name: 'Grafana Delete Dashboard', + description: 'Delete a dashboard by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to delete', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + title: data.title || '', + message: data.message || 'Dashboard deleted', + id: data.id || 0, + }, + } + }, + + outputs: { + title: { + type: 'string', + description: 'The title of the deleted dashboard', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + id: { + type: 'number', + description: 'The ID of the deleted dashboard', + }, + }, +} diff --git a/apps/sim/tools/grafana/folders.ts b/apps/sim/tools/grafana/folders.ts deleted file mode 100644 index 9232c2a1303..00000000000 --- a/apps/sim/tools/grafana/folders.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type { - GrafanaCreateFolderParams, - GrafanaCreateFolderResponse, - GrafanaListFoldersParams, - GrafanaListFoldersResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const listFoldersTool: ToolConfig = { - id: 'grafana_list_folders', - name: 'Grafana List Folders', - description: 'List all folders in Grafana', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - limit: { - type: 'number', - required: false, - visibility: 'user-only', - description: 'Maximum number of folders to return', - }, - page: { - type: 'number', - required: false, - visibility: 'user-only', - description: 'Page number for pagination', - }, - }, - - request: { - url: (params) => { - const baseUrl = params.baseUrl.replace(/\/$/, '') - const searchParams = new URLSearchParams() - - if (params.limit) searchParams.set('limit', String(params.limit)) - if (params.page) searchParams.set('page', String(params.page)) - - const queryString = searchParams.toString() - return `${baseUrl}/api/folders${queryString ? `?${queryString}` : ''}` - }, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - folders: Array.isArray(data) - ? data.map((f: any) => ({ - id: f.id, - uid: f.uid, - title: f.title, - url: f.url, - hasAcl: f.hasAcl || false, - canSave: f.canSave || false, - canEdit: f.canEdit || false, - canAdmin: f.canAdmin || false, - canDelete: f.canDelete || false, - createdBy: f.createdBy || '', - created: f.created || '', - updatedBy: f.updatedBy || '', - updated: f.updated || '', - version: f.version || 0, - })) - : [], - }, - } - }, - - outputs: { - folders: { - type: 'array', - description: 'List of folders', - items: { - type: 'object', - properties: { - id: { type: 'number', description: 'Folder ID' }, - uid: { type: 'string', description: 'Folder UID' }, - title: { type: 'string', description: 'Folder title' }, - url: { type: 'string', description: 'Folder URL path' }, - }, - }, - }, - }, -} - -export const createFolderTool: ToolConfig = - { - id: 'grafana_create_folder', - name: 'Grafana Create Folder', - description: 'Create a new folder in Grafana', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - title: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The title of the new folder', - }, - uid: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Optional UID for the folder (auto-generated if not provided)', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders`, - method: 'POST', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - body: (params) => { - const body: Record = { - title: params.title, - } - - if (params.uid) { - body.uid = params.uid - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - id: data.id, - uid: data.uid, - title: data.title, - url: data.url, - hasAcl: data.hasAcl || false, - canSave: data.canSave || false, - canEdit: data.canEdit || false, - canAdmin: data.canAdmin || false, - canDelete: data.canDelete || false, - createdBy: data.createdBy || '', - created: data.created || '', - updatedBy: data.updatedBy || '', - updated: data.updated || '', - version: data.version || 0, - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'The numeric ID of the created folder', - }, - uid: { - type: 'string', - description: 'The UID of the created folder', - }, - title: { - type: 'string', - description: 'The title of the created folder', - }, - url: { - type: 'string', - description: 'The URL path to the folder', - }, - }, - } diff --git a/apps/sim/tools/grafana/get_alert_rule.ts b/apps/sim/tools/grafana/get_alert_rule.ts new file mode 100644 index 00000000000..313dcb3c079 --- /dev/null +++ b/apps/sim/tools/grafana/get_alert_rule.ts @@ -0,0 +1,123 @@ +import type { GrafanaGetAlertRuleParams, GrafanaGetAlertRuleResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const getAlertRuleTool: ToolConfig = + { + id: 'grafana_get_alert_rule', + name: 'Grafana Get Alert Rule', + description: 'Get a specific alert rule by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to retrieve', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'Alert rule UID', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + condition: { + type: 'string', + description: 'Alert condition', + }, + data: { + type: 'json', + description: 'Alert rule query data', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + noDataState: { + type: 'string', + description: 'State when no data is returned', + }, + execErrState: { + type: 'string', + description: 'State on execution error', + }, + annotations: { + type: 'json', + description: 'Alert annotations', + }, + labels: { + type: 'json', + description: 'Alert labels', + }, + }, + } diff --git a/apps/sim/tools/grafana/get_dashboard.ts b/apps/sim/tools/grafana/get_dashboard.ts new file mode 100644 index 00000000000..a112bb7d371 --- /dev/null +++ b/apps/sim/tools/grafana/get_dashboard.ts @@ -0,0 +1,76 @@ +import type { GrafanaGetDashboardParams, GrafanaGetDashboardResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const getDashboardTool: ToolConfig = + { + id: 'grafana_get_dashboard', + name: 'Grafana Get Dashboard', + description: 'Get a dashboard by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to retrieve', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dashboard: data.dashboard, + meta: data.meta, + }, + } + }, + + outputs: { + dashboard: { + type: 'json', + description: 'The full dashboard JSON object', + }, + meta: { + type: 'json', + description: 'Dashboard metadata (version, permissions, etc.)', + }, + }, + } diff --git a/apps/sim/tools/grafana/data_sources.ts b/apps/sim/tools/grafana/get_data_source.ts similarity index 53% rename from apps/sim/tools/grafana/data_sources.ts rename to apps/sim/tools/grafana/get_data_source.ts index 30a9ac91b3b..c2711246fe4 100644 --- a/apps/sim/tools/grafana/data_sources.ts +++ b/apps/sim/tools/grafana/get_data_source.ts @@ -1,104 +1,9 @@ import type { GrafanaGetDataSourceParams, GrafanaGetDataSourceResponse, - GrafanaListDataSourcesParams, - GrafanaListDataSourcesResponse, } from '@/tools/grafana/types' import type { ToolConfig } from '@/tools/types' -export const listDataSourcesTool: ToolConfig< - GrafanaListDataSourcesParams, - GrafanaListDataSourcesResponse -> = { - id: 'grafana_list_data_sources', - name: 'Grafana List Data Sources', - description: 'List all data sources configured in Grafana', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/datasources`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - dataSources: Array.isArray(data) - ? data.map((ds: any) => ({ - id: ds.id, - uid: ds.uid, - orgId: ds.orgId, - name: ds.name, - type: ds.type, - typeName: ds.typeName, - typeLogoUrl: ds.typeLogoUrl, - access: ds.access, - url: ds.url, - user: ds.user, - database: ds.database, - basicAuth: ds.basicAuth || false, - isDefault: ds.isDefault || false, - jsonData: ds.jsonData || {}, - readOnly: ds.readOnly || false, - })) - : [], - }, - } - }, - - outputs: { - dataSources: { - type: 'array', - description: 'List of data sources', - items: { - type: 'object', - properties: { - id: { type: 'number', description: 'Data source ID' }, - uid: { type: 'string', description: 'Data source UID' }, - name: { type: 'string', description: 'Data source name' }, - type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' }, - url: { type: 'string', description: 'Data source URL' }, - isDefault: { type: 'boolean', description: 'Whether this is the default data source' }, - }, - }, - }, - }, -} - export const getDataSourceTool: ToolConfig< GrafanaGetDataSourceParams, GrafanaGetDataSourceResponse diff --git a/apps/sim/tools/grafana/health_check.ts b/apps/sim/tools/grafana/health_check.ts new file mode 100644 index 00000000000..46b32775702 --- /dev/null +++ b/apps/sim/tools/grafana/health_check.ts @@ -0,0 +1,73 @@ +import type { GrafanaHealthCheckParams, GrafanaHealthCheckResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const healthCheckTool: ToolConfig = { + id: 'grafana_health_check', + name: 'Grafana Health Check', + description: 'Check the health status of a Grafana instance', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + commit: data.commit || '', + database: data.database || 'ok', + version: data.version || '', + }, + } + }, + + outputs: { + commit: { + type: 'string', + description: 'Git commit hash of the Grafana build', + }, + database: { + type: 'string', + description: 'Database connection status (ok or failing)', + }, + version: { + type: 'string', + description: 'Grafana version number', + }, + }, +} diff --git a/apps/sim/tools/grafana/index.ts b/apps/sim/tools/grafana/index.ts index 2247c15416b..5e031d6a5a6 100644 --- a/apps/sim/tools/grafana/index.ts +++ b/apps/sim/tools/grafana/index.ts @@ -1,27 +1,24 @@ -import { - createAlertRuleTool, - deleteAlertRuleTool, - getAlertRuleTool, - listAlertRulesTool, - listContactPointsTool, - updateAlertRuleTool, -} from '@/tools/grafana/alerts' -import { - createAnnotationTool, - deleteAnnotationTool, - listAnnotationsTool, - updateAnnotationTool, -} from '@/tools/grafana/annotations' -import { - createDashboardTool, - deleteDashboardTool, - getDashboardTool, - listDashboardsTool, - updateDashboardTool, -} from '@/tools/grafana/dashboards' -import { getDataSourceTool, listDataSourcesTool } from '@/tools/grafana/data_sources' -import { createFolderTool, listFoldersTool } from '@/tools/grafana/folders' -import { dataSourceHealthTool, healthCheckTool } from '@/tools/grafana/health' +import { createAlertRuleTool } from '@/tools/grafana/create_alert_rule' +import { createAnnotationTool } from '@/tools/grafana/create_annotation' +import { createDashboardTool } from '@/tools/grafana/create_dashboard' +import { createFolderTool } from '@/tools/grafana/create_folder' +import { dataSourceHealthTool } from '@/tools/grafana/data_source_health' +import { deleteAlertRuleTool } from '@/tools/grafana/delete_alert_rule' +import { deleteAnnotationTool } from '@/tools/grafana/delete_annotation' +import { deleteDashboardTool } from '@/tools/grafana/delete_dashboard' +import { getAlertRuleTool } from '@/tools/grafana/get_alert_rule' +import { getDashboardTool } from '@/tools/grafana/get_dashboard' +import { getDataSourceTool } from '@/tools/grafana/get_data_source' +import { healthCheckTool } from '@/tools/grafana/health_check' +import { listAlertRulesTool } from '@/tools/grafana/list_alert_rules' +import { listAnnotationsTool } from '@/tools/grafana/list_annotations' +import { listContactPointsTool } from '@/tools/grafana/list_contact_points' +import { listDashboardsTool } from '@/tools/grafana/list_dashboards' +import { listDataSourcesTool } from '@/tools/grafana/list_data_sources' +import { listFoldersTool } from '@/tools/grafana/list_folders' +import { updateAlertRuleTool } from '@/tools/grafana/update_alert_rule' +import { updateAnnotationTool } from '@/tools/grafana/update_annotation' +import { updateDashboardTool } from '@/tools/grafana/update_dashboard' // Health tools export const grafanaHealthCheckTool = healthCheckTool diff --git a/apps/sim/tools/grafana/list_alert_rules.ts b/apps/sim/tools/grafana/list_alert_rules.ts new file mode 100644 index 00000000000..5a8d7540e24 --- /dev/null +++ b/apps/sim/tools/grafana/list_alert_rules.ts @@ -0,0 +1,101 @@ +import type { + GrafanaListAlertRulesParams, + GrafanaListAlertRulesResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listAlertRulesTool: ToolConfig< + GrafanaListAlertRulesParams, + GrafanaListAlertRulesResponse +> = { + id: 'grafana_list_alert_rules', + name: 'Grafana List Alert Rules', + description: 'List all alert rules in the Grafana instance', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + rules: Array.isArray(data) + ? data.map((rule: any) => ({ + uid: rule.uid, + title: rule.title, + condition: rule.condition, + data: rule.data, + updated: rule.updated, + noDataState: rule.noDataState, + execErrState: rule.execErrState, + for: rule.for, + annotations: rule.annotations || {}, + labels: rule.labels || {}, + isPaused: rule.isPaused || false, + folderUID: rule.folderUID, + ruleGroup: rule.ruleGroup, + orgId: rule.orgId, + namespace_uid: rule.namespace_uid, + namespace_id: rule.namespace_id, + provenance: rule.provenance || '', + })) + : [], + }, + } + }, + + outputs: { + rules: { + type: 'array', + description: 'List of alert rules', + items: { + type: 'object', + properties: { + uid: { type: 'string', description: 'Alert rule UID' }, + title: { type: 'string', description: 'Alert rule title' }, + condition: { type: 'string', description: 'Alert condition' }, + folderUID: { type: 'string', description: 'Parent folder UID' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + noDataState: { type: 'string', description: 'State when no data is returned' }, + execErrState: { type: 'string', description: 'State on execution error' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/list_annotations.ts b/apps/sim/tools/grafana/list_annotations.ts new file mode 100644 index 00000000000..5d68434e1ca --- /dev/null +++ b/apps/sim/tools/grafana/list_annotations.ts @@ -0,0 +1,161 @@ +import type { + GrafanaListAnnotationsParams, + GrafanaListAnnotationsResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listAnnotationsTool: ToolConfig< + GrafanaListAnnotationsParams, + GrafanaListAnnotationsResponse +> = { + id: 'grafana_list_annotations', + name: 'Grafana List Annotations', + description: 'Query annotations by time range, dashboard, or tags', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + from: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time in epoch milliseconds', + }, + to: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time in epoch milliseconds', + }, + dashboardUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by dashboard UID', + }, + panelId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Filter by panel ID', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags to filter by', + }, + type: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by type (alert or annotation)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of annotations to return', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + + if (params.from) searchParams.set('from', String(params.from)) + if (params.to) searchParams.set('to', String(params.to)) + if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid) + if (params.panelId) searchParams.set('panelId', String(params.panelId)) + if (params.tags) { + params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim())) + } + if (params.type) searchParams.set('type', params.type) + if (params.limit) searchParams.set('limit', String(params.limit)) + + const queryString = searchParams.toString() + return `${baseUrl}/api/annotations${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + annotations: Array.isArray(data) + ? data.map((a: any) => ({ + id: a.id, + alertId: a.alertId, + alertName: a.alertName, + dashboardId: a.dashboardId, + dashboardUID: a.dashboardUID, + panelId: a.panelId, + userId: a.userId, + newState: a.newState, + prevState: a.prevState, + created: a.created, + updated: a.updated, + time: a.time, + timeEnd: a.timeEnd, + text: a.text, + tags: a.tags || [], + login: a.login, + email: a.email, + avatarUrl: a.avatarUrl, + data: a.data, + })) + : [], + }, + } + }, + + outputs: { + annotations: { + type: 'array', + description: 'List of annotations', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Annotation ID' }, + text: { type: 'string', description: 'Annotation text' }, + tags: { type: 'array', description: 'Annotation tags' }, + time: { type: 'number', description: 'Start time in epoch ms' }, + timeEnd: { type: 'number', description: 'End time in epoch ms' }, + dashboardUID: { type: 'string', description: 'Dashboard UID' }, + panelId: { type: 'number', description: 'Panel ID' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/list_contact_points.ts b/apps/sim/tools/grafana/list_contact_points.ts new file mode 100644 index 00000000000..3cde333bd8b --- /dev/null +++ b/apps/sim/tools/grafana/list_contact_points.ts @@ -0,0 +1,87 @@ +import type { + GrafanaListContactPointsParams, + GrafanaListContactPointsResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listContactPointsTool: ToolConfig< + GrafanaListContactPointsParams, + GrafanaListContactPointsResponse +> = { + id: 'grafana_list_contact_points', + name: 'Grafana List Contact Points', + description: 'List all alert notification contact points', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + contactPoints: Array.isArray(data) + ? data.map((cp: any) => ({ + uid: cp.uid, + name: cp.name, + type: cp.type, + settings: cp.settings || {}, + disableResolveMessage: cp.disableResolveMessage || false, + provenance: cp.provenance || '', + })) + : [], + }, + } + }, + + outputs: { + contactPoints: { + type: 'array', + description: 'List of contact points', + items: { + type: 'object', + properties: { + uid: { type: 'string', description: 'Contact point UID' }, + name: { type: 'string', description: 'Contact point name' }, + type: { type: 'string', description: 'Notification type (email, slack, etc.)' }, + settings: { type: 'object', description: 'Type-specific settings' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/list_dashboards.ts b/apps/sim/tools/grafana/list_dashboards.ts new file mode 100644 index 00000000000..068abb0249a --- /dev/null +++ b/apps/sim/tools/grafana/list_dashboards.ts @@ -0,0 +1,143 @@ +import type { + GrafanaListDashboardsParams, + GrafanaListDashboardsResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listDashboardsTool: ToolConfig< + GrafanaListDashboardsParams, + GrafanaListDashboardsResponse +> = { + id: 'grafana_list_dashboards', + name: 'Grafana List Dashboards', + description: 'Search and list all dashboards', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter dashboards by title', + }, + tag: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by tag (comma-separated for multiple tags)', + }, + folderIds: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by folder IDs (comma-separated)', + }, + starred: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Only return starred dashboards', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of dashboards to return', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + searchParams.set('type', 'dash-db') + + if (params.query) searchParams.set('query', params.query) + if (params.tag) { + params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim())) + } + if (params.folderIds) { + params.folderIds.split(',').forEach((id) => searchParams.append('folderIds', id.trim())) + } + if (params.starred) searchParams.set('starred', 'true') + if (params.limit) searchParams.set('limit', String(params.limit)) + + return `${baseUrl}/api/search?${searchParams.toString()}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dashboards: Array.isArray(data) + ? data.map((d: any) => ({ + id: d.id, + uid: d.uid, + title: d.title, + uri: d.uri, + url: d.url, + slug: d.slug, + type: d.type, + tags: d.tags || [], + isStarred: d.isStarred || false, + folderId: d.folderId, + folderUid: d.folderUid, + folderTitle: d.folderTitle, + folderUrl: d.folderUrl, + sortMeta: d.sortMeta, + })) + : [], + }, + } + }, + + outputs: { + dashboards: { + type: 'array', + description: 'List of dashboard search results', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Dashboard ID' }, + uid: { type: 'string', description: 'Dashboard UID' }, + title: { type: 'string', description: 'Dashboard title' }, + url: { type: 'string', description: 'Dashboard URL path' }, + tags: { type: 'array', description: 'Dashboard tags' }, + folderTitle: { type: 'string', description: 'Parent folder title' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/list_data_sources.ts b/apps/sim/tools/grafana/list_data_sources.ts new file mode 100644 index 00000000000..f835cf3a4a8 --- /dev/null +++ b/apps/sim/tools/grafana/list_data_sources.ts @@ -0,0 +1,98 @@ +import type { + GrafanaListDataSourcesParams, + GrafanaListDataSourcesResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listDataSourcesTool: ToolConfig< + GrafanaListDataSourcesParams, + GrafanaListDataSourcesResponse +> = { + id: 'grafana_list_data_sources', + name: 'Grafana List Data Sources', + description: 'List all data sources configured in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/datasources`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + dataSources: Array.isArray(data) + ? data.map((ds: any) => ({ + id: ds.id, + uid: ds.uid, + orgId: ds.orgId, + name: ds.name, + type: ds.type, + typeName: ds.typeName, + typeLogoUrl: ds.typeLogoUrl, + access: ds.access, + url: ds.url, + user: ds.user, + database: ds.database, + basicAuth: ds.basicAuth || false, + isDefault: ds.isDefault || false, + jsonData: ds.jsonData || {}, + readOnly: ds.readOnly || false, + })) + : [], + }, + } + }, + + outputs: { + dataSources: { + type: 'array', + description: 'List of data sources', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Data source ID' }, + uid: { type: 'string', description: 'Data source UID' }, + name: { type: 'string', description: 'Data source name' }, + type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' }, + url: { type: 'string', description: 'Data source URL' }, + isDefault: { type: 'boolean', description: 'Whether this is the default data source' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/list_folders.ts b/apps/sim/tools/grafana/list_folders.ts new file mode 100644 index 00000000000..c3f6f3a33f3 --- /dev/null +++ b/apps/sim/tools/grafana/list_folders.ts @@ -0,0 +1,110 @@ +import type { GrafanaListFoldersParams, GrafanaListFoldersResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const listFoldersTool: ToolConfig = { + id: 'grafana_list_folders', + name: 'Grafana List Folders', + description: 'List all folders in Grafana', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of folders to return', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + + if (params.limit) searchParams.set('limit', String(params.limit)) + if (params.page) searchParams.set('page', String(params.page)) + + const queryString = searchParams.toString() + return `${baseUrl}/api/folders${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + folders: Array.isArray(data) + ? data.map((f: any) => ({ + id: f.id, + uid: f.uid, + title: f.title, + url: f.url, + hasAcl: f.hasAcl || false, + canSave: f.canSave || false, + canEdit: f.canEdit || false, + canAdmin: f.canAdmin || false, + canDelete: f.canDelete || false, + createdBy: f.createdBy || '', + created: f.created || '', + updatedBy: f.updatedBy || '', + updated: f.updated || '', + version: f.version || 0, + })) + : [], + }, + } + }, + + outputs: { + folders: { + type: 'array', + description: 'List of folders', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Folder ID' }, + uid: { type: 'string', description: 'Folder UID' }, + title: { type: 'string', description: 'Folder title' }, + url: { type: 'string', description: 'Folder URL path' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts new file mode 100644 index 00000000000..ae8febc8b51 --- /dev/null +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -0,0 +1,201 @@ +import type { + GrafanaUpdateAlertRuleParams, + GrafanaUpdateAlertRuleResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const updateAlertRuleTool: ToolConfig< + GrafanaUpdateAlertRuleParams, + GrafanaUpdateAlertRuleResponse +> = { + id: 'grafana_update_alert_rule', + name: 'Grafana Update Alert Rule', + description: 'Update an existing alert rule', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + alertRuleUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the alert rule to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the alert rule', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New folder UID to move the alert to', + }, + ruleGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New rule group name', + }, + condition: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New condition refId', + }, + data: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New JSON array of query/expression data objects', + }, + forDuration: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Duration to wait before firing (e.g., 5m, 1h)', + }, + noDataState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State when no data is returned (NoData, Alerting, OK)', + }, + execErrState: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'State on execution error (Alerting, OK)', + }, + annotations: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of annotations', + }, + labels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of labels', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + method: 'PUT', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = {} + + if (params.title) body.title = params.title + if (params.folderUid) body.folderUID = params.folderUid + if (params.ruleGroup) body.ruleGroup = params.ruleGroup + if (params.condition) body.condition = params.condition + if (params.forDuration) body.for = params.forDuration + if (params.noDataState) body.noDataState = params.noDataState + if (params.execErrState) body.execErrState = params.execErrState + + if (params.data) { + try { + body.data = JSON.parse(params.data) + } catch { + // Skip if invalid JSON + } + } + + if (params.annotations) { + try { + body.annotations = JSON.parse(params.annotations) + } catch { + // Skip if invalid JSON + } + } + + if (params.labels) { + try { + body.labels = JSON.parse(params.labels) + } catch { + // Skip if invalid JSON + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: data.uid, + title: data.title, + condition: data.condition, + data: data.data, + updated: data.updated, + noDataState: data.noDataState, + execErrState: data.execErrState, + for: data.for, + annotations: data.annotations || {}, + labels: data.labels || {}, + isPaused: data.isPaused || false, + folderUID: data.folderUID, + ruleGroup: data.ruleGroup, + orgId: data.orgId, + namespace_uid: data.namespace_uid, + namespace_id: data.namespace_id, + provenance: data.provenance || '', + }, + } + }, + + outputs: { + uid: { + type: 'string', + description: 'The UID of the updated alert rule', + }, + title: { + type: 'string', + description: 'Alert rule title', + }, + folderUID: { + type: 'string', + description: 'Parent folder UID', + }, + ruleGroup: { + type: 'string', + description: 'Rule group name', + }, + }, +} diff --git a/apps/sim/tools/grafana/update_annotation.ts b/apps/sim/tools/grafana/update_annotation.ts new file mode 100644 index 00000000000..f3b475ced62 --- /dev/null +++ b/apps/sim/tools/grafana/update_annotation.ts @@ -0,0 +1,126 @@ +import type { + GrafanaUpdateAnnotationParams, + GrafanaUpdateAnnotationResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const updateAnnotationTool: ToolConfig< + GrafanaUpdateAnnotationParams, + GrafanaUpdateAnnotationResponse +> = { + id: 'grafana_update_annotation', + name: 'Grafana Update Annotation', + description: 'Update an existing annotation', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + annotationId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the annotation to update', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New text content for the annotation', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of new tags', + }, + time: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New start time in epoch milliseconds', + }, + timeEnd: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New end time in epoch milliseconds', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`, + method: 'PATCH', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const body: Record = { + text: params.text, + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + + if (params.time) { + body.time = params.time + } + + if (params.timeEnd) { + body.timeEnd = params.timeEnd + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id || 0, + message: data.message || 'Annotation updated successfully', + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The ID of the updated annotation', + }, + message: { + type: 'string', + description: 'Confirmation message', + }, + }, +} diff --git a/apps/sim/tools/grafana/update_dashboard.ts b/apps/sim/tools/grafana/update_dashboard.ts new file mode 100644 index 00000000000..78561d474ef --- /dev/null +++ b/apps/sim/tools/grafana/update_dashboard.ts @@ -0,0 +1,185 @@ +import type { + GrafanaUpdateDashboardParams, + GrafanaUpdateDashboardResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const updateDashboardTool: ToolConfig< + GrafanaUpdateDashboardParams, + GrafanaUpdateDashboardResponse +> = { + id: 'grafana_update_dashboard', + name: 'Grafana Update Dashboard', + description: 'Update an existing dashboard', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Organization ID for multi-org Grafana instances', + }, + dashboardUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the dashboard to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the dashboard', + }, + folderUid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New folder UID to move the dashboard to', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of new tags', + }, + timezone: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Dashboard timezone (e.g., browser, utc)', + }, + refresh: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)', + }, + panels: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of panel configurations', + }, + overwrite: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Overwrite even if there is a version conflict', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Commit message for this version', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + body: (params) => { + const dashboard: Record = { + uid: params.dashboardUid, + } + + if (params.title) dashboard.title = params.title + if (params.tags) { + dashboard.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } + if (params.timezone) dashboard.timezone = params.timezone + if (params.refresh) dashboard.refresh = params.refresh + if (params.panels) { + try { + dashboard.panels = JSON.parse(params.panels) + } catch { + // Keep existing panels if parse fails + } + } + + const body: Record = { + dashboard, + overwrite: params.overwrite !== false, + } + + if (params.folderUid) { + body.folderUid = params.folderUid + } + + if (params.message) { + body.message = params.message + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + uid: data.uid, + url: data.url, + status: data.status, + version: data.version, + slug: data.slug, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'The numeric ID of the updated dashboard', + }, + uid: { + type: 'string', + description: 'The UID of the updated dashboard', + }, + url: { + type: 'string', + description: 'The URL path to the dashboard', + }, + status: { + type: 'string', + description: 'Status of the operation (success)', + }, + version: { + type: 'number', + description: 'The new version number of the dashboard', + }, + slug: { + type: 'string', + description: 'URL-friendly slug of the dashboard', + }, + }, +} From b4a869fa99f250d47d3891288da6ffe66ab7f44e Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 15:10:23 -0800 Subject: [PATCH 05/11] update docs script & docs --- apps/docs/content/docs/en/tools/dropbox.mdx | 4 ++++ apps/docs/content/docs/en/tools/pylon.mdx | 10 ++++++++++ apps/docs/content/docs/en/tools/zendesk.mdx | 3 +++ scripts/generate-docs.ts | 4 ++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/docs/en/tools/dropbox.mdx b/apps/docs/content/docs/en/tools/dropbox.mdx index a92555a697f..399d06a0026 100644 --- a/apps/docs/content/docs/en/tools/dropbox.mdx +++ b/apps/docs/content/docs/en/tools/dropbox.mdx @@ -186,6 +186,10 @@ Create a shareable link for a file or folder in Dropbox | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the file or folder to share | +| `requestedVisibility` | string | No | Visibility: public, team_only, or password | +| `linkPassword` | string | No | Password for the shared link \(only if visibility is password\) | +| `expires` | string | No | Expiration date in ISO 8601 format \(e.g., 2025-12-31T23:59:59Z\) | #### Output diff --git a/apps/docs/content/docs/en/tools/pylon.mdx b/apps/docs/content/docs/en/tools/pylon.mdx index ff81223f5dc..4ac96873fd1 100644 --- a/apps/docs/content/docs/en/tools/pylon.mdx +++ b/apps/docs/content/docs/en/tools/pylon.mdx @@ -89,6 +89,13 @@ Create a new issue with specified properties | `bodyHtml` | string | Yes | Issue body in HTML format | | `accountId` | string | No | Account ID to associate with issue | | `assigneeId` | string | No | User ID to assign issue to | +| `teamId` | string | No | Team ID to assign issue to | +| `requesterId` | string | No | Requester user ID \(alternative to requester_email\) | +| `requesterEmail` | string | No | Requester email address \(alternative to requester_id\) | +| `priority` | string | No | Issue priority | +| `tags` | string | No | Comma-separated tag IDs | +| `customFields` | string | No | Custom fields as JSON object | +| `attachmentUrls` | string | No | Comma-separated attachment URLs | #### Output @@ -130,6 +137,9 @@ Update an existing issue | `teamId` | string | No | Team ID to assign issue to | | `tags` | string | No | Comma-separated tag IDs | | `customFields` | string | No | Custom fields as JSON object | +| `customerPortalVisible` | boolean | No | Whether issue is visible in customer portal | +| `requesterId` | string | No | Requester user ID | +| `accountId` | string | No | Account ID to associate with issue | #### Output diff --git a/apps/docs/content/docs/en/tools/zendesk.mdx b/apps/docs/content/docs/en/tools/zendesk.mdx index 9be81447603..a5b10998161 100644 --- a/apps/docs/content/docs/en/tools/zendesk.mdx +++ b/apps/docs/content/docs/en/tools/zendesk.mdx @@ -117,6 +117,9 @@ Create a new ticket in Zendesk with support for custom fields | `type` | string | No | Type \(problem, incident, question, task\) | | `tags` | string | No | Comma-separated tags | | `assigneeId` | string | No | Assignee user ID | +| `groupId` | string | No | Group ID | +| `requesterId` | string | No | Requester user ID | +| `customFields` | string | No | Custom fields as JSON object \(e.g., \{"field_id": "value"\}\) | #### Output diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index c2885e4efea..f863e53fee7 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -388,7 +388,7 @@ function extractToolInfo( } | null { try { const toolConfigRegex = - /params\s*:\s*{([\s\S]*?)},?\s*(?:outputs|oauth|request|directExecution|postProcess|transformResponse)/ + /params\s*:\s*{([\s\S]*?)},?\s*(?:outputs|oauth|request|directExecution|postProcess|transformResponse)\s*:/ const toolConfigMatch = fileContent.match(toolConfigRegex) const descriptionRegex = /description\s*:\s*['"](.*?)['"].*/ @@ -461,7 +461,7 @@ function extractToolInfo( let outputs: Record = {} const outputsFieldRegex = - /outputs\s*:\s*{([\s\S]*?)}\s*,?\s*(?:oauth|params|request|directExecution|postProcess|transformResponse|$|\})/ + /outputs\s*:\s*{([\s\S]*?)}\s*,?\s*(?:(?:oauth|params|request|directExecution|postProcess|transformResponse)\s*:|$|\})/ const outputsFieldMatch = fileContent.match(outputsFieldRegex) if (outputsFieldMatch) { From 5fe54b392b66210eefde8b4051691fae89b025b2 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 16:20:25 -0800 Subject: [PATCH 06/11] added more zoom endpoints --- apps/docs/content/docs/en/tools/zoom.mdx | 81 ++++++++- .../callback/shopify}/route.ts | 2 +- .../auth/{ => oauth2}/shopify/store/route.ts | 0 .../app/api/auth/shopify/authorize/route.ts | 9 +- .../components/oauth-required-modal.tsx | 28 +-- apps/sim/blocks/blocks/shopify.ts | 13 +- apps/sim/blocks/blocks/zoom.ts | 141 ++++++++++++++-- apps/sim/lib/auth/auth.ts | 63 +++++++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/oauth/oauth.ts | 39 +++-- apps/sim/tools/registry.ts | 8 + apps/sim/tools/types.ts | 1 + apps/sim/tools/zoom/create_meeting.ts | 1 + apps/sim/tools/zoom/delete_meeting.ts | 1 + apps/sim/tools/zoom/delete_recording.ts | 93 ++++++++++ apps/sim/tools/zoom/get_meeting.ts | 1 + apps/sim/tools/zoom/get_meeting_invitation.ts | 1 + apps/sim/tools/zoom/get_meeting_recordings.ts | 131 +++++++++++++++ apps/sim/tools/zoom/index.ts | 15 ++ apps/sim/tools/zoom/list_meetings.ts | 1 + apps/sim/tools/zoom/list_past_participants.ts | 127 ++++++++++++++ apps/sim/tools/zoom/list_recordings.ts | 159 ++++++++++++++++++ apps/sim/tools/zoom/types.ts | 116 +++++++++++++ apps/sim/tools/zoom/update_meeting.ts | 1 + 24 files changed, 974 insertions(+), 60 deletions(-) rename apps/sim/app/api/auth/{shopify/callback => oauth2/callback/shopify}/route.ts (98%) rename apps/sim/app/api/auth/{ => oauth2}/shopify/store/route.ts (100%) create mode 100644 apps/sim/tools/zoom/delete_recording.ts create mode 100644 apps/sim/tools/zoom/get_meeting_recordings.ts create mode 100644 apps/sim/tools/zoom/list_past_participants.ts create mode 100644 apps/sim/tools/zoom/list_recordings.ts diff --git a/apps/docs/content/docs/en/tools/zoom.mdx b/apps/docs/content/docs/en/tools/zoom.mdx index 6503342b62b..60cf268e59f 100644 --- a/apps/docs/content/docs/en/tools/zoom.mdx +++ b/apps/docs/content/docs/en/tools/zoom.mdx @@ -1,6 +1,6 @@ --- title: Zoom -description: Create and manage Zoom meetings +description: Create and manage Zoom meetings and recordings --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -33,7 +33,7 @@ These capabilities let you streamline remote collaboration, automate recurring v ## Usage Instructions -Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, and manage meeting settings programmatically. +Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, recordings, and participants. Manage cloud recordings programmatically. @@ -170,6 +170,83 @@ Get the meeting invitation text for a Zoom meeting | --------- | ---- | ----------- | | `invitation` | string | The meeting invitation text | +### `zoom_list_recordings` + +List all cloud recordings for a Zoom user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `userId` | string | Yes | The user ID or email address. Use "me" for the authenticated user. | +| `from` | string | No | Start date in yyyy-mm-dd format \(within last 6 months\) | +| `to` | string | No | End date in yyyy-mm-dd format | +| `pageSize` | number | No | Number of records per page \(max 300\) | +| `nextPageToken` | string | No | Token for pagination to get next page of results | +| `trash` | boolean | No | Set to true to list recordings from trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recordings` | array | List of recordings | +| `pageInfo` | object | Pagination information | + +### `zoom_get_meeting_recordings` + +Get all recordings for a specific Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID or meeting UUID | +| `includeFolderItems` | boolean | No | Include items within a folder | +| `ttl` | number | No | Time to live for download URLs in seconds \(max 604800\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `recording` | object | The meeting recording with all files | + +### `zoom_delete_recording` + +Delete cloud recordings for a Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The meeting ID or meeting UUID | +| `recordingId` | string | No | Specific recording file ID to delete. If not provided, deletes all recordings. | +| `action` | string | No | Delete action: "trash" \(move to trash\) or "delete" \(permanently delete\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the recording was deleted successfully | + +### `zoom_list_past_participants` + +List participants from a past Zoom meeting + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `meetingId` | string | Yes | The past meeting ID or UUID | +| `pageSize` | number | No | Number of records per page \(max 300\) | +| `nextPageToken` | string | No | Token for pagination to get next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `participants` | array | List of meeting participants | +| `pageInfo` | object | Pagination information | + ## Notes diff --git a/apps/sim/app/api/auth/shopify/callback/route.ts b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts similarity index 98% rename from apps/sim/app/api/auth/shopify/callback/route.ts rename to apps/sim/app/api/auth/oauth2/callback/shopify/route.ts index a95287f36d6..21e33ccacb2 100644 --- a/apps/sim/app/api/auth/shopify/callback/route.ts +++ b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts @@ -83,7 +83,7 @@ export async function GET(request: NextRequest) { } // Store the token - redirect to store endpoint with token data - const storeUrl = new URL(`${baseUrl}/api/auth/shopify/store`) + const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/shopify/store`) // Create response with redirect to store page that will handle token storage const response = NextResponse.redirect(storeUrl) diff --git a/apps/sim/app/api/auth/shopify/store/route.ts b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts similarity index 100% rename from apps/sim/app/api/auth/shopify/store/route.ts rename to apps/sim/app/api/auth/oauth2/shopify/store/route.ts diff --git a/apps/sim/app/api/auth/shopify/authorize/route.ts b/apps/sim/app/api/auth/shopify/authorize/route.ts index 0a65ec76f12..e0e8f2878b9 100644 --- a/apps/sim/app/api/auth/shopify/authorize/route.ts +++ b/apps/sim/app/api/auth/shopify/authorize/route.ts @@ -9,17 +9,12 @@ const logger = createLogger('ShopifyAuthorize') export const dynamic = 'force-dynamic' // Shopify OAuth scopes needed for the integration +// Note: In Shopify, write scopes imply read access const SHOPIFY_SCOPES = [ - 'read_products', 'write_products', - 'read_orders', 'write_orders', - 'read_customers', 'write_customers', - 'read_inventory', 'write_inventory', - 'read_fulfillments', - 'write_fulfillments', ].join(',') export async function GET(request: NextRequest) { @@ -160,7 +155,7 @@ export async function GET(request: NextRequest) { } const baseUrl = getBaseUrl() - const redirectUri = `${baseUrl}/api/auth/shopify/callback` + const redirectUri = `${baseUrl}/api/auth/oauth2/callback/shopify` // Generate a random state for CSRF protection const state = crypto.randomUUID() 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 814c6dca553..a5a0313b132 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 @@ -235,17 +235,23 @@ const SCOPE_DESCRIPTIONS: Record = { // Box scopes root_readwrite: 'Read and write all files and folders in your Box account', root_readonly: 'Read all files and folders in your Box account', - // Shopify scopes - read_products: 'View your Shopify products', - write_products: 'Create and manage your Shopify products', - read_orders: 'View your Shopify orders', - write_orders: 'Manage your Shopify orders', - read_customers: 'View your Shopify customers', - write_customers: 'Create and manage your Shopify customers', - read_inventory: 'View your Shopify inventory', - write_inventory: 'Manage your Shopify inventory levels', - read_fulfillments: 'View your Shopify fulfillments', - write_fulfillments: 'Create and manage your Shopify fulfillments', + // Shopify scopes (write implies read in Shopify) + write_products: 'Read and manage your Shopify products', + write_orders: 'Read and manage your Shopify orders', + write_customers: 'Read and manage your Shopify customers', + write_inventory: 'Read and manage your Shopify inventory levels', + // Zoom scopes + 'user:read:user': 'View your Zoom profile information', + 'meeting:write:meeting': 'Create Zoom meetings', + 'meeting:read:meeting': 'View Zoom meeting details', + 'meeting:read:list_meetings': 'List your Zoom meetings', + 'meeting:update:meeting': 'Update Zoom meetings', + 'meeting:delete:meeting': 'Delete Zoom meetings', + 'meeting:read:invitation': 'View Zoom meeting invitations', + 'meeting:read:list_past_participants': 'View past meeting participants', + 'cloud_recording:read:list_user_recordings': 'List your Zoom cloud recordings', + 'cloud_recording:read:list_recording_files': 'View recording files', + 'cloud_recording:delete:recording_file': 'Delete cloud recordings', } function getScopeDescription(scope: string): string { diff --git a/apps/sim/blocks/blocks/shopify.ts b/apps/sim/blocks/blocks/shopify.ts index f614f5a3068..bc5cc59160c 100644 --- a/apps/sim/blocks/blocks/shopify.ts +++ b/apps/sim/blocks/blocks/shopify.ts @@ -53,16 +53,7 @@ export const ShopifyBlock: BlockConfig = { title: 'Shopify Account', type: 'oauth-input', serviceId: 'shopify', - requiredScopes: [ - 'read_products', - 'write_products', - 'read_orders', - 'write_orders', - 'read_customers', - 'write_customers', - 'read_inventory', - 'write_inventory', - ], + requiredScopes: ['write_products', 'write_orders', 'write_customers', 'write_inventory'], placeholder: 'Select Shopify account', required: true, }, @@ -71,7 +62,7 @@ export const ShopifyBlock: BlockConfig = { title: 'Shop Domain', type: 'short-input', placeholder: 'Auto-detected from OAuth or enter manually', - // Not required - will use shop domain from OAuth credential if not provided + hidden: true, // Auto-detected from OAuth credential's idToken field }, // Product ID (for get/update/delete operations) { diff --git a/apps/sim/blocks/blocks/zoom.ts b/apps/sim/blocks/blocks/zoom.ts index 1a3cd0d166e..54361514187 100644 --- a/apps/sim/blocks/blocks/zoom.ts +++ b/apps/sim/blocks/blocks/zoom.ts @@ -6,10 +6,10 @@ import type { ZoomResponse } from '@/tools/zoom/types' export const ZoomBlock: BlockConfig = { type: 'zoom', name: 'Zoom', - description: 'Create and manage Zoom meetings', + description: 'Create and manage Zoom meetings and recordings', authMode: AuthMode.OAuth, longDescription: - 'Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, and manage meeting settings programmatically.', + 'Integrate Zoom into workflows. Create, list, update, and delete Zoom meetings. Get meeting details, invitations, recordings, and participants. Manage cloud recordings programmatically.', docsLink: 'https://docs.sim.ai/tools/zoom', category: 'tools', bgColor: '#2D8CFF', @@ -26,6 +26,10 @@ export const ZoomBlock: BlockConfig = { { label: 'Update Meeting', id: 'zoom_update_meeting' }, { label: 'Delete Meeting', id: 'zoom_delete_meeting' }, { label: 'Get Meeting Invitation', id: 'zoom_get_meeting_invitation' }, + { label: 'List Recordings', id: 'zoom_list_recordings' }, + { label: 'Get Meeting Recordings', id: 'zoom_get_meeting_recordings' }, + { label: 'Delete Recording', id: 'zoom_delete_recording' }, + { label: 'List Past Participants', id: 'zoom_list_past_participants' }, ], value: () => 'zoom_create_meeting', }, @@ -35,11 +39,17 @@ export const ZoomBlock: BlockConfig = { type: 'oauth-input', serviceId: 'zoom', requiredScopes: [ - 'meeting:write:meeting:admin', - 'meeting:read:meeting:admin', - 'meeting:read:list_meetings:admin', - 'meeting:update:meeting:admin', - 'meeting:delete:meeting:admin', + 'user:read:user', + 'meeting:write:meeting', + 'meeting:read:meeting', + 'meeting:read:list_meetings', + 'meeting:update:meeting', + 'meeting:delete:meeting', + 'meeting:read:invitation', + 'meeting:read:list_past_participants', + 'cloud_recording:read:list_user_recordings', + 'cloud_recording:read:list_recording_files', + 'cloud_recording:delete:recording_file', ], placeholder: 'Select Zoom account', required: true, @@ -53,10 +63,10 @@ export const ZoomBlock: BlockConfig = { required: true, condition: { field: 'operation', - value: ['zoom_create_meeting', 'zoom_list_meetings'], + value: ['zoom_create_meeting', 'zoom_list_meetings', 'zoom_list_recordings'], }, }, - // Meeting ID for get/update/delete/invitation operations + // Meeting ID for get/update/delete/invitation/recordings/participants operations { id: 'meetingId', title: 'Meeting ID', @@ -70,6 +80,9 @@ export const ZoomBlock: BlockConfig = { 'zoom_update_meeting', 'zoom_delete_meeting', 'zoom_get_meeting_invitation', + 'zoom_get_meeting_recordings', + 'zoom_delete_recording', + 'zoom_list_past_participants', ], }, }, @@ -254,7 +267,7 @@ export const ZoomBlock: BlockConfig = { placeholder: 'Number of results (max 300)', condition: { field: 'operation', - value: ['zoom_list_meetings'], + value: ['zoom_list_meetings', 'zoom_list_recordings', 'zoom_list_past_participants'], }, }, { @@ -264,7 +277,54 @@ export const ZoomBlock: BlockConfig = { placeholder: 'Token for next page', condition: { field: 'operation', - value: ['zoom_list_meetings'], + value: ['zoom_list_meetings', 'zoom_list_recordings', 'zoom_list_past_participants'], + }, + }, + // Recording date range + { + id: 'fromDate', + title: 'From Date', + type: 'short-input', + placeholder: 'yyyy-mm-dd (within last 6 months)', + condition: { + field: 'operation', + value: ['zoom_list_recordings'], + }, + }, + { + id: 'toDate', + title: 'To Date', + type: 'short-input', + placeholder: 'yyyy-mm-dd', + condition: { + field: 'operation', + value: ['zoom_list_recordings'], + }, + }, + // Recording ID for delete + { + id: 'recordingId', + title: 'Recording ID', + type: 'short-input', + placeholder: 'Specific recording file ID (optional)', + condition: { + field: 'operation', + value: ['zoom_delete_recording'], + }, + }, + // Delete action + { + id: 'deleteAction', + title: 'Delete Action', + type: 'dropdown', + options: [ + { label: 'Move to Trash', id: 'trash' }, + { label: 'Permanently Delete', id: 'delete' }, + ], + value: () => 'trash', + condition: { + field: 'operation', + value: ['zoom_delete_recording'], }, }, // Delete options @@ -296,6 +356,10 @@ export const ZoomBlock: BlockConfig = { 'zoom_update_meeting', 'zoom_delete_meeting', 'zoom_get_meeting_invitation', + 'zoom_list_recordings', + 'zoom_get_meeting_recordings', + 'zoom_delete_recording', + 'zoom_list_past_participants', ], config: { tool: (params) => { @@ -396,6 +460,50 @@ export const ZoomBlock: BlockConfig = { meetingId: params.meetingId.trim(), } + case 'zoom_list_recordings': + if (!params.userId?.trim()) { + throw new Error('User ID is required.') + } + return { + ...baseParams, + userId: params.userId.trim(), + from: params.fromDate, + to: params.toDate, + pageSize: params.pageSize ? Number(params.pageSize) : undefined, + nextPageToken: params.nextPageToken, + } + + case 'zoom_get_meeting_recordings': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + } + + case 'zoom_delete_recording': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + recordingId: params.recordingId, + action: params.deleteAction, + } + + case 'zoom_list_past_participants': + if (!params.meetingId?.trim()) { + throw new Error('Meeting ID is required.') + } + return { + ...baseParams, + meetingId: params.meetingId.trim(), + pageSize: params.pageSize ? Number(params.pageSize) : undefined, + nextPageToken: params.nextPageToken, + } + default: return baseParams } @@ -426,6 +534,10 @@ export const ZoomBlock: BlockConfig = { nextPageToken: { type: 'string', description: 'Page token for pagination' }, occurrenceId: { type: 'string', description: 'Occurrence ID for recurring meetings' }, cancelMeetingReminder: { type: 'boolean', description: 'Send cancellation email' }, + fromDate: { type: 'string', description: 'Start date for recordings list (yyyy-mm-dd)' }, + toDate: { type: 'string', description: 'End date for recordings list (yyyy-mm-dd)' }, + recordingId: { type: 'string', description: 'Specific recording file ID' }, + deleteAction: { type: 'string', description: 'Delete action (trash or delete)' }, }, outputs: { // Meeting outputs @@ -445,6 +557,13 @@ export const ZoomBlock: BlockConfig = { settings: { type: 'json', description: 'Meeting settings' }, // Invitation invitation: { type: 'string', description: 'Meeting invitation text' }, + // Recording outputs + recording: { type: 'json', description: 'Recording data' }, + recordings: { type: 'json', description: 'List of recordings' }, + recording_files: { type: 'json', description: 'Recording files' }, + share_url: { type: 'string', description: 'Share URL for recording' }, + // Participant outputs + participants: { type: 'json', description: 'List of participants' }, // Pagination pageInfo: { type: 'json', description: 'Pagination information' }, // Success indicator diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index c79036a60e7..2e9a15580e3 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1694,6 +1694,69 @@ export const auth = betterAuth({ } }, }, + + // Zoom provider + { + providerId: 'zoom', + clientId: env.ZOOM_CLIENT_ID as string, + clientSecret: env.ZOOM_CLIENT_SECRET as string, + authorizationUrl: 'https://zoom.us/oauth/authorize', + tokenUrl: 'https://zoom.us/oauth/token', + userInfoUrl: 'https://api.zoom.us/v2/users/me', + scopes: [ + 'user:read:user', + 'meeting:write:meeting', + 'meeting:read:meeting', + 'meeting:read:list_meetings', + 'meeting:update:meeting', + 'meeting:delete:meeting', + 'meeting:read:invitation', + 'meeting:read:list_past_participants', + 'cloud_recording:read:list_user_recordings', + 'cloud_recording:read:list_recording_files', + 'cloud_recording:delete:recording_file', + ], + responseType: 'code', + accessType: 'offline', + authentication: 'basic', + prompt: 'consent', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/zoom`, + getUserInfo: async (tokens) => { + try { + logger.info('Fetching Zoom user profile') + + const response = await fetch('https://api.zoom.us/v2/users/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Failed to fetch Zoom user info', { + status: response.status, + statusText: response.statusText, + }) + throw new Error('Failed to fetch user info') + } + + const profile = await response.json() + + return { + id: profile.id, + name: + `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Zoom User', + email: profile.email || `${profile.id}@zoom.user`, + emailVerified: profile.verified === 1, + image: profile.pic_url || undefined, + createdAt: new Date(), + updatedAt: new Date(), + } + } catch (error) { + logger.error('Error in Zoom getUserInfo:', { error }) + return null + } + }, + }, ], }), // Include SSO plugin when enabled diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 476d4a5dac0..493e379e453 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -225,6 +225,8 @@ export const env = createEnv({ SHOPIFY_CLIENT_SECRET: z.string().optional(), // Shopify OAuth client secret BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret + ZOOM_CLIENT_ID: z.string().optional(), // Zoom OAuth client ID + ZOOM_CLIENT_SECRET: z.string().optional(), // Zoom OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 128d4a4f230..79912809009 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -584,18 +584,7 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'shopify', icon: (props) => ShopifyIcon(props), baseProviderIcon: (props) => ShopifyIcon(props), - scopes: [ - 'read_products', - 'write_products', - 'read_orders', - 'write_orders', - 'read_customers', - 'write_customers', - 'read_inventory', - 'write_inventory', - 'read_fulfillments', - 'write_fulfillments', - ], + scopes: ['write_products', 'write_orders', 'write_customers', 'write_inventory'], }, }, defaultService: 'shopify', @@ -844,11 +833,17 @@ export const OAUTH_PROVIDERS: Record = { icon: (props) => ZoomIcon(props), baseProviderIcon: (props) => ZoomIcon(props), scopes: [ - 'meeting:write:meeting:admin', - 'meeting:read:meeting:admin', - 'meeting:read:list_meetings:admin', - 'meeting:update:meeting:admin', - 'meeting:delete:meeting:admin', + 'user:read:user', + 'meeting:write:meeting', + 'meeting:read:meeting', + 'meeting:read:list_meetings', + 'meeting:update:meeting', + 'meeting:delete:meeting', + 'meeting:read:invitation', + 'meeting:read:list_past_participants', + 'cloud_recording:read:list_user_recordings', + 'cloud_recording:read:list_recording_files', + 'cloud_recording:delete:recording_file', ], }, }, @@ -1377,6 +1372,16 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'zoom': { + const { clientId, clientSecret } = getCredentials(env.ZOOM_CLIENT_ID, env.ZOOM_CLIENT_SECRET) + return { + tokenEndpoint: 'https://zoom.us/oauth/token', + clientId, + clientSecret, + useBasicAuth: true, + supportsRefreshTokenRotation: false, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 019fbf460af..df137b82268 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1242,9 +1242,13 @@ import { import { zoomCreateMeetingTool, zoomDeleteMeetingTool, + zoomDeleteRecordingTool, zoomGetMeetingInvitationTool, + zoomGetMeetingRecordingsTool, zoomGetMeetingTool, zoomListMeetingsTool, + zoomListPastParticipantsTool, + zoomListRecordingsTool, zoomUpdateMeetingTool, } from '@/tools/zoom' @@ -2381,4 +2385,8 @@ export const tools: Record = { zoom_update_meeting: zoomUpdateMeetingTool, zoom_delete_meeting: zoomDeleteMeetingTool, zoom_get_meeting_invitation: zoomGetMeetingInvitationTool, + zoom_list_recordings: zoomListRecordingsTool, + zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool, + zoom_delete_recording: zoomDeleteRecordingTool, + zoom_list_past_participants: zoomListPastParticipantsTool, } diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index e64c33e4d56..3d152cc4c46 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -34,6 +34,7 @@ export interface ToolResponse { export interface OAuthConfig { required: boolean // Whether this tool requires OAuth authentication provider: OAuthService // The service that needs to be authorized + requiredScopes?: string[] // Specific scopes this tool needs (for granular scope validation) } export interface ToolConfig

{ diff --git a/apps/sim/tools/zoom/create_meeting.ts b/apps/sim/tools/zoom/create_meeting.ts index 98fd3f3f4fa..2ec161549e2 100644 --- a/apps/sim/tools/zoom/create_meeting.ts +++ b/apps/sim/tools/zoom/create_meeting.ts @@ -11,6 +11,7 @@ export const zoomCreateMeetingTool: ToolConfig = { + id: 'zoom_delete_recording', + name: 'Zoom Delete Recording', + description: 'Delete cloud recordings for a Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + requiredScopes: ['cloud_recording:delete:recording_file'], + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID or meeting UUID', + }, + recordingId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Specific recording file ID to delete. If not provided, deletes all recordings.', + }, + action: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Delete action: "trash" (move to trash) or "delete" (permanently delete)', + }, + }, + + request: { + url: (params) => { + let baseUrl = `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}/recordings` + + if (params.recordingId) { + baseUrl += `/${encodeURIComponent(params.recordingId)}` + } + + const queryParams = new URLSearchParams() + if (params.action) { + queryParams.append('action', params.action) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { success: false }, + } + } + + // Zoom returns 204 No Content on successful deletion + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the recording was deleted successfully', + }, + }, +} diff --git a/apps/sim/tools/zoom/get_meeting.ts b/apps/sim/tools/zoom/get_meeting.ts index 9da9442302d..20b9d422baa 100644 --- a/apps/sim/tools/zoom/get_meeting.ts +++ b/apps/sim/tools/zoom/get_meeting.ts @@ -10,6 +10,7 @@ export const zoomGetMeetingTool: ToolConfig = { + id: 'zoom_get_meeting_recordings', + name: 'Zoom Get Meeting Recordings', + description: 'Get all recordings for a specific Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + requiredScopes: ['cloud_recording:read:list_recording_files'], + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The meeting ID or meeting UUID', + }, + includeFolderItems: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include items within a folder', + }, + ttl: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Time to live for download URLs in seconds (max 604800)', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/meetings/${encodeURIComponent(params.meetingId)}/recordings` + const queryParams = new URLSearchParams() + + if (params.includeFolderItems != null) { + queryParams.append('include_folder_items', String(params.includeFolderItems)) + } + if (params.ttl) { + queryParams.append('ttl', String(params.ttl)) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { recording: {} as any }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + recording: { + uuid: data.uuid, + id: data.id, + account_id: data.account_id, + host_id: data.host_id, + topic: data.topic, + type: data.type, + start_time: data.start_time, + duration: data.duration, + total_size: data.total_size, + recording_count: data.recording_count, + share_url: data.share_url, + recording_files: (data.recording_files || []).map((file: any) => ({ + id: file.id, + meeting_id: file.meeting_id, + recording_start: file.recording_start, + recording_end: file.recording_end, + file_type: file.file_type, + file_extension: file.file_extension, + file_size: file.file_size, + play_url: file.play_url, + download_url: file.download_url, + status: file.status, + recording_type: file.recording_type, + })), + }, + }, + } + }, + + outputs: { + recording: { + type: 'object', + description: 'The meeting recording with all files', + properties: { + uuid: { type: 'string', description: 'Meeting UUID' }, + id: { type: 'number', description: 'Meeting ID' }, + topic: { type: 'string', description: 'Meeting topic' }, + start_time: { type: 'string', description: 'Meeting start time' }, + duration: { type: 'number', description: 'Meeting duration in minutes' }, + total_size: { type: 'number', description: 'Total size of recordings in bytes' }, + share_url: { type: 'string', description: 'URL to share recordings' }, + recording_files: { type: 'array', description: 'List of recording files' }, + }, + }, + }, +} diff --git a/apps/sim/tools/zoom/index.ts b/apps/sim/tools/zoom/index.ts index 9ac76046a38..3da1cda3bc4 100644 --- a/apps/sim/tools/zoom/index.ts +++ b/apps/sim/tools/zoom/index.ts @@ -1,24 +1,39 @@ // Zoom tools exports export { zoomCreateMeetingTool } from './create_meeting' export { zoomDeleteMeetingTool } from './delete_meeting' +export { zoomDeleteRecordingTool } from './delete_recording' export { zoomGetMeetingTool } from './get_meeting' export { zoomGetMeetingInvitationTool } from './get_meeting_invitation' +export { zoomGetMeetingRecordingsTool } from './get_meeting_recordings' export { zoomListMeetingsTool } from './list_meetings' +export { zoomListPastParticipantsTool } from './list_past_participants' +export { zoomListRecordingsTool } from './list_recordings' // Type exports export type { ZoomCreateMeetingParams, ZoomCreateMeetingResponse, ZoomDeleteMeetingParams, ZoomDeleteMeetingResponse, + ZoomDeleteRecordingParams, + ZoomDeleteRecordingResponse, ZoomGetMeetingInvitationParams, ZoomGetMeetingInvitationResponse, ZoomGetMeetingParams, + ZoomGetMeetingRecordingsParams, + ZoomGetMeetingRecordingsResponse, ZoomGetMeetingResponse, ZoomListMeetingsParams, ZoomListMeetingsResponse, + ZoomListPastParticipantsParams, + ZoomListPastParticipantsResponse, + ZoomListRecordingsParams, + ZoomListRecordingsResponse, ZoomMeeting, ZoomMeetingSettings, ZoomMeetingType, + ZoomParticipant, + ZoomRecording, + ZoomRecordingFile, ZoomResponse, ZoomUpdateMeetingParams, ZoomUpdateMeetingResponse, diff --git a/apps/sim/tools/zoom/list_meetings.ts b/apps/sim/tools/zoom/list_meetings.ts index 30a863dfe21..db4eb8c105a 100644 --- a/apps/sim/tools/zoom/list_meetings.ts +++ b/apps/sim/tools/zoom/list_meetings.ts @@ -10,6 +10,7 @@ export const zoomListMeetingsTool: ToolConfig = { + id: 'zoom_list_past_participants', + name: 'Zoom List Past Participants', + description: 'List participants from a past Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + requiredScopes: ['meeting:read:list_past_participants'], + }, + + params: { + meetingId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The past meeting ID or UUID', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records per page (max 300)', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination to get next page of results', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/past_meetings/${encodeURIComponent(params.meetingId)}/participants` + const queryParams = new URLSearchParams() + + if (params.pageSize) { + queryParams.append('page_size', String(params.pageSize)) + } + if (params.nextPageToken) { + queryParams.append('next_page_token', params.nextPageToken) + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { + participants: [], + pageInfo: { + pageSize: 0, + totalRecords: 0, + }, + }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + participants: (data.participants || []).map((participant: any) => ({ + id: participant.id, + user_id: participant.user_id, + name: participant.name, + user_email: participant.user_email, + join_time: participant.join_time, + leave_time: participant.leave_time, + duration: participant.duration, + attentiveness_score: participant.attentiveness_score, + failover: participant.failover, + status: participant.status, + })), + pageInfo: { + pageSize: data.page_size || 0, + totalRecords: data.total_records || 0, + nextPageToken: data.next_page_token, + }, + }, + } + }, + + outputs: { + participants: { + type: 'array', + description: 'List of meeting participants', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + properties: { + pageSize: { type: 'number', description: 'Number of records per page' }, + totalRecords: { type: 'number', description: 'Total number of records' }, + nextPageToken: { type: 'string', description: 'Token for next page' }, + }, + }, + }, +} diff --git a/apps/sim/tools/zoom/list_recordings.ts b/apps/sim/tools/zoom/list_recordings.ts new file mode 100644 index 00000000000..a0d90f3f722 --- /dev/null +++ b/apps/sim/tools/zoom/list_recordings.ts @@ -0,0 +1,159 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomListRecordingsParams, ZoomListRecordingsResponse } from '@/tools/zoom/types' + +export const zoomListRecordingsTool: ToolConfig< + ZoomListRecordingsParams, + ZoomListRecordingsResponse +> = { + id: 'zoom_list_recordings', + name: 'Zoom List Recordings', + description: 'List all cloud recordings for a Zoom user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + requiredScopes: ['cloud_recording:read:list_user_recordings'], + }, + + params: { + userId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The user ID or email address. Use "me" for the authenticated user.', + }, + from: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Start date in yyyy-mm-dd format (within last 6 months)', + }, + to: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End date in yyyy-mm-dd format', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of records per page (max 300)', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination to get next page of results', + }, + trash: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Set to true to list recordings from trash', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.zoom.us/v2/users/${encodeURIComponent(params.userId)}/recordings` + const queryParams = new URLSearchParams() + + if (params.from) { + queryParams.append('from', params.from) + } + if (params.to) { + queryParams.append('to', params.to) + } + if (params.pageSize) { + queryParams.append('page_size', String(params.pageSize)) + } + if (params.nextPageToken) { + queryParams.append('next_page_token', params.nextPageToken) + } + if (params.trash) { + queryParams.append('trash', 'true') + } + + const queryString = queryParams.toString() + return queryString ? `${baseUrl}?${queryString}` : baseUrl + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Zoom API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: errorData.message || `Zoom API error: ${response.status} ${response.statusText}`, + output: { + recordings: [], + pageInfo: { + from: '', + to: '', + pageSize: 0, + totalRecords: 0, + }, + }, + } + } + + const data = await response.json() + + return { + success: true, + output: { + recordings: (data.meetings || []).map((recording: any) => ({ + uuid: recording.uuid, + id: recording.id, + account_id: recording.account_id, + host_id: recording.host_id, + topic: recording.topic, + type: recording.type, + start_time: recording.start_time, + duration: recording.duration, + total_size: recording.total_size, + recording_count: recording.recording_count, + share_url: recording.share_url, + recording_files: recording.recording_files || [], + })), + pageInfo: { + from: data.from || '', + to: data.to || '', + pageSize: data.page_size || 0, + totalRecords: data.total_records || 0, + nextPageToken: data.next_page_token, + }, + }, + } + }, + + outputs: { + recordings: { + type: 'array', + description: 'List of recordings', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + properties: { + from: { type: 'string', description: 'Start date of query range' }, + to: { type: 'string', description: 'End date of query range' }, + pageSize: { type: 'number', description: 'Number of records per page' }, + totalRecords: { type: 'number', description: 'Total number of records' }, + nextPageToken: { type: 'string', description: 'Token for next page' }, + }, + }, + }, +} diff --git a/apps/sim/tools/zoom/types.ts b/apps/sim/tools/zoom/types.ts index ab9ddaff4e6..6454644717f 100644 --- a/apps/sim/tools/zoom/types.ts +++ b/apps/sim/tools/zoom/types.ts @@ -176,6 +176,118 @@ export interface ZoomGetMeetingInvitationResponse extends ToolResponse { } } +// Recording types +export interface ZoomRecordingFile { + id: string + meeting_id: string + recording_start: string + recording_end: string + file_type: string + file_extension: string + file_size: number + play_url?: string + download_url?: string + status: string + recording_type: string +} + +export interface ZoomRecording { + uuid: string + id: number + account_id: string + host_id: string + topic: string + type: number + start_time: string + duration: number + total_size: number + recording_count: number + share_url?: string + recording_files: ZoomRecordingFile[] +} + +// List Recordings tool types +export interface ZoomListRecordingsParams extends ZoomBaseParams { + userId: string + from?: string + to?: string + pageSize?: number + nextPageToken?: string + trash?: boolean + trashType?: 'meeting_recordings' | 'recording_file' +} + +export interface ZoomListRecordingsResponse extends ToolResponse { + output: { + recordings: ZoomRecording[] + pageInfo: { + from: string + to: string + pageSize: number + totalRecords: number + nextPageToken?: string + } + } +} + +// Get Meeting Recordings tool types +export interface ZoomGetMeetingRecordingsParams extends ZoomBaseParams { + meetingId: string + includeFolderItems?: boolean + ttl?: number +} + +export interface ZoomGetMeetingRecordingsResponse extends ToolResponse { + output: { + recording: ZoomRecording + } +} + +// Delete Recording tool types +export interface ZoomDeleteRecordingParams extends ZoomBaseParams { + meetingId: string + recordingId?: string + action?: 'trash' | 'delete' +} + +export interface ZoomDeleteRecordingResponse extends ToolResponse { + output: { + success: boolean + } +} + +// Participant types +export interface ZoomParticipant { + id: string + user_id?: string + name: string + user_email?: string + join_time: string + leave_time?: string + duration: number + attentiveness_score?: string + failover?: boolean + status?: string +} + +// List Past Participants tool types +export interface ZoomListPastParticipantsParams extends ZoomBaseParams { + meetingId: string + pageSize?: number + nextPageToken?: string +} + +export interface ZoomListPastParticipantsResponse extends ToolResponse { + output: { + participants: ZoomParticipant[] + pageInfo: { + pageSize: number + totalRecords: number + nextPageToken?: string + } + } +} + // Combined response type for block export type ZoomResponse = | ZoomCreateMeetingResponse @@ -184,3 +296,7 @@ export type ZoomResponse = | ZoomUpdateMeetingResponse | ZoomDeleteMeetingResponse | ZoomGetMeetingInvitationResponse + | ZoomListRecordingsResponse + | ZoomGetMeetingRecordingsResponse + | ZoomDeleteRecordingResponse + | ZoomListPastParticipantsResponse diff --git a/apps/sim/tools/zoom/update_meeting.ts b/apps/sim/tools/zoom/update_meeting.ts index f676cd84493..18748bbe71e 100644 --- a/apps/sim/tools/zoom/update_meeting.ts +++ b/apps/sim/tools/zoom/update_meeting.ts @@ -11,6 +11,7 @@ export const zoomUpdateMeetingTool: ToolConfig Date: Wed, 3 Dec 2025 16:24:53 -0800 Subject: [PATCH 07/11] remove unused box creds --- apps/sim/lib/core/config/env.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 493e379e453..2b9b6e741f4 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -223,8 +223,6 @@ export const env = createEnv({ LINKEDIN_CLIENT_SECRET: z.string().optional(), // LinkedIn OAuth client secret SHOPIFY_CLIENT_ID: z.string().optional(), // Shopify OAuth client ID SHOPIFY_CLIENT_SECRET: z.string().optional(), // Shopify OAuth client secret - BOX_CLIENT_ID: z.string().optional(), // Box OAuth client ID - BOX_CLIENT_SECRET: z.string().optional(), // Box OAuth client secret ZOOM_CLIENT_ID: z.string().optional(), // Zoom OAuth client ID ZOOM_CLIENT_SECRET: z.string().optional(), // Zoom OAuth client secret From 573a052930bf6dea16864a6eb48a42c0b6b0c2d9 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 19:18:42 -0800 Subject: [PATCH 08/11] finished wordpress, shopify, kalshi --- apps/docs/content/docs/en/tools/kalshi.mdx | 37 +-- apps/docs/content/docs/en/tools/shopify.mdx | 101 +++++++- apps/docs/content/docs/en/tools/wordpress.mdx | 164 ++++-------- .../api/auth/oauth2/callback/shopify/route.ts | 71 +++++- .../api/auth/oauth2/shopify/store/route.ts | 21 +- .../app/api/auth/shopify/authorize/route.ts | 60 +++-- .../components/oauth-required-modal.tsx | 18 +- .../components/short-input/short-input.tsx | 8 + .../panel/components/editor/editor.tsx | 1 + apps/sim/blocks/blocks/elasticsearch.ts | 28 +- apps/sim/blocks/blocks/kalshi.ts | 20 -- apps/sim/blocks/blocks/shopify.ts | 192 +++++++++++++- apps/sim/blocks/blocks/wordpress.ts | 63 ++--- apps/sim/hooks/queries/oauth-connections.ts | 7 + apps/sim/lib/auth/auth.ts | 48 ++++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/oauth/oauth.ts | 44 +++- apps/sim/tools/kalshi/get_series.ts | 101 -------- apps/sim/tools/kalshi/index.ts | 1 - apps/sim/tools/kalshi/types.ts | 74 +++++- apps/sim/tools/polymarket/get_tick_size.ts | 6 +- apps/sim/tools/registry.ts | 12 +- apps/sim/tools/shopify/cancel_order.ts | 6 +- apps/sim/tools/shopify/create_customer.ts | 3 +- apps/sim/tools/shopify/create_fulfillment.ts | 240 ++++++++++++++++++ apps/sim/tools/shopify/create_product.ts | 1 - apps/sim/tools/shopify/get_collection.ts | 224 ++++++++++++++++ apps/sim/tools/shopify/get_customer.ts | 3 +- apps/sim/tools/shopify/get_inventory_level.ts | 29 ++- apps/sim/tools/shopify/get_product.ts | 4 - apps/sim/tools/shopify/index.ts | 10 +- apps/sim/tools/shopify/list_collections.ts | 187 ++++++++++++++ apps/sim/tools/shopify/list_customers.ts | 5 +- .../sim/tools/shopify/list_inventory_items.ts | 235 +++++++++++++++++ apps/sim/tools/shopify/list_locations.ts | 166 ++++++++++++ apps/sim/tools/shopify/list_orders.ts | 18 +- apps/sim/tools/shopify/types.ts | 12 +- apps/sim/tools/shopify/update_customer.ts | 3 +- apps/sim/tools/wordpress/create_category.ts | 48 ++-- apps/sim/tools/wordpress/create_comment.ts | 48 ++-- apps/sim/tools/wordpress/create_page.ts | 48 ++-- apps/sim/tools/wordpress/create_post.ts | 48 ++-- apps/sim/tools/wordpress/create_tag.ts | 48 ++-- apps/sim/tools/wordpress/delete_comment.ts | 46 ++-- apps/sim/tools/wordpress/delete_media.ts | 46 ++-- apps/sim/tools/wordpress/delete_page.ts | 46 ++-- apps/sim/tools/wordpress/delete_post.ts | 46 ++-- apps/sim/tools/wordpress/get_current_user.ts | 48 ++-- apps/sim/tools/wordpress/get_media.ts | 48 ++-- apps/sim/tools/wordpress/get_page.ts | 48 ++-- apps/sim/tools/wordpress/get_post.ts | 48 ++-- apps/sim/tools/wordpress/get_user.ts | 48 ++-- apps/sim/tools/wordpress/list_categories.ts | 67 ++--- apps/sim/tools/wordpress/list_comments.ts | 46 ++-- apps/sim/tools/wordpress/list_media.ts | 46 ++-- apps/sim/tools/wordpress/list_pages.ts | 46 ++-- apps/sim/tools/wordpress/list_posts.ts | 46 ++-- apps/sim/tools/wordpress/list_tags.ts | 60 ++--- apps/sim/tools/wordpress/list_users.ts | 53 ++-- apps/sim/tools/wordpress/search_content.ts | 46 ++-- apps/sim/tools/wordpress/types.ts | 17 +- apps/sim/tools/wordpress/update_comment.ts | 48 ++-- apps/sim/tools/wordpress/update_page.ts | 48 ++-- apps/sim/tools/wordpress/update_post.ts | 48 ++-- apps/sim/tools/wordpress/upload_media.ts | 41 ++- 65 files changed, 2323 insertions(+), 1178 deletions(-) delete mode 100644 apps/sim/tools/kalshi/get_series.ts create mode 100644 apps/sim/tools/shopify/create_fulfillment.ts create mode 100644 apps/sim/tools/shopify/get_collection.ts create mode 100644 apps/sim/tools/shopify/list_collections.ts create mode 100644 apps/sim/tools/shopify/list_inventory_items.ts create mode 100644 apps/sim/tools/shopify/list_locations.ts diff --git a/apps/docs/content/docs/en/tools/kalshi.mdx b/apps/docs/content/docs/en/tools/kalshi.mdx index 0003dcea0d6..2aa55a8d886 100644 --- a/apps/docs/content/docs/en/tools/kalshi.mdx +++ b/apps/docs/content/docs/en/tools/kalshi.mdx @@ -11,18 +11,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" /> {/* MANUAL-CONTENT-START:intro */} -[Kalshi](https://kalshi.com) is a federally regulated exchange that enables users to trade directly on the outcomes of future events, known as prediction markets. Kalshi offers an open API, giving developers access to real-time market and event data, as well as account and trading information for automated or data-driven workflows. +[Kalshi](https://kalshi.com) is a federally regulated exchange where users can trade directly on the outcomes of future events—prediction markets. Kalshi’s robust API and Sim integration enable agents and workflows to programmatically access all aspects of the platform, supporting everything from research and analytics to automated trading and monitoring. -With Kalshi's API and Sim integration, you can enable agents to retrieve up-to-date prediction market information, explore open events, check account balances, manage orders, and track positions—all programmatically. This provides powerful capabilities for research, portfolio management, and creating intelligent workflows based on real-world event probabilities and outcomes. +With Kalshi’s integration in Sim, you can: -Key features of the Kalshi integration include: +- **Market & Event Data:** Search, filter, and retrieve real-time and historical data for markets and events; fetch granular details on market status, series, event groupings, and more. +- **Account & Balance Management:** Access account balances, available funds, and monitor real-time open positions. +- **Order & Trade Management:** Place new orders, cancel existing ones, view open orders, retrieve a live orderbook, and access complete trade histories. +- **Execution Analysis:** Fetch recent trades, historical fills, and candlestick data for backtesting or market structure research. +- **Monitoring:** Check exchange-wide or series-level status, receive real-time updates about market changes or trading halts, and automate responses. +- **Automation Ready:** Build end-to-end automated agents and dashboards that consume, analyze, and trade on real-world event probabilities. -- **Market Listing & Filtering:** Retrieve lists of markets or individual market details, including filtering by status, series, or event. -- **Event Data:** Explore and filter real-world event listings and retrieve detailed event information. -- **Account and Trading:** Access user account balances, open positions, and manage order flow. -- **Automation Ready:** Build intelligent agents that leverage real-time prediction market data for automated actions and insights. - -By using the documented API tools, you can seamlessly incorporate Kalshi's rich event and financial market data into your AI workflows, dashboards, and trading automations. +By using these unified tools and endpoints, you can seamlessly incorporate Kalshi’s prediction markets, live trading capabilities, and deep event data into your AI-powered applications, dashboards, and workflows—enabling sophisticated, automated decision-making tied to real-world outcomes. {/* MANUAL-CONTENT-END */} @@ -259,25 +259,6 @@ Retrieve your portfolio | `success` | boolean | Operation success status | | `output` | object | Fills data and metadata | -### `kalshi_get_series` - -Retrieve a list of market series templates from Kalshi - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `status` | string | No | Filter by status | -| `limit` | string | No | Number of results \(1-1000, default: 100\) | -| `cursor` | string | No | Pagination cursor for next page | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Series data and metadata | - ### `kalshi_get_series_by_ticker` Retrieve details of a specific market series by ticker diff --git a/apps/docs/content/docs/en/tools/shopify.mdx b/apps/docs/content/docs/en/tools/shopify.mdx index d29d6d2e9d1..327e935aee0 100644 --- a/apps/docs/content/docs/en/tools/shopify.mdx +++ b/apps/docs/content/docs/en/tools/shopify.mdx @@ -157,6 +157,7 @@ List orders from your Shopify store with optional filtering | --------- | ---- | -------- | ----------- | | `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | | `first` | number | No | Number of orders to return \(default: 50, max: 250\) | +| `status` | string | No | Filter by order status \(open, closed, cancelled, any\) | | `query` | string | No | Search query to filter orders \(e.g., "financial_status:paid" or "fulfillment_status:unfulfilled" or "email:customer@example.com"\) | #### Output @@ -196,7 +197,7 @@ Cancel an order in your Shopify store | --------- | ---- | -------- | ----------- | | `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | | `orderId` | string | Yes | Order ID to cancel \(gid://shopify/Order/123456789\) | -| `reason` | string | Yes | Cancellation reason \(CUSTOMER, FRAUD, INVENTORY, DECLINED, OTHER\) | +| `reason` | string | Yes | Cancellation reason \(CUSTOMER, DECLINED, FRAUD, INVENTORY, STAFF, OTHER\) | | `notifyCustomer` | boolean | No | Whether to notify the customer about the cancellation | | `refund` | boolean | No | Whether to refund the order | | `restock` | boolean | No | Whether to restock the inventory | @@ -258,7 +259,7 @@ List customers from your Shopify store with optional filtering | --------- | ---- | -------- | ----------- | | `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | | `first` | number | No | Number of customers to return \(default: 50, max: 250\) | -| `query` | string | No | Search query to filter customers \(e.g., "email:*@example.com" or "tag:vip" or "orders_count:>5"\) | +| `query` | string | No | Search query to filter customers \(e.g., "first_name:John" or "last_name:Smith" or "email:*@gmail.com" or "tag:vip"\) | #### Output @@ -307,6 +308,25 @@ Delete a customer from your Shopify store | --------- | ---- | ----------- | | `deletedId` | string | The ID of the deleted customer | +### `shopify_list_inventory_items` + +List inventory items from your Shopify store. Use this to find inventory item IDs by SKU. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of inventory items to return \(default: 50, max: 250\) | +| `query` | string | No | Search query to filter inventory items \(e.g., "sku:ABC123"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inventoryItems` | array | List of inventory items with their IDs, SKUs, and stock levels | +| `pageInfo` | object | Pagination information | + ### `shopify_get_inventory_level` Get inventory level for a product variant at a specific location @@ -344,6 +364,83 @@ Adjust inventory quantity for a product variant at a specific location | --------- | ---- | ----------- | | `inventoryLevel` | object | The inventory adjustment result | +### `shopify_list_locations` + +List inventory locations from your Shopify store. Use this to find location IDs needed for inventory operations. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of locations to return \(default: 50, max: 250\) | +| `includeInactive` | boolean | No | Whether to include deactivated locations \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `locations` | array | List of locations with their IDs, names, and addresses | +| `pageInfo` | object | Pagination information | + +### `shopify_create_fulfillment` + +Create a fulfillment to mark order items as shipped. Requires a fulfillment order ID (get this from the order details). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `fulfillmentOrderId` | string | Yes | The fulfillment order ID \(e.g., gid://shopify/FulfillmentOrder/123456789\) | +| `trackingNumber` | string | No | Tracking number for the shipment | +| `trackingCompany` | string | No | Shipping carrier name \(e.g., UPS, FedEx, USPS, DHL\) | +| `trackingUrl` | string | No | URL to track the shipment | +| `notifyCustomer` | boolean | No | Whether to send a shipping confirmation email to the customer \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fulfillment` | object | The created fulfillment with tracking info and fulfilled items | + +### `shopify_list_collections` + +List product collections from your Shopify store. Filter by title, type (custom/smart), or handle. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `first` | number | No | Number of collections to return \(default: 50, max: 250\) | +| `query` | string | No | Search query to filter collections \(e.g., "title:Summer" or "collection_type:smart"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `collections` | array | List of collections with their IDs, titles, and product counts | +| `pageInfo` | object | Pagination information | + +### `shopify_get_collection` + +Get a specific collection by ID, including its products. Use this to retrieve products within a collection. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `shopDomain` | string | Yes | Your Shopify store domain \(e.g., mystore.myshopify.com\) | +| `collectionId` | string | Yes | The collection ID \(e.g., gid://shopify/Collection/123456789\) | +| `productsFirst` | number | No | Number of products to return from this collection \(default: 50, max: 250\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `collection` | object | The collection details including its products | + ## Notes diff --git a/apps/docs/content/docs/en/tools/wordpress.mdx b/apps/docs/content/docs/en/tools/wordpress.mdx index 288f279aeac..214b7d0c15a 100644 --- a/apps/docs/content/docs/en/tools/wordpress.mdx +++ b/apps/docs/content/docs/en/tools/wordpress.mdx @@ -21,7 +21,7 @@ By connecting WordPress to your automations, Sim empowers your agents to streaml ## Usage Instructions -Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports self-hosted WordPress sites using Application Passwords authentication. +Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports WordPress.com sites via OAuth and self-hosted WordPress sites using Application Passwords authentication. @@ -29,15 +29,13 @@ Integrate with WordPress to create, update, and manage posts, pages, media, comm ### `wordpress_create_post` -Create a new blog post in WordPress +Create a new blog post in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `title` | string | Yes | Post title | | `content` | string | No | Post content \(HTML or plain text\) | | `status` | string | No | Post status: publish, draft, pending, private, or future | @@ -55,15 +53,13 @@ Create a new blog post in WordPress ### `wordpress_update_post` -Update an existing blog post in WordPress +Update an existing blog post in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `postId` | number | Yes | The ID of the post to update | | `title` | string | No | Post title | | `content` | string | No | Post content \(HTML or plain text\) | @@ -82,15 +78,13 @@ Update an existing blog post in WordPress ### `wordpress_delete_post` -Delete a blog post from WordPress +Delete a blog post from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `postId` | number | Yes | The ID of the post to delete | | `force` | boolean | No | Bypass trash and force delete permanently | @@ -103,15 +97,13 @@ Delete a blog post from WordPress ### `wordpress_get_post` -Get a single blog post from WordPress by ID +Get a single blog post from WordPress.com by ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `postId` | number | Yes | The ID of the post to retrieve | #### Output @@ -122,15 +114,13 @@ Get a single blog post from WordPress by ID ### `wordpress_list_posts` -List blog posts from WordPress with optional filters +List blog posts from WordPress.com with optional filters #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of posts per page \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `status` | string | No | Post status filter: publish, draft, pending, private | @@ -149,15 +139,13 @@ List blog posts from WordPress with optional filters ### `wordpress_create_page` -Create a new page in WordPress +Create a new page in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `title` | string | Yes | Page title | | `content` | string | No | Page content \(HTML or plain text\) | | `status` | string | No | Page status: publish, draft, pending, private | @@ -175,15 +163,13 @@ Create a new page in WordPress ### `wordpress_update_page` -Update an existing page in WordPress +Update an existing page in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `pageId` | number | Yes | The ID of the page to update | | `title` | string | No | Page title | | `content` | string | No | Page content \(HTML or plain text\) | @@ -202,15 +188,13 @@ Update an existing page in WordPress ### `wordpress_delete_page` -Delete a page from WordPress +Delete a page from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `pageId` | number | Yes | The ID of the page to delete | | `force` | boolean | No | Bypass trash and force delete permanently | @@ -223,15 +207,13 @@ Delete a page from WordPress ### `wordpress_get_page` -Get a single page from WordPress by ID +Get a single page from WordPress.com by ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `pageId` | number | Yes | The ID of the page to retrieve | #### Output @@ -242,15 +224,13 @@ Get a single page from WordPress by ID ### `wordpress_list_pages` -List pages from WordPress with optional filters +List pages from WordPress.com with optional filters #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of pages per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `status` | string | No | Page status filter: publish, draft, pending, private | @@ -267,15 +247,13 @@ List pages from WordPress with optional filters ### `wordpress_upload_media` -Upload a media file (image, video, document) to WordPress +Upload a media file (image, video, document) to WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `file` | string | Yes | Base64 encoded file data or URL to fetch file from | | `filename` | string | Yes | Filename with extension \(e.g., image.jpg\) | | `title` | string | No | Media title | @@ -291,15 +269,13 @@ Upload a media file (image, video, document) to WordPress ### `wordpress_get_media` -Get a single media item from WordPress by ID +Get a single media item from WordPress.com by ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `mediaId` | number | Yes | The ID of the media item to retrieve | #### Output @@ -310,15 +286,13 @@ Get a single media item from WordPress by ID ### `wordpress_list_media` -List media items from the WordPress media library +List media items from the WordPress.com media library #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of media items per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `search` | string | No | Search term to filter media | @@ -335,15 +309,13 @@ List media items from the WordPress media library ### `wordpress_delete_media` -Delete a media item from WordPress +Delete a media item from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `mediaId` | number | Yes | The ID of the media item to delete | | `force` | boolean | No | Force delete \(media has no trash, so deletion is permanent\) | @@ -356,15 +328,13 @@ Delete a media item from WordPress ### `wordpress_create_comment` -Create a new comment on a WordPress post +Create a new comment on a WordPress.com post #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `postId` | number | Yes | The ID of the post to comment on | | `content` | string | Yes | Comment content | | `parent` | number | No | Parent comment ID for replies | @@ -380,15 +350,13 @@ Create a new comment on a WordPress post ### `wordpress_list_comments` -List comments from WordPress with optional filters +List comments from WordPress.com with optional filters #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of comments per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `postId` | number | No | Filter by post ID | @@ -405,15 +373,13 @@ List comments from WordPress with optional filters ### `wordpress_update_comment` -Update a comment in WordPress (content or status) +Update a comment in WordPress.com (content or status) #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `commentId` | number | Yes | The ID of the comment to update | | `content` | string | No | Updated comment content | | `status` | string | No | Comment status: approved, hold, spam, trash | @@ -426,15 +392,13 @@ Update a comment in WordPress (content or status) ### `wordpress_delete_comment` -Delete a comment from WordPress +Delete a comment from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `commentId` | number | Yes | The ID of the comment to delete | | `force` | boolean | No | Bypass trash and force delete permanently | @@ -447,15 +411,13 @@ Delete a comment from WordPress ### `wordpress_create_category` -Create a new category in WordPress +Create a new category in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `name` | string | Yes | Category name | | `description` | string | No | Category description | | `parent` | number | No | Parent category ID for hierarchical categories | @@ -469,21 +431,16 @@ Create a new category in WordPress ### `wordpress_list_categories` -List categories from WordPress +List categories from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of categories per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `search` | string | No | Search term to filter categories | -| `parent` | number | No | Filter by parent category ID | -| `hideEmpty` | boolean | No | Hide categories with no posts | -| `orderBy` | string | No | Order by field: id, name, slug, count | | `order` | string | No | Order direction: asc or desc | #### Output @@ -494,15 +451,13 @@ List categories from WordPress ### `wordpress_create_tag` -Create a new tag in WordPress +Create a new tag in WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `name` | string | Yes | Tag name | | `description` | string | No | Tag description | | `slug` | string | No | URL slug for the tag | @@ -515,20 +470,16 @@ Create a new tag in WordPress ### `wordpress_list_tags` -List tags from WordPress +List tags from WordPress.com #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of tags per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `search` | string | No | Search term to filter tags | -| `hideEmpty` | boolean | No | Hide tags with no posts | -| `orderBy` | string | No | Order by field: id, name, slug, count | | `order` | string | No | Order direction: asc or desc | #### Output @@ -539,15 +490,13 @@ List tags from WordPress ### `wordpress_get_current_user` -Get information about the currently authenticated WordPress user +Get information about the currently authenticated WordPress.com user #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | #### Output @@ -557,20 +506,17 @@ Get information about the currently authenticated WordPress user ### `wordpress_list_users` -List users from WordPress (requires admin privileges) +List users from WordPress.com (requires admin privileges) #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `perPage` | number | No | Number of users per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | | `search` | string | No | Search term to filter users | | `roles` | string | No | Comma-separated role names to filter by | -| `orderBy` | string | No | Order by field: id, name, slug, email, registered_date | | `order` | string | No | Order direction: asc or desc | #### Output @@ -581,15 +527,13 @@ List users from WordPress (requires admin privileges) ### `wordpress_get_user` -Get a specific user from WordPress by ID +Get a specific user from WordPress.com by ID #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `userId` | number | Yes | The ID of the user to retrieve | #### Output @@ -600,15 +544,13 @@ Get a specific user from WordPress by ID ### `wordpress_search_content` -Search across all content types in WordPress (posts, pages, media) +Search across all content types in WordPress.com (posts, pages, media) #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `siteUrl` | string | Yes | WordPress site URL \(e.g., https://example.com\) | -| `username` | string | Yes | WordPress username | -| `applicationPassword` | string | Yes | WordPress Application Password | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | | `query` | string | Yes | Search query | | `perPage` | number | No | Number of results per request \(default: 10, max: 100\) | | `page` | number | No | Page number for pagination | diff --git a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts index 21e33ccacb2..3952de90fa1 100644 --- a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts +++ b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts @@ -1,3 +1,4 @@ +import crypto from 'crypto' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { env } from '@/lib/core/config/env' @@ -8,6 +9,44 @@ const logger = createLogger('ShopifyCallback') export const dynamic = 'force-dynamic' +// Shopify shop domain must match this pattern +const SHOP_DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/ + +/** + * Validates the HMAC signature from Shopify to ensure the request is authentic + * @see https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens + */ +function validateHmac(searchParams: URLSearchParams, clientSecret: string): boolean { + const hmac = searchParams.get('hmac') + if (!hmac) { + return false + } + + // Create a copy of params without hmac for validation + const params: Record = {} + searchParams.forEach((value, key) => { + if (key !== 'hmac') { + params[key] = value + } + }) + + // Sort parameters alphabetically and create query string + const message = Object.keys(params) + .sort() + .map((key) => `${key}=${params[key]}`) + .join('&') + + // Generate HMAC using SHA256 + const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex') + + // Use timing-safe comparison to prevent timing attacks + try { + return crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(generatedHmac, 'hex')) + } catch { + return false + } +} + export async function GET(request: NextRequest) { const baseUrl = getBaseUrl() @@ -22,10 +61,25 @@ export async function GET(request: NextRequest) { const state = searchParams.get('state') const shop = searchParams.get('shop') - // Verify state to prevent CSRF + // Get stored values from cookies const storedState = request.cookies.get('shopify_oauth_state')?.value const storedShop = request.cookies.get('shopify_shop_domain')?.value + const clientId = env.SHOPIFY_CLIENT_ID + const clientSecret = env.SHOPIFY_CLIENT_SECRET + + if (!clientId || !clientSecret) { + logger.error('Shopify credentials not configured') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_config_error`) + } + + // Validate HMAC signature from Shopify + if (!validateHmac(searchParams, clientSecret)) { + logger.error('HMAC validation failed in Shopify OAuth callback') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_hmac_invalid`) + } + + // Validate state to prevent CSRF if (!state || state !== storedState) { logger.error('State mismatch in Shopify OAuth callback') return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_state_mismatch`) @@ -43,12 +97,10 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_shop`) } - const clientId = env.SHOPIFY_CLIENT_ID - const clientSecret = env.SHOPIFY_CLIENT_SECRET - - if (!clientId || !clientSecret) { - logger.error('Shopify credentials not configured') - return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_config_error`) + // Validate shop domain format to prevent injection attacks + if (!SHOP_DOMAIN_REGEX.test(shopDomain)) { + logger.error('Invalid shop domain format:', { shopDomain }) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_shop`) } // Exchange code for access token @@ -77,6 +129,11 @@ export async function GET(request: NextRequest) { const accessToken = tokenData.access_token const scope = tokenData.scope + logger.info('Shopify token exchange successful:', { + hasAccessToken: !!accessToken, + scope: scope, + }) + if (!accessToken) { logger.error('No access token in response') return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_token`) diff --git a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts index 5e1f3a1eb33..84f4ad06ee9 100644 --- a/apps/sim/app/api/auth/oauth2/shopify/store/route.ts +++ b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts @@ -1,10 +1,10 @@ +import { db } from '@sim/db' +import { account } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { getBaseUrl } from '@/lib/core/utils/urls' import { createLogger } from '@/lib/logs/console/logger' -import { db } from '@/../../packages/db' -import { account } from '@/../../packages/db/schema' const logger = createLogger('ShopifyStore') @@ -20,7 +20,6 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`) } - // Get token data from cookies (set by callback) const accessToken = request.cookies.get('shopify_pending_token')?.value const shopDomain = request.cookies.get('shopify_pending_shop')?.value const scope = request.cookies.get('shopify_pending_scope')?.value @@ -30,7 +29,6 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_missing_data`) } - // Validate the token by making a simple API call const shopResponse = await fetch(`https://${shopDomain}/admin/api/2024-10/shop.json`, { headers: { 'X-Shopify-Access-Token': accessToken, @@ -50,22 +48,17 @@ export async function GET(request: NextRequest) { const shopData = await shopResponse.json() const shopInfo = shopData.shop - // Check if account already exists for this user and provider const existing = await db.query.account.findFirst({ where: and(eq(account.userId, session.user.id), eq(account.providerId, 'shopify')), }) const now = new Date() - // Store the shop domain in the accessTokenExpiresAt field as metadata - // Since Shopify tokens don't expire, we use this field to store the shop domain - // The actual shop domain is needed for API calls const accountData = { accessToken: accessToken, accountId: shopInfo.id?.toString() || shopDomain, scope: scope || '', updatedAt: now, - // Store shop domain in idToken field (repurposed for shop domain since Shopify doesn't use idToken) idToken: shopDomain, } @@ -83,11 +76,17 @@ export async function GET(request: NextRequest) { logger.info('Created new Shopify account for user', { userId: session.user.id }) } - // Clear the pending cookies - const response = NextResponse.redirect(`${baseUrl}/workspace?shopify_connected=true`) + const returnUrl = request.cookies.get('shopify_return_url')?.value + + const redirectUrl = returnUrl || `${baseUrl}/workspace` + const finalUrl = new URL(redirectUrl) + finalUrl.searchParams.set('shopify_connected', 'true') + + const response = NextResponse.redirect(finalUrl.toString()) response.cookies.delete('shopify_pending_token') response.cookies.delete('shopify_pending_shop') response.cookies.delete('shopify_pending_scope') + response.cookies.delete('shopify_return_url') return response } catch (error) { diff --git a/apps/sim/app/api/auth/shopify/authorize/route.ts b/apps/sim/app/api/auth/shopify/authorize/route.ts index e0e8f2878b9..aa06b2c7b4a 100644 --- a/apps/sim/app/api/auth/shopify/authorize/route.ts +++ b/apps/sim/app/api/auth/shopify/authorize/route.ts @@ -8,13 +8,13 @@ const logger = createLogger('ShopifyAuthorize') export const dynamic = 'force-dynamic' -// Shopify OAuth scopes needed for the integration -// Note: In Shopify, write scopes imply read access const SHOPIFY_SCOPES = [ 'write_products', 'write_orders', 'write_customers', 'write_inventory', + 'read_locations', + 'write_merchant_managed_fulfillment_orders', ].join(',') export async function GET(request: NextRequest) { @@ -31,11 +31,11 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Shopify client ID not configured' }, { status: 500 }) } - // Get shop domain from query parameter const shopDomain = request.nextUrl.searchParams.get('shop') + const returnUrl = request.nextUrl.searchParams.get('returnUrl') if (!shopDomain) { - // Return a page that asks for the shop domain + const returnUrlParam = returnUrl ? encodeURIComponent(returnUrl) : '' return new NextResponse( ` @@ -123,6 +123,7 @@ export async function GET(request: NextRequest) {

@@ -147,7 +152,6 @@ export async function GET(request: NextRequest) { ) } - // Validate and clean shop domain let cleanShop = shopDomain.toLowerCase().trim() cleanShop = cleanShop.replace('https://', '').replace('http://', '') if (!cleanShop.endsWith('.myshopify.com')) { @@ -157,38 +161,52 @@ export async function GET(request: NextRequest) { const baseUrl = getBaseUrl() const redirectUri = `${baseUrl}/api/auth/oauth2/callback/shopify` - // Generate a random state for CSRF protection const state = crypto.randomUUID() - // Store state in a cookie for verification - const response = NextResponse.redirect( + const oauthUrl = `https://${cleanShop}/admin/oauth/authorize?` + - new URLSearchParams({ - client_id: clientId, - scope: SHOPIFY_SCOPES, - redirect_uri: redirectUri, - state: state, - }).toString() - ) - - // Set state cookie for CSRF verification + new URLSearchParams({ + client_id: clientId, + scope: SHOPIFY_SCOPES, + redirect_uri: redirectUri, + state: state, + }).toString() + + logger.info('Initiating Shopify OAuth:', { + shop: cleanShop, + requestedScopes: SHOPIFY_SCOPES, + redirectUri, + returnUrl: returnUrl || 'not specified', + }) + + const response = NextResponse.redirect(oauthUrl) + response.cookies.set('shopify_oauth_state', state, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - maxAge: 60 * 10, // 10 minutes + maxAge: 60 * 10, path: '/', }) - // Store the shop domain for the callback response.cookies.set('shopify_shop_domain', cleanShop, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - maxAge: 60 * 10, // 10 minutes + maxAge: 60 * 10, path: '/', }) + if (returnUrl) { + response.cookies.set('shopify_return_url', returnUrl, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 10, + path: '/', + }) + } + return response } catch (error) { logger.error('Error initiating Shopify authorization:', error) 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 a5a0313b132..13d8c65550e 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 @@ -235,11 +235,13 @@ const SCOPE_DESCRIPTIONS: Record = { // Box scopes root_readwrite: 'Read and write all files and folders in your Box account', root_readonly: 'Read all files and folders in your Box account', - // Shopify scopes (write implies read in Shopify) + // Shopify scopes (write_* implicitly includes read access) write_products: 'Read and manage your Shopify products', write_orders: 'Read and manage your Shopify orders', write_customers: 'Read and manage your Shopify customers', write_inventory: 'Read and manage your Shopify inventory levels', + read_locations: 'View your store locations', + write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders', // Zoom scopes 'user:read:user': 'View your Zoom profile information', 'meeting:write:meeting': 'Create Zoom meetings', @@ -252,6 +254,16 @@ const SCOPE_DESCRIPTIONS: Record = { 'cloud_recording:read:list_user_recordings': 'List your Zoom cloud recordings', 'cloud_recording:read:list_recording_files': 'View recording files', 'cloud_recording:delete:recording_file': 'Delete cloud recordings', + // Dropbox scopes + 'account_info.read': 'View your Dropbox account information', + 'files.metadata.read': 'View file and folder names, sizes, and dates', + 'files.metadata.write': 'Modify file and folder metadata', + 'files.content.read': 'Download and read your Dropbox files', + 'files.content.write': 'Upload, copy, move, and delete files in your Dropbox', + 'sharing.read': 'View your shared files and folders', + 'sharing.write': 'Share files and folders with others', + // WordPress.com scopes + global: 'Full access to manage your WordPress.com sites, posts, pages, media, and settings', } function getScopeDescription(scope: string): string { @@ -310,7 +322,9 @@ export function OAuthRequiredModal({ } if (providerId === 'shopify') { - window.location.href = '/api/auth/shopify/authorize' + // Pass the current URL so we can redirect back after OAuth + const returnUrl = encodeURIComponent(window.location.href) + window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}` return } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx index 0a2f6ad988f..b9bb1e3518a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx @@ -84,6 +84,7 @@ export function ShortInput({ const [isFocused, setIsFocused] = useState(false) const [copied, setCopied] = useState(false) const persistSubBlockValueRef = useRef<(value: string) => void>(() => {}) + const justPastedRef = useRef(false) // Always call the hook - hooks must be called unconditionally const webhookManagement = useWebhookManagement({ @@ -177,6 +178,11 @@ export function ShortInput({ return { show: true, searchTerm: '' } } if (event === 'change') { + // Don't show dropdown if user just pasted (they want to use the value directly) + if (justPastedRef.current) { + justPastedRef.current = false + return { show: false } + } // For API key fields, show env vars while typing without requiring '{{' return { show: true, searchTerm: value } } @@ -269,6 +275,8 @@ export function ShortInput({ // Handle paste events to ensure long values are handled correctly const handlePaste = useCallback((_e: React.ClipboardEvent) => { + // Mark that a paste just happened so we don't show env var dropdown + justPastedRef.current = true // Let the paste happen normally // Then ensure scroll positions are synced after the content is updated setTimeout(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 531207547fa..a26190a92af 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -353,6 +353,7 @@ export function Editor() { blockId={currentBlockId} config={subBlock} isPreview={false} + subBlockValues={subBlockState} disabled={!userPermissions.canEdit} fieldDiffStatus={undefined} allowExpandInPreview={false} diff --git a/apps/sim/blocks/blocks/elasticsearch.ts b/apps/sim/blocks/blocks/elasticsearch.ts index b1279adff2d..5c4d196e0d1 100644 --- a/apps/sim/blocks/blocks/elasticsearch.ts +++ b/apps/sim/blocks/blocks/elasticsearch.ts @@ -160,7 +160,7 @@ export const ElasticsearchBlock: BlockConfig = { // Optional Document ID - for index document { id: 'documentId', - title: 'Document ID (optional)', + title: 'Document ID', type: 'short-input', placeholder: 'Leave empty for auto-generated ID', condition: { field: 'operation', value: 'elasticsearch_index_document' }, @@ -170,7 +170,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'document', title: 'Document', - type: 'long-input', + type: 'code', placeholder: '{ "field": "value", "another_field": 123 }', required: true, condition: { field: 'operation', value: 'elasticsearch_index_document' }, @@ -180,7 +180,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'document', title: 'Partial Document', - type: 'long-input', + type: 'code', placeholder: '{ "field_to_update": "new_value" }', required: true, condition: { field: 'operation', value: 'elasticsearch_update_document' }, @@ -190,7 +190,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'query', title: 'Search Query', - type: 'long-input', + type: 'code', placeholder: '{ "match": { "field": "search term" } }', condition: { field: 'operation', value: 'elasticsearch_search' }, }, @@ -198,8 +198,8 @@ export const ElasticsearchBlock: BlockConfig = { // Count query { id: 'query', - title: 'Query (optional)', - type: 'long-input', + title: 'Query', + type: 'code', placeholder: '{ "match": { "field": "value" } }', condition: { field: 'operation', value: 'elasticsearch_count' }, }, @@ -226,7 +226,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'sort', title: 'Sort', - type: 'long-input', + type: 'code', placeholder: '[{ "field": { "order": "asc" } }]', condition: { field: 'operation', value: 'elasticsearch_search' }, }, @@ -259,7 +259,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'operations', title: 'Bulk Operations', - type: 'long-input', + type: 'code', placeholder: '{ "index": { "_index": "my-index", "_id": "1" } }\n{ "field": "value" }\n{ "delete": { "_index": "my-index", "_id": "2" } }', required: true, @@ -270,7 +270,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'settings', title: 'Index Settings', - type: 'long-input', + type: 'code', placeholder: '{ "number_of_shards": 1, "number_of_replicas": 1 }', condition: { field: 'operation', value: 'elasticsearch_create_index' }, }, @@ -279,7 +279,7 @@ export const ElasticsearchBlock: BlockConfig = { { id: 'mappings', title: 'Index Mappings', - type: 'long-input', + type: 'code', placeholder: '{ "properties": { "field": { "type": "text" } } }', condition: { field: 'operation', value: 'elasticsearch_create_index' }, }, @@ -324,9 +324,9 @@ export const ElasticsearchBlock: BlockConfig = { // Cluster health timeout { id: 'timeout', - title: 'Timeout', + title: 'Timeout (seconds)', type: 'short-input', - placeholder: '30s', + placeholder: '30', condition: { field: 'operation', value: 'elasticsearch_cluster_health' }, }, @@ -367,6 +367,10 @@ export const ElasticsearchBlock: BlockConfig = { if (params.retryOnConflict) { params.retryOnConflict = Number(params.retryOnConflict) } + // Append 's' to timeout for Elasticsearch time format + if (params.timeout && !params.timeout.endsWith('s')) { + params.timeout = `${params.timeout}s` + } // Return the operation as the tool ID return params.operation || 'elasticsearch_search' diff --git a/apps/sim/blocks/blocks/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts index 630fc73c066..b685b337998 100644 --- a/apps/sim/blocks/blocks/kalshi.ts +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -30,7 +30,6 @@ export const KalshiBlock: BlockConfig = { { label: 'Get Trades', id: 'get_trades' }, { label: 'Get Candlesticks', id: 'get_candlesticks' }, { label: 'Get Fills', id: 'get_fills' }, - { label: 'Get Series', id: 'get_series' }, { label: 'Get Series by Ticker', id: 'get_series_by_ticker' }, { label: 'Get Exchange Status', id: 'get_exchange_status' }, ], @@ -236,14 +235,6 @@ export const KalshiBlock: BlockConfig = { placeholder: 'Filter by order ID (optional)', condition: { field: 'operation', value: ['get_fills'] }, }, - // Get Series fields - { - id: 'statusSeries', - title: 'Status', - type: 'short-input', - placeholder: 'Filter by status (optional)', - condition: { field: 'operation', value: ['get_series'] }, - }, // Get Series by Ticker fields { id: 'seriesTickerGet', @@ -268,7 +259,6 @@ export const KalshiBlock: BlockConfig = { 'get_orders', 'get_trades', 'get_fills', - 'get_series', ], }, }, @@ -286,7 +276,6 @@ export const KalshiBlock: BlockConfig = { 'get_orders', 'get_trades', 'get_fills', - 'get_series', ], }, }, @@ -304,7 +293,6 @@ export const KalshiBlock: BlockConfig = { 'kalshi_get_trades', 'kalshi_get_candlesticks', 'kalshi_get_fills', - 'kalshi_get_series', 'kalshi_get_series_by_ticker', 'kalshi_get_exchange_status', ], @@ -333,8 +321,6 @@ export const KalshiBlock: BlockConfig = { return 'kalshi_get_candlesticks' case 'get_fills': return 'kalshi_get_fills' - case 'get_series': - return 'kalshi_get_series' case 'get_series_by_ticker': return 'kalshi_get_series_by_ticker' case 'get_exchange_status': @@ -353,7 +339,6 @@ export const KalshiBlock: BlockConfig = { tickerCandlesticks, seriesTickerCandlesticks, seriesTickerGet, - statusSeries, ...rest } = params const cleanParams: Record = {} @@ -389,11 +374,6 @@ export const KalshiBlock: BlockConfig = { cleanParams.seriesTicker = seriesTickerGet } - // Map statusSeries to status for get_series - if (operation === 'get_series' && statusSeries) { - cleanParams.status = statusSeries - } - Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { cleanParams[key] = value diff --git a/apps/sim/blocks/blocks/shopify.ts b/apps/sim/blocks/blocks/shopify.ts index bc5cc59160c..3d98542f42f 100644 --- a/apps/sim/blocks/blocks/shopify.ts +++ b/apps/sim/blocks/blocks/shopify.ts @@ -43,8 +43,16 @@ export const ShopifyBlock: BlockConfig = { { label: 'Update Customer', id: 'shopify_update_customer' }, { label: 'Delete Customer', id: 'shopify_delete_customer' }, // Inventory Operations + { label: 'List Inventory Items', id: 'shopify_list_inventory_items' }, { label: 'Get Inventory Level', id: 'shopify_get_inventory_level' }, { label: 'Adjust Inventory', id: 'shopify_adjust_inventory' }, + // Location Operations + { label: 'List Locations', id: 'shopify_list_locations' }, + // Fulfillment Operations + { label: 'Create Fulfillment', id: 'shopify_create_fulfillment' }, + // Collection Operations + { label: 'List Collections', id: 'shopify_list_collections' }, + { label: 'Get Collection', id: 'shopify_get_collection' }, ], value: () => 'shopify_list_products', }, @@ -53,7 +61,14 @@ export const ShopifyBlock: BlockConfig = { title: 'Shopify Account', type: 'oauth-input', serviceId: 'shopify', - requiredScopes: ['write_products', 'write_orders', 'write_customers', 'write_inventory'], + requiredScopes: [ + 'write_products', + 'write_orders', + 'write_customers', + 'write_inventory', + 'read_locations', + 'write_merchant_managed_fulfillment_orders', + ], placeholder: 'Select Shopify account', required: true, }, @@ -153,13 +168,35 @@ export const ShopifyBlock: BlockConfig = { }, // Query for listing products { - id: 'query', + id: 'productQuery', title: 'Search Query', type: 'short-input', placeholder: 'Filter products (optional)', condition: { field: 'operation', - value: ['shopify_list_products', 'shopify_list_customers'], + value: ['shopify_list_products'], + }, + }, + // Query for listing customers + { + id: 'customerQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., first_name:John OR email:*@gmail.com', + condition: { + field: 'operation', + value: ['shopify_list_customers'], + }, + }, + // Query for listing inventory items + { + id: 'inventoryQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., sku:ABC123', + condition: { + field: 'operation', + value: ['shopify_list_inventory_items'], }, }, // Order ID @@ -362,7 +399,7 @@ export const ShopifyBlock: BlockConfig = { placeholder: 'gid://shopify/Location/123456789', required: { field: 'operation', - value: ['shopify_adjust_inventory'], + value: 'shopify_adjust_inventory', }, condition: { field: 'operation', @@ -381,6 +418,84 @@ export const ShopifyBlock: BlockConfig = { value: ['shopify_adjust_inventory'], }, }, + // Fulfillment Order ID + { + id: 'fulfillmentOrderId', + title: 'Fulfillment Order ID', + type: 'short-input', + placeholder: 'gid://shopify/FulfillmentOrder/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_create_fulfillment'], + }, + }, + // Tracking Number + { + id: 'trackingNumber', + title: 'Tracking Number', + type: 'short-input', + placeholder: 'Enter tracking number', + condition: { + field: 'operation', + value: ['shopify_create_fulfillment'], + }, + }, + // Tracking Company + { + id: 'trackingCompany', + title: 'Shipping Carrier', + type: 'short-input', + placeholder: 'e.g., UPS, FedEx, USPS, DHL', + condition: { + field: 'operation', + value: ['shopify_create_fulfillment'], + }, + }, + // Tracking URL + { + id: 'trackingUrl', + title: 'Tracking URL', + type: 'short-input', + placeholder: 'https://...', + condition: { + field: 'operation', + value: ['shopify_create_fulfillment'], + }, + }, + // Notify Customer (for fulfillment) + { + id: 'notifyCustomer', + title: 'Notify Customer', + type: 'switch', + condition: { + field: 'operation', + value: ['shopify_create_fulfillment'], + }, + }, + // Collection ID + { + id: 'collectionId', + title: 'Collection ID', + type: 'short-input', + placeholder: 'gid://shopify/Collection/123456789', + required: true, + condition: { + field: 'operation', + value: ['shopify_get_collection'], + }, + }, + // Collection Query + { + id: 'collectionQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., title:Summer OR collection_type:smart', + condition: { + field: 'operation', + value: ['shopify_list_collections'], + }, + }, ], tools: { access: [ @@ -398,8 +513,13 @@ export const ShopifyBlock: BlockConfig = { 'shopify_list_customers', 'shopify_update_customer', 'shopify_delete_customer', + 'shopify_list_inventory_items', 'shopify_get_inventory_level', 'shopify_adjust_inventory', + 'shopify_list_locations', + 'shopify_create_fulfillment', + 'shopify_list_collections', + 'shopify_get_collection', ], config: { tool: (params) => { @@ -442,7 +562,7 @@ export const ShopifyBlock: BlockConfig = { case 'shopify_list_products': return { ...baseParams, - query: params.query?.trim(), + query: params.productQuery?.trim(), } case 'shopify_update_product': @@ -546,7 +666,7 @@ export const ShopifyBlock: BlockConfig = { case 'shopify_list_customers': return { ...baseParams, - query: params.query?.trim(), + query: params.customerQuery?.trim(), } case 'shopify_update_customer': @@ -577,6 +697,12 @@ export const ShopifyBlock: BlockConfig = { } // Inventory Operations + case 'shopify_list_inventory_items': + return { + ...baseParams, + query: params.inventoryQuery?.trim(), + } + case 'shopify_get_inventory_level': if (!params.inventoryItemId?.trim()) { throw new Error('Inventory Item ID is required.') @@ -604,6 +730,42 @@ export const ShopifyBlock: BlockConfig = { delta: Number(params.delta), } + // Location Operations + case 'shopify_list_locations': + return { + ...baseParams, + } + + // Fulfillment Operations + case 'shopify_create_fulfillment': + if (!params.fulfillmentOrderId?.trim()) { + throw new Error('Fulfillment Order ID is required.') + } + return { + ...baseParams, + fulfillmentOrderId: params.fulfillmentOrderId.trim(), + trackingNumber: params.trackingNumber?.trim(), + trackingCompany: params.trackingCompany?.trim(), + trackingUrl: params.trackingUrl?.trim(), + notifyCustomer: params.notifyCustomer, + } + + // Collection Operations + case 'shopify_list_collections': + return { + ...baseParams, + query: params.collectionQuery?.trim(), + } + + case 'shopify_get_collection': + if (!params.collectionId?.trim()) { + throw new Error('Collection ID is required.') + } + return { + ...baseParams, + collectionId: params.collectionId.trim(), + } + default: return baseParams } @@ -641,9 +803,19 @@ export const ShopifyBlock: BlockConfig = { customerTags: { type: 'string', description: 'Customer tags' }, acceptsMarketing: { type: 'boolean', description: 'Accepts marketing' }, // Inventory inputs + inventoryQuery: { type: 'string', description: 'Inventory search query' }, inventoryItemId: { type: 'string', description: 'Inventory item ID' }, locationId: { type: 'string', description: 'Location ID' }, delta: { type: 'number', description: 'Quantity change' }, + // Fulfillment inputs + fulfillmentOrderId: { type: 'string', description: 'Fulfillment order ID' }, + trackingNumber: { type: 'string', description: 'Shipment tracking number' }, + trackingCompany: { type: 'string', description: 'Shipping carrier name' }, + trackingUrl: { type: 'string', description: 'Tracking URL' }, + notifyCustomer: { type: 'boolean', description: 'Send shipping notification email' }, + // Collection inputs + collectionId: { type: 'string', description: 'Collection ID' }, + collectionQuery: { type: 'string', description: 'Collection search query' }, }, outputs: { // Product outputs @@ -656,7 +828,15 @@ export const ShopifyBlock: BlockConfig = { customer: { type: 'json', description: 'Customer data' }, customers: { type: 'json', description: 'Customers list' }, // Inventory outputs + inventoryItems: { type: 'json', description: 'Inventory items list' }, inventoryLevel: { type: 'json', description: 'Inventory level data' }, + // Location outputs + locations: { type: 'json', description: 'Locations list' }, + // Fulfillment outputs + fulfillment: { type: 'json', description: 'Fulfillment data' }, + // Collection outputs + collection: { type: 'json', description: 'Collection data with products' }, + collections: { type: 'json', description: 'Collections list' }, // Delete outputs deletedId: { type: 'string', description: 'ID of deleted resource' }, // Success indicator diff --git a/apps/sim/blocks/blocks/wordpress.ts b/apps/sim/blocks/blocks/wordpress.ts index 36e8d98e418..d96e388245c 100644 --- a/apps/sim/blocks/blocks/wordpress.ts +++ b/apps/sim/blocks/blocks/wordpress.ts @@ -7,9 +7,9 @@ export const WordPressBlock: BlockConfig = { type: 'wordpress', name: 'WordPress', description: 'Manage WordPress content', - authMode: AuthMode.ApiKey, + authMode: AuthMode.OAuth, longDescription: - 'Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports self-hosted WordPress sites using Application Passwords authentication.', + 'Integrate with WordPress to create, update, and manage posts, pages, media, comments, categories, tags, and users. Supports WordPress.com sites via OAuth and self-hosted WordPress sites using Application Passwords authentication.', docsLink: 'https://docs.sim.ai/tools/wordpress', category: 'tools', bgColor: '#21759B', @@ -59,27 +59,24 @@ export const WordPressBlock: BlockConfig = { value: () => 'wordpress_create_post', }, - // Authentication Fields - { - id: 'siteUrl', - title: 'Site URL', - type: 'short-input', - placeholder: 'https://your-site.com', - required: true, - }, - { - id: 'username', - title: 'Username', - type: 'short-input', - placeholder: 'WordPress username', + // Credential selector for OAuth + { + id: 'credential', + title: 'WordPress Account', + type: 'oauth-input', + serviceId: 'wordpress', + requiredScopes: ['global'], + placeholder: 'Select WordPress account', required: true, }, + + // Site ID for WordPress.com (required for OAuth) { - id: 'applicationPassword', - title: 'Application Password', + id: 'siteId', + title: 'Site ID or Domain', type: 'short-input', - placeholder: 'xxxx xxxx xxxx xxxx xxxx xxxx', - password: true, + placeholder: 'e.g., 12345678 or yoursite.wordpress.com', + description: 'Your WordPress.com site ID or domain. Find it in Settings → General.', required: true, }, @@ -621,17 +618,6 @@ export const WordPressBlock: BlockConfig = { ], }, }, - - // Hide empty for taxonomies - { - id: 'hideEmpty', - title: 'Hide Empty', - type: 'switch', - condition: { - field: 'operation', - value: ['wordpress_list_categories', 'wordpress_list_tags'], - }, - }, ], tools: { access: [ @@ -665,10 +651,10 @@ export const WordPressBlock: BlockConfig = { config: { tool: (params) => params.operation || 'wordpress_create_post', params: (params) => { - const baseParams = { - siteUrl: params.siteUrl, - username: params.username, - applicationPassword: params.applicationPassword, + // OAuth authentication for WordPress.com + const baseParams: Record = { + credential: params.credential, + siteId: params.siteId, } switch (params.operation) { @@ -840,8 +826,6 @@ export const WordPressBlock: BlockConfig = { perPage: params.perPage ? Number(params.perPage) : undefined, page: params.page ? Number(params.page) : undefined, search: params.search, - hideEmpty: params.hideEmpty, - orderBy: params.orderBy, order: params.order, } case 'wordpress_create_tag': @@ -857,8 +841,6 @@ export const WordPressBlock: BlockConfig = { perPage: params.perPage ? Number(params.perPage) : undefined, page: params.page ? Number(params.page) : undefined, search: params.search, - hideEmpty: params.hideEmpty, - orderBy: params.orderBy, order: params.order, } case 'wordpress_get_current_user': @@ -870,7 +852,6 @@ export const WordPressBlock: BlockConfig = { page: params.page ? Number(params.page) : undefined, search: params.search, roles: params.roles, - orderBy: params.orderBy, order: params.order, } case 'wordpress_get_user': @@ -894,9 +875,7 @@ export const WordPressBlock: BlockConfig = { }, inputs: { operation: { type: 'string', description: 'Operation to perform' }, - siteUrl: { type: 'string', description: 'WordPress site URL' }, - username: { type: 'string', description: 'WordPress username' }, - applicationPassword: { type: 'string', description: 'WordPress Application Password' }, + siteId: { type: 'string', description: 'WordPress.com site ID or domain' }, // Post inputs postId: { type: 'number', description: 'Post ID' }, title: { type: 'string', description: 'Post or page title' }, diff --git a/apps/sim/hooks/queries/oauth-connections.ts b/apps/sim/hooks/queries/oauth-connections.ts index 70b9932e3f9..56569f7b949 100644 --- a/apps/sim/hooks/queries/oauth-connections.ts +++ b/apps/sim/hooks/queries/oauth-connections.ts @@ -135,6 +135,13 @@ export function useConnectOAuthService() { return { success: true } } + // Shopify requires a custom OAuth flow with shop domain input + if (providerId === 'shopify') { + const returnUrl = encodeURIComponent(callbackURL) + window.location.href = `/api/auth/shopify/authorize?returnUrl=${returnUrl}` + return { success: true } + } + await client.oauth2.link({ providerId, callbackURL, diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 2e9a15580e3..f656407dece 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1757,6 +1757,54 @@ export const auth = betterAuth({ } }, }, + + // WordPress.com provider + { + providerId: 'wordpress', + clientId: env.WORDPRESS_CLIENT_ID as string, + clientSecret: env.WORDPRESS_CLIENT_SECRET as string, + authorizationUrl: 'https://public-api.wordpress.com/oauth2/authorize', + tokenUrl: 'https://public-api.wordpress.com/oauth2/token', + userInfoUrl: 'https://public-api.wordpress.com/rest/v1.1/me', + scopes: ['global'], + responseType: 'code', + prompt: 'consent', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/wordpress`, + getUserInfo: async (tokens) => { + try { + logger.info('Fetching WordPress.com user profile') + + const response = await fetch('https://public-api.wordpress.com/rest/v1.1/me', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Failed to fetch WordPress.com user info', { + status: response.status, + statusText: response.statusText, + }) + throw new Error('Failed to fetch user info') + } + + const profile = await response.json() + + return { + id: profile.ID?.toString() || profile.id?.toString(), + name: profile.display_name || profile.username || 'WordPress User', + email: profile.email || `${profile.username}@wordpress.com`, + emailVerified: profile.email_verified || false, + image: profile.avatar_URL || undefined, + createdAt: new Date(), + updatedAt: new Date(), + } + } catch (error) { + logger.error('Error in WordPress.com getUserInfo:', { error }) + return null + } + }, + }, ], }), // Include SSO plugin when enabled diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 2b9b6e741f4..dd94c92d610 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -225,6 +225,8 @@ export const env = createEnv({ SHOPIFY_CLIENT_SECRET: z.string().optional(), // Shopify OAuth client secret ZOOM_CLIENT_ID: z.string().optional(), // Zoom OAuth client ID ZOOM_CLIENT_SECRET: z.string().optional(), // Zoom OAuth client secret + WORDPRESS_CLIENT_ID: z.string().optional(), // WordPress.com OAuth client ID + WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 79912809009..4379b159579 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -34,6 +34,7 @@ import { TrelloIcon, WealthboxIcon, WebflowIcon, + WordpressIcon, xIcon, ZoomIcon, } from '@/components/icons' @@ -67,6 +68,7 @@ export type OAuthProvider = | 'linkedin' | 'shopify' | 'zoom' + | 'wordpress' | string export type OAuthService = @@ -106,6 +108,7 @@ export type OAuthService = | 'linkedin' | 'shopify' | 'zoom' + | 'wordpress' export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -584,7 +587,14 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'shopify', icon: (props) => ShopifyIcon(props), baseProviderIcon: (props) => ShopifyIcon(props), - scopes: ['write_products', 'write_orders', 'write_customers', 'write_inventory'], + scopes: [ + 'write_products', + 'write_orders', + 'write_customers', + 'write_inventory', + 'read_locations', + 'write_merchant_managed_fulfillment_orders', + ], }, }, defaultService: 'shopify', @@ -849,6 +859,23 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'zoom', }, + wordpress: { + id: 'wordpress', + name: 'WordPress', + icon: (props) => WordpressIcon(props), + services: { + wordpress: { + id: 'wordpress', + name: 'WordPress.com', + description: 'Manage posts, pages, media, comments, and more on WordPress.com sites.', + providerId: 'wordpress', + icon: (props) => WordpressIcon(props), + baseProviderIcon: (props) => WordpressIcon(props), + scopes: ['global'], + }, + }, + defaultService: 'wordpress', + }, } export function getServiceByProviderAndId( @@ -1382,6 +1409,21 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: false, } } + case 'wordpress': { + // WordPress.com does NOT support refresh tokens + // Users will need to re-authorize when tokens expire (~2 weeks) + const { clientId, clientSecret } = getCredentials( + env.WORDPRESS_CLIENT_ID, + env.WORDPRESS_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://public-api.wordpress.com/oauth2/token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: false, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/tools/kalshi/get_series.ts b/apps/sim/tools/kalshi/get_series.ts deleted file mode 100644 index 5d9de9c2d41..00000000000 --- a/apps/sim/tools/kalshi/get_series.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { KalshiPaginationParams, KalshiPagingInfo, KalshiSeries } from './types' -import { buildKalshiUrl, handleKalshiError } from './types' - -export interface KalshiGetSeriesParams extends KalshiPaginationParams { - status?: string -} - -export interface KalshiGetSeriesResponse { - success: boolean - output: { - series: KalshiSeries[] - paging?: KalshiPagingInfo - metadata: { - operation: 'get_series' - totalReturned: number - } - success: boolean - } -} - -export const kalshiGetSeriesTool: ToolConfig = { - id: 'kalshi_get_series', - name: 'Get Series from Kalshi', - description: 'Retrieve a list of market series templates from Kalshi', - version: '1.0.0', - - params: { - status: { - type: 'string', - required: false, - description: 'Filter by status', - }, - limit: { - type: 'string', - required: false, - description: 'Number of results (1-1000, default: 100)', - }, - cursor: { - type: 'string', - required: false, - description: 'Pagination cursor for next page', - }, - }, - - request: { - url: (params) => { - const queryParams = new URLSearchParams() - if (params.status) queryParams.append('status', params.status) - if (params.limit) queryParams.append('limit', params.limit) - if (params.cursor) queryParams.append('cursor', params.cursor) - - const query = queryParams.toString() - const url = buildKalshiUrl('/series') - return query ? `${url}?${query}` : url - }, - method: 'GET', - headers: () => ({ - 'Content-Type': 'application/json', - }), - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - if (!response.ok) { - handleKalshiError(data, response.status, 'get_series') - } - - const series = data.series || [] - - return { - success: true, - output: { - series, - paging: { - cursor: data.cursor || null, - }, - metadata: { - operation: 'get_series' as const, - totalReturned: series.length, - }, - success: true, - }, - } - }, - - outputs: { - success: { type: 'boolean', description: 'Operation success status' }, - output: { - type: 'object', - description: 'Series data and metadata', - properties: { - series: { type: 'array', description: 'Array of series objects' }, - paging: { type: 'object', description: 'Pagination information' }, - metadata: { type: 'object', description: 'Operation metadata' }, - success: { type: 'boolean', description: 'Operation success' }, - }, - }, - }, -} diff --git a/apps/sim/tools/kalshi/index.ts b/apps/sim/tools/kalshi/index.ts index d1cf2c83bb3..3bae8d37d0f 100644 --- a/apps/sim/tools/kalshi/index.ts +++ b/apps/sim/tools/kalshi/index.ts @@ -9,6 +9,5 @@ export { kalshiGetMarketsTool } from './get_markets' export { kalshiGetOrderbookTool } from './get_orderbook' export { kalshiGetOrdersTool } from './get_orders' export { kalshiGetPositionsTool } from './get_positions' -export { kalshiGetSeriesTool } from './get_series' export { kalshiGetSeriesByTickerTool } from './get_series_by_ticker' export { kalshiGetTradesTool } from './get_trades' diff --git a/apps/sim/tools/kalshi/types.ts b/apps/sim/tools/kalshi/types.ts index a2c18a980e1..bd63d081528 100644 --- a/apps/sim/tools/kalshi/types.ts +++ b/apps/sim/tools/kalshi/types.ts @@ -165,6 +165,12 @@ export interface KalshiFill { trade_id: string } +// Settlement source type +export interface KalshiSettlementSource { + name: string + url: string +} + // Series type export interface KalshiSeries { ticker: string @@ -172,7 +178,13 @@ export interface KalshiSeries { frequency: string category: string tags?: string[] - status?: string + settlement_sources?: KalshiSettlementSource[] + contract_url?: string + contract_terms_url?: string + fee_type?: string // 'quadratic' | 'quadratic_with_maker_fees' | 'flat' + fee_multiplier?: number + additional_prohibitions?: string[] + product_metadata?: Record } // Exchange status type @@ -186,7 +198,51 @@ export function buildKalshiUrl(path: string): string { return `${KALSHI_BASE_URL}${path}` } -// RSA signature generation for authenticated requests +// Helper to normalize PEM key format +// Handles: literal \n strings, missing line breaks, various PEM formats +function normalizePemKey(privateKey: string): string { + let key = privateKey.trim() + + // Convert literal \n strings to actual newlines + key = key.replace(/\\n/g, '\n') + + // Extract the key type and base64 content + const beginMatch = key.match(/-----BEGIN ([A-Z\s]+)-----/) + const endMatch = key.match(/-----END ([A-Z\s]+)-----/) + + if (beginMatch && endMatch) { + // Extract the key type (e.g., "RSA PRIVATE KEY" or "PRIVATE KEY") + const keyType = beginMatch[1] + + // Extract base64 content between headers + const startIdx = key.indexOf('-----', key.indexOf('-----') + 5) + 5 + const endIdx = key.lastIndexOf('-----END') + let base64Content = key.substring(startIdx, endIdx) + + // Remove all whitespace from base64 content + base64Content = base64Content.replace(/\s/g, '') + + // Reconstruct PEM with proper 64-character line breaks + const lines: string[] = [] + for (let i = 0; i < base64Content.length; i += 64) { + lines.push(base64Content.substring(i, i + 64)) + } + + return `-----BEGIN ${keyType}-----\n${lines.join('\n')}\n-----END ${keyType}-----` + } + + // No PEM headers found - assume raw base64, wrap in PKCS#8 format + const cleanKey = key.replace(/\s/g, '') + const lines: string[] = [] + for (let i = 0; i < cleanKey.length; i += 64) { + lines.push(cleanKey.substring(i, i + 64)) + } + + return `-----BEGIN PRIVATE KEY-----\n${lines.join('\n')}\n-----END PRIVATE KEY-----` +} + +// RSA-PSS signature generation for authenticated requests +// Kalshi requires RSA-PSS with SHA256, not plain PKCS#1 v1.5 export function generateKalshiSignature( privateKey: string, timestamp: string, @@ -198,9 +254,17 @@ export function generateKalshiSignature( const pathWithoutQuery = path.split('?')[0] const message = timestamp + method.toUpperCase() + pathWithoutQuery - const sign = crypto.createSign('RSA-SHA256') - sign.update(message) - return sign.sign(privateKey, 'base64') + // Normalize PEM key format (handles literal \n, missing line breaks, etc.) + const pemKey = normalizePemKey(privateKey) + + // Use RSA-PSS padding with SHA256 (required by Kalshi API) + const signature = crypto.sign('sha256', Buffer.from(message, 'utf-8'), { + key: pemKey, + padding: crypto.constants.RSA_PKCS1_PSS_PADDING, + saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST, + }) + + return signature.toString('base64') } // Build auth headers for authenticated requests diff --git a/apps/sim/tools/polymarket/get_tick_size.ts b/apps/sim/tools/polymarket/get_tick_size.ts index 79d8f9a1983..8a3a9788c4d 100644 --- a/apps/sim/tools/polymarket/get_tick_size.ts +++ b/apps/sim/tools/polymarket/get_tick_size.ts @@ -53,10 +53,14 @@ export const polymarketGetTickSizeTool: ToolConfig< handlePolymarketError(data, response.status, 'get_tick_size') } + // API returns { minimum_tick_size: "0.01" } + const tickSize = + typeof data === 'string' ? data : data.minimum_tick_size || data.tick_size || '' + return { success: true, output: { - tickSize: typeof data === 'string' ? data : data.tick_size || data.tickSize || '', + tickSize: String(tickSize), metadata: { operation: 'get_tick_size' as const, tokenId: params?.tokenId || '', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index df137b82268..0043de7b3ca 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -455,7 +455,6 @@ import { kalshiGetOrdersTool, kalshiGetPositionsTool, kalshiGetSeriesByTickerTool, - kalshiGetSeriesTool, kalshiGetTradesTool, } from '@/tools/kalshi' import { @@ -966,14 +965,19 @@ import { shopifyAdjustInventoryTool, shopifyCancelOrderTool, shopifyCreateCustomerTool, + shopifyCreateFulfillmentTool, shopifyCreateProductTool, shopifyDeleteCustomerTool, shopifyDeleteProductTool, + shopifyGetCollectionTool, shopifyGetCustomerTool, shopifyGetInventoryLevelTool, shopifyGetOrderTool, shopifyGetProductTool, + shopifyListCollectionsTool, shopifyListCustomersTool, + shopifyListInventoryItemsTool, + shopifyListLocationsTool, shopifyListOrdersTool, shopifyListProductsTool, shopifyUpdateCustomerTool, @@ -1376,7 +1380,6 @@ export const tools: Record = { kalshi_get_trades: kalshiGetTradesTool, kalshi_get_candlesticks: kalshiGetCandlesticksTool, kalshi_get_fills: kalshiGetFillsTool, - kalshi_get_series: kalshiGetSeriesTool, kalshi_get_series_by_ticker: kalshiGetSeriesByTickerTool, kalshi_get_exchange_status: kalshiGetExchangeStatusTool, polymarket_get_markets: polymarketGetMarketsTool, @@ -2002,6 +2005,11 @@ export const tools: Record = { shopify_delete_customer: shopifyDeleteCustomerTool, shopify_get_inventory_level: shopifyGetInventoryLevelTool, shopify_adjust_inventory: shopifyAdjustInventoryTool, + shopify_list_inventory_items: shopifyListInventoryItemsTool, + shopify_list_locations: shopifyListLocationsTool, + shopify_create_fulfillment: shopifyCreateFulfillmentTool, + shopify_list_collections: shopifyListCollectionsTool, + shopify_get_collection: shopifyGetCollectionTool, onedrive_create_folder: onedriveCreateFolderTool, onedrive_delete: onedriveDeleteTool, onedrive_download: onedriveDownloadTool, diff --git a/apps/sim/tools/shopify/cancel_order.ts b/apps/sim/tools/shopify/cancel_order.ts index 30893da11ab..532286b7d4e 100644 --- a/apps/sim/tools/shopify/cancel_order.ts +++ b/apps/sim/tools/shopify/cancel_order.ts @@ -29,7 +29,7 @@ export const shopifyCancelOrderTool: ToolConfig + fulfillmentLineItems: Array<{ + id: string + quantity: number + lineItem: { + title: string + } + }> + } + } +} + +export const shopifyCreateFulfillmentTool: ToolConfig< + ShopifyCreateFulfillmentParams, + ShopifyCreateFulfillmentResponse +> = { + id: 'shopify_create_fulfillment', + name: 'Shopify Create Fulfillment', + description: + 'Create a fulfillment to mark order items as shipped. Requires a fulfillment order ID (get this from the order details).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + fulfillmentOrderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The fulfillment order ID (e.g., gid://shopify/FulfillmentOrder/123456789)', + }, + trackingNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Tracking number for the shipment', + }, + trackingCompany: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Shipping carrier name (e.g., UPS, FedEx, USPS, DHL)', + }, + trackingUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL to track the shipment', + }, + notifyCustomer: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to send a shipping confirmation email to the customer (default: true)', + }, + }, + + request: { + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + // Build tracking info if any tracking details provided + const trackingInfo: { + number?: string + company?: string + url?: string + } = {} + + if (params.trackingNumber) { + trackingInfo.number = params.trackingNumber + } + if (params.trackingCompany) { + trackingInfo.company = params.trackingCompany + } + if (params.trackingUrl) { + trackingInfo.url = params.trackingUrl + } + + const fulfillmentInput: { + lineItemsByFulfillmentOrder: Array<{ fulfillmentOrderId: string }> + notifyCustomer?: boolean + trackingInfo?: typeof trackingInfo + } = { + lineItemsByFulfillmentOrder: [ + { + fulfillmentOrderId: params.fulfillmentOrderId, + }, + ], + notifyCustomer: params.notifyCustomer !== false, // Default to true + } + + // Only include trackingInfo if we have at least one tracking field + if (Object.keys(trackingInfo).length > 0) { + fulfillmentInput.trackingInfo = trackingInfo + } + + return { + query: ` + mutation fulfillmentCreateV2($fulfillment: FulfillmentV2Input!) { + fulfillmentCreateV2(fulfillment: $fulfillment) { + fulfillment { + id + status + createdAt + updatedAt + trackingInfo { + company + number + url + } + fulfillmentLineItems(first: 50) { + edges { + node { + id + quantity + lineItem { + title + } + } + } + } + } + userErrors { + field + message + } + } + } + `, + variables: { + fulfillment: fulfillmentInput, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create fulfillment', + output: {}, + } + } + + const result = data.data?.fulfillmentCreateV2 + if (!result) { + return { + success: false, + error: 'Failed to create fulfillment', + output: {}, + } + } + + if (result.userErrors && result.userErrors.length > 0) { + return { + success: false, + error: result.userErrors.map((e: { message: string }) => e.message).join(', '), + output: {}, + } + } + + const fulfillment = result.fulfillment + if (!fulfillment) { + return { + success: false, + error: 'No fulfillment returned', + output: {}, + } + } + + // Transform fulfillment line items from edges format + const fulfillmentLineItems = + fulfillment.fulfillmentLineItems?.edges?.map((edge: { node: unknown }) => edge.node) || [] + + return { + success: true, + output: { + fulfillment: { + id: fulfillment.id, + status: fulfillment.status, + createdAt: fulfillment.createdAt, + updatedAt: fulfillment.updatedAt, + trackingInfo: fulfillment.trackingInfo || [], + fulfillmentLineItems, + }, + }, + } + }, + + outputs: { + fulfillment: { + type: 'object', + description: 'The created fulfillment with tracking info and fulfilled items', + }, + }, +} diff --git a/apps/sim/tools/shopify/create_product.ts b/apps/sim/tools/shopify/create_product.ts index 9bbc92fa5d6..5d4ffe4c00f 100644 --- a/apps/sim/tools/shopify/create_product.ts +++ b/apps/sim/tools/shopify/create_product.ts @@ -121,7 +121,6 @@ export const shopifyCreateProductTool: ToolConfig< price compareAtPrice sku - barcode inventoryQuantity } } diff --git a/apps/sim/tools/shopify/get_collection.ts b/apps/sim/tools/shopify/get_collection.ts new file mode 100644 index 00000000000..4747d99d52d --- /dev/null +++ b/apps/sim/tools/shopify/get_collection.ts @@ -0,0 +1,224 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { ShopifyBaseParams } from './types' + +interface ShopifyGetCollectionParams extends ShopifyBaseParams { + collectionId: string + productsFirst?: number +} + +interface ShopifyGetCollectionResponse extends ToolResponse { + output: { + collection?: { + id: string + title: string + handle: string + description: string | null + descriptionHtml: string | null + productsCount: number + sortOrder: string + updatedAt: string + image: { + url: string + altText: string | null + } | null + products: Array<{ + id: string + title: string + handle: string + status: string + vendor: string + productType: string + totalInventory: number + featuredImage: { + url: string + altText: string | null + } | null + }> + } + } +} + +export const shopifyGetCollectionTool: ToolConfig< + ShopifyGetCollectionParams, + ShopifyGetCollectionResponse +> = { + id: 'shopify_get_collection', + name: 'Shopify Get Collection', + description: + 'Get a specific collection by ID, including its products. Use this to retrieve products within a collection.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The collection ID (e.g., gid://shopify/Collection/123456789)', + }, + productsFirst: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of products to return from this collection (default: 50, max: 250)', + }, + }, + + request: { + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const productsFirst = Math.min(params.productsFirst || 50, 250) + + return { + query: ` + query getCollection($id: ID!, $productsFirst: Int!) { + collection(id: $id) { + id + title + handle + description + descriptionHtml + productsCount { + count + } + sortOrder + updatedAt + image { + url + altText + } + products(first: $productsFirst) { + edges { + node { + id + title + handle + status + vendor + productType + totalInventory + featuredMedia { + preview { + image { + url + altText + } + } + } + } + } + } + } + } + `, + variables: { + id: params.collectionId, + productsFirst, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get collection', + output: {}, + } + } + + const collection = data.data?.collection + if (!collection) { + return { + success: false, + error: 'Collection not found', + output: {}, + } + } + + // Transform products from edges format and map featuredMedia to featuredImage + const products = + collection.products?.edges?.map( + (edge: { + node: { + id: string + title: string + handle: string + status: string + vendor: string + productType: string + totalInventory: number + featuredMedia?: { + preview?: { + image?: { + url: string + altText: string | null + } + } + } + } + }) => { + const product = edge.node + return { + id: product.id, + title: product.title, + handle: product.handle, + status: product.status, + vendor: product.vendor, + productType: product.productType, + totalInventory: product.totalInventory, + featuredImage: product.featuredMedia?.preview?.image || null, + } + } + ) || [] + + return { + success: true, + output: { + collection: { + id: collection.id, + title: collection.title, + handle: collection.handle, + description: collection.description, + descriptionHtml: collection.descriptionHtml, + productsCount: collection.productsCount?.count ?? 0, + sortOrder: collection.sortOrder, + updatedAt: collection.updatedAt, + image: collection.image, + products, + }, + }, + } + }, + + outputs: { + collection: { + type: 'object', + description: 'The collection details including its products', + }, + }, +} diff --git a/apps/sim/tools/shopify/get_customer.ts b/apps/sim/tools/shopify/get_customer.ts index 4eefd4a94b7..b1aa0f9b58a 100644 --- a/apps/sim/tools/shopify/get_customer.ts +++ b/apps/sim/tools/shopify/get_customer.ts @@ -59,8 +59,7 @@ export const shopifyGetCustomerTool: ToolConfig edge.node + (edge: { + node: { + id: string + quantities: Array<{ name: string; quantity: number }> + location: { id: string; name: string } + } + }) => { + const node = edge.node + // Extract quantities into a more usable format + const quantitiesMap: Record = {} + node.quantities.forEach((q) => { + quantitiesMap[q.name] = q.quantity + }) + return { + id: node.id, + available: quantitiesMap.available ?? 0, + onHand: quantitiesMap.on_hand ?? 0, + committed: quantitiesMap.committed ?? 0, + incoming: quantitiesMap.incoming ?? 0, + reserved: quantitiesMap.reserved ?? 0, + location: node.location, + } + } ) return { diff --git a/apps/sim/tools/shopify/get_product.ts b/apps/sim/tools/shopify/get_product.ts index 98d2fc92230..321bad45678 100644 --- a/apps/sim/tools/shopify/get_product.ts +++ b/apps/sim/tools/shopify/get_product.ts @@ -67,11 +67,7 @@ export const shopifyGetProductTool: ToolConfig + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export const shopifyListCollectionsTool: ToolConfig< + ShopifyListCollectionsParams, + ShopifyCollectionsResponse +> = { + id: 'shopify_list_collections', + name: 'Shopify List Collections', + description: + 'List product collections from your Shopify store. Filter by title, type (custom/smart), or handle.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of collections to return (default: 50, max: 250)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search query to filter collections (e.g., "title:Summer" or "collection_type:smart")', + }, + }, + + request: { + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listCollections($first: Int!, $query: String) { + collections(first: $first, query: $query) { + edges { + node { + id + title + handle + description + descriptionHtml + productsCount { + count + } + sortOrder + updatedAt + image { + url + altText + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + query: params.query || null, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list collections', + output: {}, + } + } + + const collectionsData = data.data?.collections + if (!collectionsData) { + return { + success: false, + error: 'Failed to retrieve collections', + output: {}, + } + } + + const collections = collectionsData.edges.map( + (edge: { + node: { + id: string + title: string + handle: string + description: string | null + descriptionHtml: string | null + productsCount: { count: number } + sortOrder: string + updatedAt: string + image: { url: string; altText: string | null } | null + } + }) => ({ + id: edge.node.id, + title: edge.node.title, + handle: edge.node.handle, + description: edge.node.description, + descriptionHtml: edge.node.descriptionHtml, + productsCount: edge.node.productsCount?.count ?? 0, + sortOrder: edge.node.sortOrder, + updatedAt: edge.node.updatedAt, + image: edge.node.image, + }) + ) + + return { + success: true, + output: { + collections, + pageInfo: collectionsData.pageInfo, + }, + } + }, + + outputs: { + collections: { + type: 'array', + description: 'List of collections with their IDs, titles, and product counts', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/list_customers.ts b/apps/sim/tools/shopify/list_customers.ts index 6ab9c6e70a0..1a6ecc9bc97 100644 --- a/apps/sim/tools/shopify/list_customers.ts +++ b/apps/sim/tools/shopify/list_customers.ts @@ -33,7 +33,7 @@ export const shopifyListCustomersTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Search query to filter customers (e.g., "email:*@example.com" or "tag:vip" or "orders_count:>5")', + 'Search query to filter customers (e.g., "first_name:John" or "last_name:Smith" or "email:*@gmail.com" or "tag:vip")', }, }, @@ -68,8 +68,7 @@ export const shopifyListCustomersTool: ToolConfig< updatedAt note tags - ordersCount - totalSpentV2 { + amountSpent { amount currencyCode } diff --git a/apps/sim/tools/shopify/list_inventory_items.ts b/apps/sim/tools/shopify/list_inventory_items.ts new file mode 100644 index 00000000000..228f1287e9b --- /dev/null +++ b/apps/sim/tools/shopify/list_inventory_items.ts @@ -0,0 +1,235 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { ShopifyBaseParams } from './types' + +interface ShopifyListInventoryItemsParams extends ShopifyBaseParams { + first?: number + query?: string +} + +interface ShopifyInventoryItemsResponse extends ToolResponse { + output: { + inventoryItems?: Array<{ + id: string + sku: string | null + tracked: boolean + createdAt: string + updatedAt: string + variant?: { + id: string + title: string + product?: { + id: string + title: string + } + } + inventoryLevels: Array<{ + id: string + available: number + location: { + id: string + name: string + } + }> + }> + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export const shopifyListInventoryItemsTool: ToolConfig< + ShopifyListInventoryItemsParams, + ShopifyInventoryItemsResponse +> = { + id: 'shopify_list_inventory_items', + name: 'Shopify List Inventory Items', + description: + 'List inventory items from your Shopify store. Use this to find inventory item IDs by SKU.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of inventory items to return (default: 50, max: 250)', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query to filter inventory items (e.g., "sku:ABC123")', + }, + }, + + request: { + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listInventoryItems($first: Int!, $query: String) { + inventoryItems(first: $first, query: $query) { + edges { + node { + id + sku + tracked + createdAt + updatedAt + variant { + id + title + product { + id + title + } + } + inventoryLevels(first: 10) { + edges { + node { + id + quantities(names: ["available", "on_hand"]) { + name + quantity + } + location { + id + name + } + } + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + query: params.query || null, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list inventory items', + output: {}, + } + } + + const inventoryItemsData = data.data?.inventoryItems + if (!inventoryItemsData) { + return { + success: false, + error: 'Failed to retrieve inventory items', + output: {}, + } + } + + const inventoryItems = inventoryItemsData.edges.map( + (edge: { + node: { + id: string + sku: string | null + tracked: boolean + createdAt: string + updatedAt: string + variant?: { + id: string + title: string + product?: { + id: string + title: string + } + } + inventoryLevels: { + edges: Array<{ + node: { + id: string + quantities: Array<{ name: string; quantity: number }> + location: { id: string; name: string } + } + }> + } + } + }) => { + const node = edge.node + // Transform inventory levels to include available quantity + const inventoryLevels = node.inventoryLevels.edges.map((levelEdge) => { + const levelNode = levelEdge.node + const availableQty = + levelNode.quantities.find((q) => q.name === 'available')?.quantity ?? 0 + return { + id: levelNode.id, + available: availableQty, + location: levelNode.location, + } + }) + + return { + id: node.id, + sku: node.sku, + tracked: node.tracked, + createdAt: node.createdAt, + updatedAt: node.updatedAt, + variant: node.variant, + inventoryLevels, + } + } + ) + + return { + success: true, + output: { + inventoryItems, + pageInfo: inventoryItemsData.pageInfo, + }, + } + }, + + outputs: { + inventoryItems: { + type: 'array', + description: 'List of inventory items with their IDs, SKUs, and stock levels', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/list_locations.ts b/apps/sim/tools/shopify/list_locations.ts new file mode 100644 index 00000000000..21eb9dce345 --- /dev/null +++ b/apps/sim/tools/shopify/list_locations.ts @@ -0,0 +1,166 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { ShopifyBaseParams } from './types' + +interface ShopifyListLocationsParams extends ShopifyBaseParams { + first?: number + includeInactive?: boolean +} + +interface ShopifyLocationsResponse extends ToolResponse { + output: { + locations?: Array<{ + id: string + name: string + isActive: boolean + fulfillsOnlineOrders: boolean + address: { + address1: string | null + address2: string | null + city: string | null + province: string | null + provinceCode: string | null + country: string | null + countryCode: string | null + zip: string | null + phone: string | null + } | null + }> + pageInfo?: { + hasNextPage: boolean + hasPreviousPage: boolean + } + } +} + +export const shopifyListLocationsTool: ToolConfig< + ShopifyListLocationsParams, + ShopifyLocationsResponse +> = { + id: 'shopify_list_locations', + name: 'Shopify List Locations', + description: + 'List inventory locations from your Shopify store. Use this to find location IDs needed for inventory operations.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'shopify', + }, + + params: { + shopDomain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Shopify store domain (e.g., mystore.myshopify.com)', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of locations to return (default: 50, max: 250)', + }, + includeInactive: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include deactivated locations (default: false)', + }, + }, + + request: { + url: (params) => + `https://${params.shopDomain || params.idToken}/admin/api/2024-10/graphql.json`, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Shopify API request') + } + return { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': params.accessToken, + } + }, + body: (params) => { + const first = Math.min(params.first || 50, 250) + + return { + query: ` + query listLocations($first: Int!, $includeInactive: Boolean) { + locations(first: $first, includeInactive: $includeInactive) { + edges { + node { + id + name + isActive + fulfillsOnlineOrders + address { + address1 + address2 + city + province + provinceCode + country + countryCode + zip + phone + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + `, + variables: { + first, + includeInactive: params.includeInactive || false, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list locations', + output: {}, + } + } + + const locationsData = data.data?.locations + if (!locationsData) { + return { + success: false, + error: 'Failed to retrieve locations', + output: {}, + } + } + + const locations = locationsData.edges.map((edge: { node: unknown }) => edge.node) + + return { + success: true, + output: { + locations, + pageInfo: locationsData.pageInfo, + }, + } + }, + + outputs: { + locations: { + type: 'array', + description: 'List of locations with their IDs, names, and addresses', + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/shopify/list_orders.ts b/apps/sim/tools/shopify/list_orders.ts index 033a8687d1f..ed6294f22e8 100644 --- a/apps/sim/tools/shopify/list_orders.ts +++ b/apps/sim/tools/shopify/list_orders.ts @@ -25,6 +25,12 @@ export const shopifyListOrdersTool: ToolConfig { const first = Math.min(params.first || 50, 250) + // Build query string with status filter if provided + const queryParts: string[] = [] + if (params.status && params.status !== 'any') { + queryParts.push(`status:${params.status}`) + } + if (params.query) { + queryParts.push(params.query) + } + const queryString = queryParts.length > 0 ? queryParts.join(' ') : null + return { query: ` query listOrders($first: Int!, $query: String) { @@ -120,7 +136,7 @@ export const shopifyListOrdersTool: ToolConfig = { id: 'wordpress_create_category', name: 'WordPress Create Category', - description: 'Create a new category in WordPress', + description: 'Create a new category in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, name: { type: 'string', @@ -56,20 +54,12 @@ export const createCategoryTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/categories` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/categories`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = { name: params.name, diff --git a/apps/sim/tools/wordpress/create_comment.ts b/apps/sim/tools/wordpress/create_comment.ts index 2513389365a..0c3bcd2a179 100644 --- a/apps/sim/tools/wordpress/create_comment.ts +++ b/apps/sim/tools/wordpress/create_comment.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressCreateCommentParams, WordPressCreateCommentResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreateCommentParams, + type WordPressCreateCommentResponse, +} from './types' export const createCommentTool: ToolConfig< WordPressCreateCommentParams, @@ -7,27 +11,21 @@ export const createCommentTool: ToolConfig< > = { id: 'wordpress_create_comment', name: 'WordPress Create Comment', - description: 'Create a new comment on a WordPress post', + description: 'Create a new comment on a WordPress.com post', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, postId: { type: 'number', @@ -68,20 +66,12 @@ export const createCommentTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/comments` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = { post: params.postId, diff --git a/apps/sim/tools/wordpress/create_page.ts b/apps/sim/tools/wordpress/create_page.ts index 2d85905b6fd..461d91e3b66 100644 --- a/apps/sim/tools/wordpress/create_page.ts +++ b/apps/sim/tools/wordpress/create_page.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressCreatePageParams, WordPressCreatePageResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreatePageParams, + type WordPressCreatePageResponse, +} from './types' export const createPageTool: ToolConfig = { id: 'wordpress_create_page', name: 'WordPress Create Page', - description: 'Create a new page in WordPress', + description: 'Create a new page in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, title: { type: 'string', @@ -77,20 +75,12 @@ export const createPageTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/pages` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = { title: params.title, diff --git a/apps/sim/tools/wordpress/create_post.ts b/apps/sim/tools/wordpress/create_post.ts index 5473d6378c7..53f71f92522 100644 --- a/apps/sim/tools/wordpress/create_post.ts +++ b/apps/sim/tools/wordpress/create_post.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressCreatePostParams, WordPressCreatePostResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreatePostParams, + type WordPressCreatePostResponse, +} from './types' export const createPostTool: ToolConfig = { id: 'wordpress_create_post', name: 'WordPress Create Post', - description: 'Create a new blog post in WordPress', + description: 'Create a new blog post in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, title: { type: 'string', @@ -77,20 +75,12 @@ export const createPostTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/posts` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = { title: params.title, diff --git a/apps/sim/tools/wordpress/create_tag.ts b/apps/sim/tools/wordpress/create_tag.ts index 13a30280b30..80c920d67f8 100644 --- a/apps/sim/tools/wordpress/create_tag.ts +++ b/apps/sim/tools/wordpress/create_tag.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressCreateTagParams, WordPressCreateTagResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreateTagParams, + type WordPressCreateTagResponse, +} from './types' export const createTagTool: ToolConfig = { id: 'wordpress_create_tag', name: 'WordPress Create Tag', - description: 'Create a new tag in WordPress', + description: 'Create a new tag in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, name: { type: 'string', @@ -47,20 +45,12 @@ export const createTagTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/tags` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/tags`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = { name: params.name, diff --git a/apps/sim/tools/wordpress/delete_comment.ts b/apps/sim/tools/wordpress/delete_comment.ts index ee645161809..3cdc2a7664c 100644 --- a/apps/sim/tools/wordpress/delete_comment.ts +++ b/apps/sim/tools/wordpress/delete_comment.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressDeleteCommentParams, WordPressDeleteCommentResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressDeleteCommentParams, + type WordPressDeleteCommentResponse, +} from './types' export const deleteCommentTool: ToolConfig< WordPressDeleteCommentParams, @@ -7,27 +11,21 @@ export const deleteCommentTool: ToolConfig< > = { id: 'wordpress_delete_comment', name: 'WordPress Delete Comment', - description: 'Delete a comment from WordPress', + description: 'Delete a comment from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, commentId: { type: 'number', @@ -45,20 +43,14 @@ export const deleteCommentTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') const forceParam = params.force ? '?force=true' : '' - return `${baseUrl}/wp-json/wp/v2/comments/${params.commentId}${forceParam}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments/${params.commentId}${forceParam}` }, method: 'DELETE', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/delete_media.ts b/apps/sim/tools/wordpress/delete_media.ts index 19046bb61b8..cbb8afd84d9 100644 --- a/apps/sim/tools/wordpress/delete_media.ts +++ b/apps/sim/tools/wordpress/delete_media.ts @@ -1,31 +1,29 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressDeleteMediaParams, WordPressDeleteMediaResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressDeleteMediaParams, + type WordPressDeleteMediaResponse, +} from './types' export const deleteMediaTool: ToolConfig = { id: 'wordpress_delete_media', name: 'WordPress Delete Media', - description: 'Delete a media item from WordPress', + description: 'Delete a media item from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, mediaId: { type: 'number', @@ -43,20 +41,14 @@ export const deleteMediaTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') // Media deletion requires force=true to actually delete - return `${baseUrl}/wp-json/wp/v2/media/${params.mediaId}?force=true` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/media/${params.mediaId}?force=true` }, method: 'DELETE', - headers: (params) => { - const credentials = Buffer.from( - `${params.username}:${params.applicationPassword}` - ).toString('base64') - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/delete_page.ts b/apps/sim/tools/wordpress/delete_page.ts index b41b22f24c0..9bba210512a 100644 --- a/apps/sim/tools/wordpress/delete_page.ts +++ b/apps/sim/tools/wordpress/delete_page.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressDeletePageParams, WordPressDeletePageResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressDeletePageParams, + type WordPressDeletePageResponse, +} from './types' export const deletePageTool: ToolConfig = { id: 'wordpress_delete_page', name: 'WordPress Delete Page', - description: 'Delete a page from WordPress', + description: 'Delete a page from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, pageId: { type: 'number', @@ -42,20 +40,14 @@ export const deletePageTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const forceParam = params.force ? '?force=true' : '' - return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}${forceParam}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}${forceParam}` }, method: 'DELETE', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/delete_post.ts b/apps/sim/tools/wordpress/delete_post.ts index b679a478c4d..e31a8bdd2d0 100644 --- a/apps/sim/tools/wordpress/delete_post.ts +++ b/apps/sim/tools/wordpress/delete_post.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressDeletePostParams, WordPressDeletePostResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressDeletePostParams, + type WordPressDeletePostResponse, +} from './types' export const deletePostTool: ToolConfig = { id: 'wordpress_delete_post', name: 'WordPress Delete Post', - description: 'Delete a blog post from WordPress', + description: 'Delete a blog post from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, postId: { type: 'number', @@ -42,20 +40,14 @@ export const deletePostTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const forceParam = params.force ? '?force=true' : '' - return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}${forceParam}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}${forceParam}` }, method: 'DELETE', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/get_current_user.ts b/apps/sim/tools/wordpress/get_current_user.ts index 1ff83a439bd..3ff1bb0b032 100644 --- a/apps/sim/tools/wordpress/get_current_user.ts +++ b/apps/sim/tools/wordpress/get_current_user.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressGetCurrentUserParams, WordPressGetCurrentUserResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetCurrentUserParams, + type WordPressGetCurrentUserResponse, +} from './types' export const getCurrentUserTool: ToolConfig< WordPressGetCurrentUserParams, @@ -7,45 +11,31 @@ export const getCurrentUserTool: ToolConfig< > = { id: 'wordpress_get_current_user', name: 'WordPress Get Current User', - description: 'Get information about the currently authenticated WordPress user', + description: 'Get information about the currently authenticated WordPress.com user', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, }, request: { - url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/users/me` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/users/me`, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/get_media.ts b/apps/sim/tools/wordpress/get_media.ts index 5f442e23e0a..2da7af424e6 100644 --- a/apps/sim/tools/wordpress/get_media.ts +++ b/apps/sim/tools/wordpress/get_media.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressGetMediaParams, WordPressGetMediaResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetMediaParams, + type WordPressGetMediaResponse, +} from './types' export const getMediaTool: ToolConfig = { id: 'wordpress_get_media', name: 'WordPress Get Media', - description: 'Get a single media item from WordPress by ID', + description: 'Get a single media item from WordPress.com by ID', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, mediaId: { type: 'number', @@ -35,20 +33,12 @@ export const getMediaTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/media/${params.mediaId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/media/${params.mediaId}`, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/get_page.ts b/apps/sim/tools/wordpress/get_page.ts index ae660f5c0bb..48e4d84b143 100644 --- a/apps/sim/tools/wordpress/get_page.ts +++ b/apps/sim/tools/wordpress/get_page.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressGetPageParams, WordPressGetPageResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetPageParams, + type WordPressGetPageResponse, +} from './types' export const getPageTool: ToolConfig = { id: 'wordpress_get_page', name: 'WordPress Get Page', - description: 'Get a single page from WordPress by ID', + description: 'Get a single page from WordPress.com by ID', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, pageId: { type: 'number', @@ -35,20 +33,12 @@ export const getPageTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}`, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/get_post.ts b/apps/sim/tools/wordpress/get_post.ts index 0ba20be687d..2f12c6eeb75 100644 --- a/apps/sim/tools/wordpress/get_post.ts +++ b/apps/sim/tools/wordpress/get_post.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressGetPostParams, WordPressGetPostResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetPostParams, + type WordPressGetPostResponse, +} from './types' export const getPostTool: ToolConfig = { id: 'wordpress_get_post', name: 'WordPress Get Post', - description: 'Get a single blog post from WordPress by ID', + description: 'Get a single blog post from WordPress.com by ID', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, postId: { type: 'number', @@ -35,20 +33,12 @@ export const getPostTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}`, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/get_user.ts b/apps/sim/tools/wordpress/get_user.ts index 5b88ebd1c2d..f733bfb1324 100644 --- a/apps/sim/tools/wordpress/get_user.ts +++ b/apps/sim/tools/wordpress/get_user.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressGetUserParams, WordPressGetUserResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetUserParams, + type WordPressGetUserResponse, +} from './types' export const getUserTool: ToolConfig = { id: 'wordpress_get_user', name: 'WordPress Get User', - description: 'Get a specific user from WordPress by ID', + description: 'Get a specific user from WordPress.com by ID', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, userId: { type: 'number', @@ -35,20 +33,12 @@ export const getUserTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/users/${params.userId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/users/${params.userId}`, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_categories.ts b/apps/sim/tools/wordpress/list_categories.ts index 4dbeb4277c5..3cd76d8713f 100644 --- a/apps/sim/tools/wordpress/list_categories.ts +++ b/apps/sim/tools/wordpress/list_categories.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListCategoriesParams, WordPressListCategoriesResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListCategoriesParams, + type WordPressListCategoriesResponse, +} from './types' export const listCategoriesTool: ToolConfig< WordPressListCategoriesParams, @@ -7,27 +11,21 @@ export const listCategoriesTool: ToolConfig< > = { id: 'wordpress_list_categories', name: 'WordPress List Categories', - description: 'List categories from WordPress', + description: 'List categories from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -47,24 +45,6 @@ export const listCategoriesTool: ToolConfig< visibility: 'user-or-llm', description: 'Search term to filter categories', }, - parent: { - type: 'number', - required: false, - visibility: 'user-only', - description: 'Filter by parent category ID', - }, - hideEmpty: { - type: 'boolean', - required: false, - visibility: 'user-only', - description: 'Hide categories with no posts', - }, - orderBy: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Order by field: id, name, slug, count', - }, order: { type: 'string', required: false, @@ -75,30 +55,21 @@ export const listCategoriesTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) if (params.page) queryParams.append('page', String(params.page)) if (params.search) queryParams.append('search', params.search) - if (params.parent !== undefined) queryParams.append('parent', String(params.parent)) - if (params.hideEmpty !== undefined) queryParams.append('hide_empty', String(params.hideEmpty)) - if (params.orderBy) queryParams.append('orderby', params.orderBy) if (params.order) queryParams.append('order', params.order) const queryString = queryParams.toString() - return `${baseUrl}/wp-json/wp/v2/categories${queryString ? `?${queryString}` : ''}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/categories${queryString ? `?${queryString}` : ''}` }, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_comments.ts b/apps/sim/tools/wordpress/list_comments.ts index bd5bed20283..efbef5b83bc 100644 --- a/apps/sim/tools/wordpress/list_comments.ts +++ b/apps/sim/tools/wordpress/list_comments.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListCommentsParams, WordPressListCommentsResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListCommentsParams, + type WordPressListCommentsResponse, +} from './types' export const listCommentsTool: ToolConfig< WordPressListCommentsParams, @@ -7,27 +11,21 @@ export const listCommentsTool: ToolConfig< > = { id: 'wordpress_list_comments', name: 'WordPress List Comments', - description: 'List comments from WordPress with optional filters', + description: 'List comments from WordPress.com with optional filters', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -75,7 +73,6 @@ export const listCommentsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) @@ -87,18 +84,13 @@ export const listCommentsTool: ToolConfig< if (params.order) queryParams.append('order', params.order) const queryString = queryParams.toString() - return `${baseUrl}/wp-json/wp/v2/comments${queryString ? `?${queryString}` : ''}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments${queryString ? `?${queryString}` : ''}` }, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_media.ts b/apps/sim/tools/wordpress/list_media.ts index 8f67a511620..d794ee9fbd6 100644 --- a/apps/sim/tools/wordpress/list_media.ts +++ b/apps/sim/tools/wordpress/list_media.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListMediaParams, WordPressListMediaResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListMediaParams, + type WordPressListMediaResponse, +} from './types' export const listMediaTool: ToolConfig = { id: 'wordpress_list_media', name: 'WordPress List Media', - description: 'List media items from the WordPress media library', + description: 'List media items from the WordPress.com media library', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -72,7 +70,6 @@ export const listMediaTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) @@ -84,18 +81,13 @@ export const listMediaTool: ToolConfig { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_pages.ts b/apps/sim/tools/wordpress/list_pages.ts index 8e14a2f1414..09bc8da275c 100644 --- a/apps/sim/tools/wordpress/list_pages.ts +++ b/apps/sim/tools/wordpress/list_pages.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListPagesParams, WordPressListPagesResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListPagesParams, + type WordPressListPagesResponse, +} from './types' export const listPagesTool: ToolConfig = { id: 'wordpress_list_pages', name: 'WordPress List Pages', - description: 'List pages from WordPress with optional filters', + description: 'List pages from WordPress.com with optional filters', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -72,7 +70,6 @@ export const listPagesTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) @@ -84,18 +81,13 @@ export const listPagesTool: ToolConfig { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_posts.ts b/apps/sim/tools/wordpress/list_posts.ts index 2cf88e4ad11..ac02eca8969 100644 --- a/apps/sim/tools/wordpress/list_posts.ts +++ b/apps/sim/tools/wordpress/list_posts.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListPostsParams, WordPressListPostsResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListPostsParams, + type WordPressListPostsResponse, +} from './types' export const listPostsTool: ToolConfig = { id: 'wordpress_list_posts', name: 'WordPress List Posts', - description: 'List blog posts from WordPress with optional filters', + description: 'List blog posts from WordPress.com with optional filters', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -84,7 +82,6 @@ export const listPostsTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) @@ -112,18 +109,13 @@ export const listPostsTool: ToolConfig { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_tags.ts b/apps/sim/tools/wordpress/list_tags.ts index 55657c152ad..91ba9717e25 100644 --- a/apps/sim/tools/wordpress/list_tags.ts +++ b/apps/sim/tools/wordpress/list_tags.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListTagsParams, WordPressListTagsResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListTagsParams, + type WordPressListTagsResponse, +} from './types' export const listTagsTool: ToolConfig = { id: 'wordpress_list_tags', name: 'WordPress List Tags', - description: 'List tags from WordPress', + description: 'List tags from WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -44,18 +42,6 @@ export const listTagsTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) if (params.page) queryParams.append('page', String(params.page)) if (params.search) queryParams.append('search', params.search) - if (params.hideEmpty !== undefined) queryParams.append('hide_empty', String(params.hideEmpty)) - if (params.orderBy) queryParams.append('orderby', params.orderBy) if (params.order) queryParams.append('order', params.order) const queryString = queryParams.toString() - return `${baseUrl}/wp-json/wp/v2/tags${queryString ? `?${queryString}` : ''}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/tags${queryString ? `?${queryString}` : ''}` }, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/list_users.ts b/apps/sim/tools/wordpress/list_users.ts index fa764a07386..982c86fe046 100644 --- a/apps/sim/tools/wordpress/list_users.ts +++ b/apps/sim/tools/wordpress/list_users.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressListUsersParams, WordPressListUsersResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListUsersParams, + type WordPressListUsersResponse, +} from './types' export const listUsersTool: ToolConfig = { id: 'wordpress_list_users', name: 'WordPress List Users', - description: 'List users from WordPress (requires admin privileges)', + description: 'List users from WordPress.com (requires admin privileges)', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, perPage: { type: 'number', @@ -50,12 +48,6 @@ export const listUsersTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() if (params.perPage) queryParams.append('per_page', String(params.perPage)) if (params.page) queryParams.append('page', String(params.page)) if (params.search) queryParams.append('search', params.search) if (params.roles) queryParams.append('roles', params.roles) - if (params.orderBy) queryParams.append('orderby', params.orderBy) if (params.order) queryParams.append('order', params.order) const queryString = queryParams.toString() - return `${baseUrl}/wp-json/wp/v2/users${queryString ? `?${queryString}` : ''}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/users${queryString ? `?${queryString}` : ''}` }, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/search_content.ts b/apps/sim/tools/wordpress/search_content.ts index f60ce5b3323..f26d779097c 100644 --- a/apps/sim/tools/wordpress/search_content.ts +++ b/apps/sim/tools/wordpress/search_content.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressSearchContentParams, WordPressSearchContentResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressSearchContentParams, + type WordPressSearchContentResponse, +} from './types' export const searchContentTool: ToolConfig< WordPressSearchContentParams, @@ -7,27 +11,21 @@ export const searchContentTool: ToolConfig< > = { id: 'wordpress_search_content', name: 'WordPress Search Content', - description: 'Search across all content types in WordPress (posts, pages, media)', + description: 'Search across all content types in WordPress.com (posts, pages, media)', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, query: { type: 'string', @@ -63,7 +61,6 @@ export const searchContentTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') const queryParams = new URLSearchParams() queryParams.append('search', params.query) @@ -72,18 +69,13 @@ export const searchContentTool: ToolConfig< if (params.type) queryParams.append('type', params.type) if (params.subtype) queryParams.append('subtype', params.subtype) - return `${baseUrl}/wp-json/wp/v2/search?${queryParams.toString()}` + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/search?${queryParams.toString()}` }, method: 'GET', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/wordpress/types.ts b/apps/sim/tools/wordpress/types.ts index 3758040fa1e..faa2bd33969 100644 --- a/apps/sim/tools/wordpress/types.ts +++ b/apps/sim/tools/wordpress/types.ts @@ -1,13 +1,16 @@ // Common types for WordPress REST API tools import type { ToolResponse } from '@/tools/types' -// Common parameters for all WordPress tools (Application Password auth) +// Common parameters for all WordPress tools (WordPress.com OAuth) +// Note: accessToken is injected by the OAuth system at runtime, not defined in tool params export interface WordPressBaseParams { - siteUrl: string // WordPress site URL (e.g., https://example.com) - username: string // WordPress username - applicationPassword: string // Application Password + siteId: string // WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com) + accessToken: string // OAuth access token (injected by OAuth system) } +// WordPress.com API base URL +export const WORDPRESS_COM_API_BASE = 'https://public-api.wordpress.com/wp/v2/sites' + // Post status types export type PostStatus = 'publish' | 'draft' | 'pending' | 'private' | 'future' @@ -457,9 +460,6 @@ export interface WordPressListCategoriesParams extends WordPressBaseParams { perPage?: number page?: number search?: string - parent?: number - hideEmpty?: boolean - orderBy?: 'id' | 'name' | 'slug' | 'count' order?: 'asc' | 'desc' } @@ -499,8 +499,6 @@ export interface WordPressListTagsParams extends WordPressBaseParams { perPage?: number page?: number search?: string - hideEmpty?: boolean - orderBy?: 'id' | 'name' | 'slug' | 'count' order?: 'asc' | 'desc' } @@ -546,7 +544,6 @@ export interface WordPressListUsersParams extends WordPressBaseParams { page?: number search?: string roles?: string - orderBy?: 'id' | 'name' | 'slug' | 'email' | 'registered_date' order?: 'asc' | 'desc' } diff --git a/apps/sim/tools/wordpress/update_comment.ts b/apps/sim/tools/wordpress/update_comment.ts index 2e5eddb0c45..bda66c92ff7 100644 --- a/apps/sim/tools/wordpress/update_comment.ts +++ b/apps/sim/tools/wordpress/update_comment.ts @@ -1,5 +1,9 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressUpdateCommentParams, WordPressUpdateCommentResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressUpdateCommentParams, + type WordPressUpdateCommentResponse, +} from './types' export const updateCommentTool: ToolConfig< WordPressUpdateCommentParams, @@ -7,27 +11,21 @@ export const updateCommentTool: ToolConfig< > = { id: 'wordpress_update_comment', name: 'WordPress Update Comment', - description: 'Update a comment in WordPress (content or status)', + description: 'Update a comment in WordPress.com (content or status)', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, commentId: { type: 'number', @@ -50,20 +48,12 @@ export const updateCommentTool: ToolConfig< }, request: { - url: (params) => { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/comments/${params.commentId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments/${params.commentId}`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/wordpress/update_page.ts b/apps/sim/tools/wordpress/update_page.ts index 27ae17705fe..48f3390f653 100644 --- a/apps/sim/tools/wordpress/update_page.ts +++ b/apps/sim/tools/wordpress/update_page.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressUpdatePageParams, WordPressUpdatePageResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressUpdatePageParams, + type WordPressUpdatePageResponse, +} from './types' export const updatePageTool: ToolConfig = { id: 'wordpress_update_page', name: 'WordPress Update Page', - description: 'Update an existing page in WordPress', + description: 'Update an existing page in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, pageId: { type: 'number', @@ -83,20 +81,12 @@ export const updatePageTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/pages/${params.pageId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/wordpress/update_post.ts b/apps/sim/tools/wordpress/update_post.ts index 75753328a66..050caf10367 100644 --- a/apps/sim/tools/wordpress/update_post.ts +++ b/apps/sim/tools/wordpress/update_post.ts @@ -1,30 +1,28 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressUpdatePostParams, WordPressUpdatePostResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressUpdatePostParams, + type WordPressUpdatePostResponse, +} from './types' export const updatePostTool: ToolConfig = { id: 'wordpress_update_post', name: 'WordPress Update Post', - description: 'Update an existing blog post in WordPress', + description: 'Update an existing blog post in WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, postId: { type: 'number', @@ -83,20 +81,12 @@ export const updatePostTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/posts/${params.postId}` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}`, method: 'POST', - headers: (params) => { - const credentials = Buffer.from(`${params.username}:${params.applicationPassword}`).toString( - 'base64' - ) - return { - 'Content-Type': 'application/json', - Authorization: `Basic ${credentials}`, - } - }, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), body: (params) => { const body: Record = {} diff --git a/apps/sim/tools/wordpress/upload_media.ts b/apps/sim/tools/wordpress/upload_media.ts index ff1306688d1..2d91e053486 100644 --- a/apps/sim/tools/wordpress/upload_media.ts +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -1,31 +1,29 @@ import type { ToolConfig } from '@/tools/types' -import type { WordPressUploadMediaParams, WordPressUploadMediaResponse } from './types' +import { + WORDPRESS_COM_API_BASE, + type WordPressUploadMediaParams, + type WordPressUploadMediaResponse, +} from './types' export const uploadMediaTool: ToolConfig = { id: 'wordpress_upload_media', name: 'WordPress Upload Media', - description: 'Upload a media file (image, video, document) to WordPress', + description: 'Upload a media file (image, video, document) to WordPress.com', version: '1.0.0', + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + params: { - siteUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress site URL (e.g., https://example.com)', - }, - username: { + siteId: { type: 'string', required: true, visibility: 'user-only', - description: 'WordPress username', - }, - applicationPassword: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'WordPress Application Password', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', }, file: { type: 'string', @@ -66,16 +64,9 @@ export const uploadMediaTool: ToolConfig { - const baseUrl = params.siteUrl.replace(/\/$/, '') - return `${baseUrl}/wp-json/wp/v2/media` - }, + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/media`, method: 'POST', headers: (params) => { - const credentials = Buffer.from( - `${params.username}:${params.applicationPassword}` - ).toString('base64') - // Determine content type from filename const ext = params.filename.split('.').pop()?.toLowerCase() || '' const mimeTypes: Record = { @@ -97,7 +88,7 @@ export const uploadMediaTool: ToolConfig { From 1602beec69736eb99e4879b036a8c1a0cabd3360 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 19:25:57 -0800 Subject: [PATCH 09/11] cleanup --- .../api/auth/oauth2/callback/shopify/route.ts | 17 +---------------- .../api/tools/ssh/check-command-exists/route.ts | 3 --- apps/sim/app/api/tools/ssh/move-rename/route.ts | 4 ---- .../api/tools/ssh/read-file-content/route.ts | 3 --- apps/sim/app/api/tools/ssh/upload-file/route.ts | 8 -------- apps/sim/app/api/tools/ssh/utils.ts | 11 ----------- apps/sim/tools/ssh/types.ts | 11 ----------- 7 files changed, 1 insertion(+), 56 deletions(-) diff --git a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts index 3952de90fa1..f1e0e24e424 100644 --- a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts +++ b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts @@ -9,7 +9,6 @@ const logger = createLogger('ShopifyCallback') export const dynamic = 'force-dynamic' -// Shopify shop domain must match this pattern const SHOP_DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/ /** @@ -22,7 +21,6 @@ function validateHmac(searchParams: URLSearchParams, clientSecret: string): bool return false } - // Create a copy of params without hmac for validation const params: Record = {} searchParams.forEach((value, key) => { if (key !== 'hmac') { @@ -30,16 +28,13 @@ function validateHmac(searchParams: URLSearchParams, clientSecret: string): bool } }) - // Sort parameters alphabetically and create query string const message = Object.keys(params) .sort() .map((key) => `${key}=${params[key]}`) .join('&') - // Generate HMAC using SHA256 const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex') - // Use timing-safe comparison to prevent timing attacks try { return crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(generatedHmac, 'hex')) } catch { @@ -61,7 +56,6 @@ export async function GET(request: NextRequest) { const state = searchParams.get('state') const shop = searchParams.get('shop') - // Get stored values from cookies const storedState = request.cookies.get('shopify_oauth_state')?.value const storedShop = request.cookies.get('shopify_shop_domain')?.value @@ -73,13 +67,11 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_config_error`) } - // Validate HMAC signature from Shopify if (!validateHmac(searchParams, clientSecret)) { logger.error('HMAC validation failed in Shopify OAuth callback') return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_hmac_invalid`) } - // Validate state to prevent CSRF if (!state || state !== storedState) { logger.error('State mismatch in Shopify OAuth callback') return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_state_mismatch`) @@ -90,20 +82,17 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_code`) } - // Use shop from query params or cookie const shopDomain = shop || storedShop if (!shopDomain) { logger.error('No shop domain available') return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_shop`) } - // Validate shop domain format to prevent injection attacks if (!SHOP_DOMAIN_REGEX.test(shopDomain)) { logger.error('Invalid shop domain format:', { shopDomain }) return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_shop`) } - // Exchange code for access token const tokenResponse = await fetch(`https://${shopDomain}/admin/oauth/access_token`, { method: 'POST', headers: { @@ -139,18 +128,15 @@ export async function GET(request: NextRequest) { return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_token`) } - // Store the token - redirect to store endpoint with token data const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/shopify/store`) - // Create response with redirect to store page that will handle token storage const response = NextResponse.redirect(storeUrl) - // Pass token data via secure cookies (will be consumed by store endpoint) response.cookies.set('shopify_pending_token', accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', - maxAge: 60, // 1 minute - just enough to complete the flow + maxAge: 60, path: '/', }) @@ -170,7 +156,6 @@ export async function GET(request: NextRequest) { path: '/', }) - // Clear the OAuth state cookies response.cookies.delete('shopify_oauth_state') response.cookies.delete('shopify_shop_domain') diff --git a/apps/sim/app/api/tools/ssh/check-command-exists/route.ts b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts index 6cdddc27b56..abed8abbfd3 100644 --- a/apps/sim/app/api/tools/ssh/check-command-exists/route.ts +++ b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts @@ -23,7 +23,6 @@ export async function POST(request: NextRequest) { const body = await request.json() const params = CheckCommandExistsSchema.parse(body) - // Validate authentication if (!params.password && !params.privateKey) { return NextResponse.json( { error: 'Either password or privateKey must be provided' }, @@ -47,7 +46,6 @@ export async function POST(request: NextRequest) { try { const escapedCommand = escapeShellArg(params.commandName) - // Use which/command to check if the command exists const result = await executeSSHCommand( client, `command -v '${escapedCommand}' 2>/dev/null || which '${escapedCommand}' 2>/dev/null` @@ -56,7 +54,6 @@ export async function POST(request: NextRequest) { const exists = result.exitCode === 0 && result.stdout.trim().length > 0 const path = exists ? result.stdout.trim() : undefined - // Try to get version if command exists let version: string | undefined if (exists) { try { diff --git a/apps/sim/app/api/tools/ssh/move-rename/route.ts b/apps/sim/app/api/tools/ssh/move-rename/route.ts index 434ea8ba593..d0283996681 100644 --- a/apps/sim/app/api/tools/ssh/move-rename/route.ts +++ b/apps/sim/app/api/tools/ssh/move-rename/route.ts @@ -30,7 +30,6 @@ export async function POST(request: NextRequest) { const body = await request.json() const params = MoveRenameSchema.parse(body) - // Validate authentication if (!params.password && !params.privateKey) { return NextResponse.json( { error: 'Either password or privateKey must be provided' }, @@ -57,7 +56,6 @@ export async function POST(request: NextRequest) { const escapedSource = escapeShellArg(sourcePath) const escapedDest = escapeShellArg(destPath) - // Check if source exists const sourceCheck = await executeSSHCommand( client, `test -e '${escapedSource}' && echo "exists"` @@ -69,7 +67,6 @@ export async function POST(request: NextRequest) { ) } - // Check if destination exists (when overwrite is false) if (!params.overwrite) { const destCheck = await executeSSHCommand( client, @@ -83,7 +80,6 @@ export async function POST(request: NextRequest) { } } - // Move/rename const command = params.overwrite ? `mv -f '${escapedSource}' '${escapedDest}'` : `mv '${escapedSource}' '${escapedDest}'` diff --git a/apps/sim/app/api/tools/ssh/read-file-content/route.ts b/apps/sim/app/api/tools/ssh/read-file-content/route.ts index 922c50797de..3c8cb25dd40 100644 --- a/apps/sim/app/api/tools/ssh/read-file-content/route.ts +++ b/apps/sim/app/api/tools/ssh/read-file-content/route.ts @@ -38,7 +38,6 @@ export async function POST(request: NextRequest) { const body = await request.json() const params = ReadFileContentSchema.parse(body) - // Validate authentication if (!params.password && !params.privateKey) { return NextResponse.json( { error: 'Either password or privateKey must be provided' }, @@ -64,7 +63,6 @@ export async function POST(request: NextRequest) { const filePath = sanitizePath(params.path) const maxBytes = params.maxSize * 1024 * 1024 // Convert MB to bytes - // Check file stats const stats = await new Promise<{ size: number }>((resolve, reject) => { sftp.stat(filePath, (err, stats) => { if (err) { @@ -82,7 +80,6 @@ export async function POST(request: NextRequest) { ) } - // Read file content const content = await new Promise((resolve, reject) => { const chunks: Buffer[] = [] const readStream = sftp.createReadStream(filePath) diff --git a/apps/sim/app/api/tools/ssh/upload-file/route.ts b/apps/sim/app/api/tools/ssh/upload-file/route.ts index feab68a2b5b..7166856cb4c 100644 --- a/apps/sim/app/api/tools/ssh/upload-file/route.ts +++ b/apps/sim/app/api/tools/ssh/upload-file/route.ts @@ -40,7 +40,6 @@ export async function POST(request: NextRequest) { const body = await request.json() const params = UploadFileSchema.parse(body) - // Validate authentication if (!params.password && !params.privateKey) { return NextResponse.json( { error: 'Either password or privateKey must be provided' }, @@ -65,7 +64,6 @@ export async function POST(request: NextRequest) { const sftp = await getSFTP(client) const remotePath = sanitizePath(params.remotePath) - // Check if file exists and overwrite is false if (!params.overwrite) { const exists = await new Promise((resolve) => { sftp.stat(remotePath, (err) => { @@ -81,23 +79,17 @@ export async function POST(request: NextRequest) { } } - // Decode content (handle base64 for binary files) let content: Buffer try { - // Try to decode as base64 first content = Buffer.from(params.fileContent, 'base64') - // If the decoded content is not valid UTF-8 or looks like base64, use it as-is const reEncoded = content.toString('base64') if (reEncoded !== params.fileContent) { - // Not valid base64, treat as plain text content = Buffer.from(params.fileContent, 'utf-8') } } catch { - // Plain text content content = Buffer.from(params.fileContent, 'utf-8') } - // Write file await new Promise((resolve, reject) => { const writeStream = sftp.createWriteStream(remotePath, { mode: params.permissions ? Number.parseInt(params.permissions, 8) : 0o644, diff --git a/apps/sim/app/api/tools/ssh/utils.ts b/apps/sim/app/api/tools/ssh/utils.ts index 8be9a4d4ee7..76e1d2d8d96 100644 --- a/apps/sim/app/api/tools/ssh/utils.ts +++ b/apps/sim/app/api/tools/ssh/utils.ts @@ -119,13 +119,11 @@ export function createSSHConnection(config: SSHConnectionConfig): Promise } From b71db27337019e9e0767cf62fe3d3c2a2cfb6b8f Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 19:30:47 -0800 Subject: [PATCH 10/11] revert envvar dropdown changes --- .../sub-block/components/short-input/short-input.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx index b9bb1e3518a..0a2f6ad988f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx @@ -84,7 +84,6 @@ export function ShortInput({ const [isFocused, setIsFocused] = useState(false) const [copied, setCopied] = useState(false) const persistSubBlockValueRef = useRef<(value: string) => void>(() => {}) - const justPastedRef = useRef(false) // Always call the hook - hooks must be called unconditionally const webhookManagement = useWebhookManagement({ @@ -178,11 +177,6 @@ export function ShortInput({ return { show: true, searchTerm: '' } } if (event === 'change') { - // Don't show dropdown if user just pasted (they want to use the value directly) - if (justPastedRef.current) { - justPastedRef.current = false - return { show: false } - } // For API key fields, show env vars while typing without requiring '{{' return { show: true, searchTerm: value } } @@ -275,8 +269,6 @@ export function ShortInput({ // Handle paste events to ensure long values are handled correctly const handlePaste = useCallback((_e: React.ClipboardEvent) => { - // Mark that a paste just happened so we don't show env var dropdown - justPastedRef.current = true // Let the paste happen normally // Then ensure scroll positions are synced after the content is updated setTimeout(() => { From a2df626b777c35b5aef95d7f9807d227d9d2f9ab Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 3 Dec 2025 19:47:05 -0800 Subject: [PATCH 11/11] updated grafana endpoints --- apps/docs/content/docs/en/tools/grafana.mdx | 44 +----- apps/sim/blocks/blocks/grafana.ts | 11 +- apps/sim/tools/grafana/data_source_health.ts | 82 ---------- apps/sim/tools/grafana/health_check.ts | 73 --------- apps/sim/tools/grafana/index.ts | 6 - apps/sim/tools/grafana/update_alert_rule.ts | 140 ++++++++++++------ apps/sim/tools/grafana/update_dashboard.ts | 148 +++++++++++++------ apps/sim/tools/registry.ts | 4 - 8 files changed, 203 insertions(+), 305 deletions(-) delete mode 100644 apps/sim/tools/grafana/data_source_health.ts delete mode 100644 apps/sim/tools/grafana/health_check.ts diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index c54de82e663..c79e5c37c8a 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -38,46 +38,6 @@ Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data s ## Tools -### `grafana_health_check` - -Check the health status of a Grafana instance - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | -| `organizationId` | string | No | Organization ID for multi-org Grafana instances | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `commit` | string | Git commit hash of the Grafana build | -| `database` | string | Database connection status \(ok or failing\) | -| `version` | string | Grafana version number | - -### `grafana_data_source_health` - -Test the health of a specific data source connection. Note: This endpoint is deprecated since Grafana v9.0.0 and may not work on newer versions. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Grafana Service Account Token | -| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | -| `organizationId` | string | No | Organization ID for multi-org Grafana instances | -| `dataSourceId` | string | Yes | The ID or UID of the data source to check | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `status` | string | Health status \(OK or ERROR\) | -| `message` | string | Status message or error description | - ### `grafana_get_dashboard` Get a dashboard by its UID @@ -154,7 +114,7 @@ Create a new dashboard ### `grafana_update_dashboard` -Update an existing dashboard +Update an existing dashboard. Fetches the current dashboard and merges your changes. #### Input @@ -284,7 +244,7 @@ Create a new alert rule ### `grafana_update_alert_rule` -Update an existing alert rule +Update an existing alert rule. Fetches the current rule and merges your changes. #### Input diff --git a/apps/sim/blocks/blocks/grafana.ts b/apps/sim/blocks/blocks/grafana.ts index e4cc43aec27..9b164169f48 100644 --- a/apps/sim/blocks/blocks/grafana.ts +++ b/apps/sim/blocks/blocks/grafana.ts @@ -21,9 +21,6 @@ export const GrafanaBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ - // Health - { label: 'Health Check', id: 'grafana_health_check' }, - { label: 'Data Source Health', id: 'grafana_data_source_health' }, // Dashboards { label: 'List Dashboards', id: 'grafana_list_dashboards' }, { label: 'Get Dashboard', id: 'grafana_get_dashboard' }, @@ -49,7 +46,7 @@ export const GrafanaBlock: BlockConfig = { { label: 'List Folders', id: 'grafana_list_folders' }, { label: 'Create Folder', id: 'grafana_create_folder' }, ], - value: () => 'grafana_health_check', + value: () => 'grafana_list_dashboards', }, // Base Configuration (common to all operations) @@ -75,7 +72,7 @@ export const GrafanaBlock: BlockConfig = { placeholder: 'Optional - for multi-org instances', }, - // Data Source Health operation + // Data Source operations { id: 'dataSourceId', title: 'Data Source ID', @@ -84,7 +81,7 @@ export const GrafanaBlock: BlockConfig = { required: true, condition: { field: 'operation', - value: ['grafana_data_source_health', 'grafana_get_data_source'], + value: 'grafana_get_data_source', }, }, @@ -382,8 +379,6 @@ export const GrafanaBlock: BlockConfig = { ], tools: { access: [ - 'grafana_health_check', - 'grafana_data_source_health', 'grafana_get_dashboard', 'grafana_list_dashboards', 'grafana_create_dashboard', diff --git a/apps/sim/tools/grafana/data_source_health.ts b/apps/sim/tools/grafana/data_source_health.ts deleted file mode 100644 index 7ce8fb18595..00000000000 --- a/apps/sim/tools/grafana/data_source_health.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { - GrafanaDataSourceHealthParams, - GrafanaDataSourceHealthResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const dataSourceHealthTool: ToolConfig< - GrafanaDataSourceHealthParams, - GrafanaDataSourceHealthResponse -> = { - id: 'grafana_data_source_health', - name: 'Grafana Data Source Health', - description: - 'Test the health of a specific data source connection. Note: This endpoint is deprecated since Grafana v9.0.0 and may not work on newer versions.', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - dataSourceId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The ID or UID of the data source to check', - }, - }, - - request: { - url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/datasources/${params.dataSourceId}/health`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - status: data.status || 'Unknown', - message: data.message || '', - }, - } - }, - - outputs: { - status: { - type: 'string', - description: 'Health status (OK or ERROR)', - }, - message: { - type: 'string', - description: 'Status message or error description', - }, - }, -} diff --git a/apps/sim/tools/grafana/health_check.ts b/apps/sim/tools/grafana/health_check.ts deleted file mode 100644 index 46b32775702..00000000000 --- a/apps/sim/tools/grafana/health_check.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { GrafanaHealthCheckParams, GrafanaHealthCheckResponse } from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const healthCheckTool: ToolConfig = { - id: 'grafana_health_check', - name: 'Grafana Health Check', - description: 'Check the health status of a Grafana instance', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana Service Account Token', - }, - baseUrl: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Grafana instance URL (e.g., https://your-grafana.com)', - }, - organizationId: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'Organization ID for multi-org Grafana instances', - }, - }, - - request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`, - method: 'GET', - headers: (params) => { - const headers: Record = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.apiKey}`, - } - if (params.organizationId) { - headers['X-Grafana-Org-Id'] = params.organizationId - } - return headers - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - return { - success: true, - output: { - commit: data.commit || '', - database: data.database || 'ok', - version: data.version || '', - }, - } - }, - - outputs: { - commit: { - type: 'string', - description: 'Git commit hash of the Grafana build', - }, - database: { - type: 'string', - description: 'Database connection status (ok or failing)', - }, - version: { - type: 'string', - description: 'Grafana version number', - }, - }, -} diff --git a/apps/sim/tools/grafana/index.ts b/apps/sim/tools/grafana/index.ts index 5e031d6a5a6..d911a193a8c 100644 --- a/apps/sim/tools/grafana/index.ts +++ b/apps/sim/tools/grafana/index.ts @@ -2,14 +2,12 @@ import { createAlertRuleTool } from '@/tools/grafana/create_alert_rule' import { createAnnotationTool } from '@/tools/grafana/create_annotation' import { createDashboardTool } from '@/tools/grafana/create_dashboard' import { createFolderTool } from '@/tools/grafana/create_folder' -import { dataSourceHealthTool } from '@/tools/grafana/data_source_health' import { deleteAlertRuleTool } from '@/tools/grafana/delete_alert_rule' import { deleteAnnotationTool } from '@/tools/grafana/delete_annotation' import { deleteDashboardTool } from '@/tools/grafana/delete_dashboard' import { getAlertRuleTool } from '@/tools/grafana/get_alert_rule' import { getDashboardTool } from '@/tools/grafana/get_dashboard' import { getDataSourceTool } from '@/tools/grafana/get_data_source' -import { healthCheckTool } from '@/tools/grafana/health_check' import { listAlertRulesTool } from '@/tools/grafana/list_alert_rules' import { listAnnotationsTool } from '@/tools/grafana/list_annotations' import { listContactPointsTool } from '@/tools/grafana/list_contact_points' @@ -20,10 +18,6 @@ import { updateAlertRuleTool } from '@/tools/grafana/update_alert_rule' import { updateAnnotationTool } from '@/tools/grafana/update_annotation' import { updateDashboardTool } from '@/tools/grafana/update_dashboard' -// Health tools -export const grafanaHealthCheckTool = healthCheckTool -export const grafanaDataSourceHealthTool = dataSourceHealthTool - // Dashboard tools export const grafanaGetDashboardTool = getDashboardTool export const grafanaListDashboardsTool = listDashboardsTool diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index ae8febc8b51..6054c1194d7 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -1,16 +1,11 @@ -import type { - GrafanaUpdateAlertRuleParams, - GrafanaUpdateAlertRuleResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const updateAlertRuleTool: ToolConfig< - GrafanaUpdateAlertRuleParams, - GrafanaUpdateAlertRuleResponse -> = { +import type { GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +// Using ToolResponse for intermediate state since this tool fetches existing data first +export const updateAlertRuleTool: ToolConfig = { id: 'grafana_update_alert_rule', name: 'Grafana Update Alert Rule', - description: 'Update an existing alert rule', + description: 'Update an existing alert rule. Fetches the current rule and merges your changes.', version: '1.0.0', params: { @@ -101,9 +96,10 @@ export const updateAlertRuleTool: ToolConfig< }, request: { + // First, GET the existing alert rule url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, - method: 'PUT', + method: 'GET', headers: (params) => { const headers: Record = { 'Content-Type': 'application/json', @@ -114,47 +110,103 @@ export const updateAlertRuleTool: ToolConfig< } return headers }, - body: (params) => { - const body: Record = {} - - if (params.title) body.title = params.title - if (params.folderUid) body.folderUID = params.folderUid - if (params.ruleGroup) body.ruleGroup = params.ruleGroup - if (params.condition) body.condition = params.condition - if (params.forDuration) body.for = params.forDuration - if (params.noDataState) body.noDataState = params.noDataState - if (params.execErrState) body.execErrState = params.execErrState - - if (params.data) { - try { - body.data = JSON.parse(params.data) - } catch { - // Skip if invalid JSON - } + }, + + transformResponse: async (response: Response) => { + // Store the existing rule data for postProcess to use + const data = await response.json() + return { + success: true, + output: { + _existingRule: data, + }, + } + }, + + postProcess: async (result, params) => { + // Merge user changes with existing rule and PUT the complete object + const existingRule = result.output._existingRule + + if (!existingRule || !existingRule.uid) { + return { + success: false, + output: {}, + error: 'Failed to fetch existing alert rule', } + } + + // Build the updated rule by merging existing data with new params + const updatedRule: Record = { + ...existingRule, + } + + // Apply user's changes + if (params.title) updatedRule.title = params.title + if (params.folderUid) updatedRule.folderUID = params.folderUid + if (params.ruleGroup) updatedRule.ruleGroup = params.ruleGroup + if (params.condition) updatedRule.condition = params.condition + if (params.forDuration) updatedRule.for = params.forDuration + if (params.noDataState) updatedRule.noDataState = params.noDataState + if (params.execErrState) updatedRule.execErrState = params.execErrState - if (params.annotations) { - try { - body.annotations = JSON.parse(params.annotations) - } catch { - // Skip if invalid JSON + if (params.data) { + try { + updatedRule.data = JSON.parse(params.data) + } catch { + // Keep existing data if parse fails + } + } + + if (params.annotations) { + try { + updatedRule.annotations = { + ...(existingRule.annotations || {}), + ...JSON.parse(params.annotations), } + } catch { + // Keep existing annotations if parse fails } + } - if (params.labels) { - try { - body.labels = JSON.parse(params.labels) - } catch { - // Skip if invalid JSON + if (params.labels) { + try { + updatedRule.labels = { + ...(existingRule.labels || {}), + ...JSON.parse(params.labels), } + } catch { + // Keep existing labels if parse fails } + } - return body - }, - }, + // Make the PUT request with the complete merged object + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } - transformResponse: async (response: Response) => { - const data = await response.json() + const updateResponse = await fetch( + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + { + method: 'PUT', + headers, + body: JSON.stringify(updatedRule), + } + ) + + if (!updateResponse.ok) { + const errorText = await updateResponse.text() + return { + success: false, + output: {}, + error: `Failed to update alert rule: ${errorText}`, + } + } + + const data = await updateResponse.json() return { success: true, diff --git a/apps/sim/tools/grafana/update_dashboard.ts b/apps/sim/tools/grafana/update_dashboard.ts index 78561d474ef..121f6424669 100644 --- a/apps/sim/tools/grafana/update_dashboard.ts +++ b/apps/sim/tools/grafana/update_dashboard.ts @@ -1,16 +1,12 @@ -import type { - GrafanaUpdateDashboardParams, - GrafanaUpdateDashboardResponse, -} from '@/tools/grafana/types' -import type { ToolConfig } from '@/tools/types' - -export const updateDashboardTool: ToolConfig< - GrafanaUpdateDashboardParams, - GrafanaUpdateDashboardResponse -> = { +import type { GrafanaUpdateDashboardParams } from '@/tools/grafana/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +// Using ToolResponse for intermediate state since this tool fetches existing data first +export const updateDashboardTool: ToolConfig = { id: 'grafana_update_dashboard', name: 'Grafana Update Dashboard', - description: 'Update an existing dashboard', + description: + 'Update an existing dashboard. Fetches the current dashboard and merges your changes.', version: '1.0.0', params: { @@ -89,8 +85,10 @@ export const updateDashboardTool: ToolConfig< }, request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, - method: 'POST', + // First, GET the existing dashboard + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + method: 'GET', headers: (params) => { const headers: Record = { 'Content-Type': 'application/json', @@ -101,47 +99,105 @@ export const updateDashboardTool: ToolConfig< } return headers }, - body: (params) => { - const dashboard: Record = { - uid: params.dashboardUid, - } + }, - if (params.title) dashboard.title = params.title - if (params.tags) { - dashboard.tags = params.tags - .split(',') - .map((t) => t.trim()) - .filter((t) => t) - } - if (params.timezone) dashboard.timezone = params.timezone - if (params.refresh) dashboard.refresh = params.refresh - if (params.panels) { - try { - dashboard.panels = JSON.parse(params.panels) - } catch { - // Keep existing panels if parse fails - } - } + transformResponse: async (response: Response) => { + // Store the existing dashboard data for postProcess to use + const data = await response.json() + return { + success: true, + output: { + _existingDashboard: data.dashboard, + _existingMeta: data.meta, + }, + } + }, - const body: Record = { - dashboard, - overwrite: params.overwrite !== false, - } + postProcess: async (result, params) => { + // Merge user changes with existing dashboard and POST the complete object + const existingDashboard = result.output._existingDashboard + const existingMeta = result.output._existingMeta - if (params.folderUid) { - body.folderUid = params.folderUid + if (!existingDashboard || !existingDashboard.uid) { + return { + success: false, + output: {}, + error: 'Failed to fetch existing dashboard', } + } + + // Build the updated dashboard by merging existing data with new params + const updatedDashboard: Record = { + ...existingDashboard, + } + + // Apply user's changes + if (params.title) updatedDashboard.title = params.title + if (params.timezone) updatedDashboard.timezone = params.timezone + if (params.refresh) updatedDashboard.refresh = params.refresh + + if (params.tags) { + updatedDashboard.tags = params.tags + .split(',') + .map((t) => t.trim()) + .filter((t) => t) + } - if (params.message) { - body.message = params.message + if (params.panels) { + try { + updatedDashboard.panels = JSON.parse(params.panels) + } catch { + // Keep existing panels if parse fails } + } - return body - }, - }, + // Increment version for update + if (existingDashboard.version) { + updatedDashboard.version = existingDashboard.version + } - transformResponse: async (response: Response) => { - const data = await response.json() + // Build the request body + const body: Record = { + dashboard: updatedDashboard, + overwrite: params.overwrite !== false, + } + + // Use existing folder if not specified + if (params.folderUid) { + body.folderUid = params.folderUid + } else if (existingMeta?.folderUid) { + body.folderUid = existingMeta.folderUid + } + + if (params.message) { + body.message = params.message + } + + // Make the POST request with the complete merged object + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + + const updateResponse = await fetch(`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, { + method: 'POST', + headers, + body: JSON.stringify(body), + }) + + if (!updateResponse.ok) { + const errorText = await updateResponse.text() + return { + success: false, + output: {}, + error: `Failed to update dashboard: ${errorText}`, + } + } + + const data = await updateResponse.json() return { success: true, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0043de7b3ca..5a0cb06b94d 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -309,14 +309,12 @@ import { grafanaCreateAnnotationTool, grafanaCreateDashboardTool, grafanaCreateFolderTool, - grafanaDataSourceHealthTool, grafanaDeleteAlertRuleTool, grafanaDeleteAnnotationTool, grafanaDeleteDashboardTool, grafanaGetAlertRuleTool, grafanaGetDashboardTool, grafanaGetDataSourceTool, - grafanaHealthCheckTool, grafanaListAlertRulesTool, grafanaListAnnotationsTool, grafanaListContactPointsTool, @@ -1279,8 +1277,6 @@ export const tools: Record = { firecrawl_crawl: crawlTool, firecrawl_map: mapTool, firecrawl_extract: extractTool, - grafana_health_check: grafanaHealthCheckTool, - grafana_data_source_health: grafanaDataSourceHealthTool, grafana_get_dashboard: grafanaGetDashboardTool, grafana_list_dashboards: grafanaListDashboardsTool, grafana_create_dashboard: grafanaCreateDashboardTool,