From 5630e133fd9854ff12ba5404b849a0be09219022 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 3 Dec 2025 20:09:27 -0800 Subject: [PATCH] feat(tools): added zoom, elasticsearch, dropbox, kalshi, polymarket, datadog, ahrefs, gitlab, shopify, ssh, wordpress (#2175) * feat(tools): added zoom, elasticsearch, dropbox, box, datadog, ahrefs, gitlab, shopify, ssh, wordpress * added polymarket & kalshi, fixed ssh * fix search modal bg instead of bgColor, added polymarket and kalshi new endpoints * split up grafana * update docs script & docs * added more zoom endpoints * remove unused box creds * finished wordpress, shopify, kalshi * cleanup * revert envvar dropdown changes * updated grafana endpoints --- apps/docs/components/icons.tsx | 297 +++++- apps/docs/components/ui/icon-mapping.ts | 24 + apps/docs/content/docs/en/tools/ahrefs.mdx | 204 ++++ apps/docs/content/docs/en/tools/datadog.mdx | 307 ++++++ apps/docs/content/docs/en/tools/dropbox.mdx | 224 ++++ .../content/docs/en/tools/elasticsearch.mdx | 370 +++++++ apps/docs/content/docs/en/tools/gitlab.mdx | 434 ++++++++ apps/docs/content/docs/en/tools/grafana.mdx | 499 +++++++++ apps/docs/content/docs/en/tools/kalshi.mdx | 300 ++++++ apps/docs/content/docs/en/tools/meta.json | 14 +- .../docs/content/docs/en/tools/polymarket.mdx | 357 +++++++ apps/docs/content/docs/en/tools/pylon.mdx | 10 + apps/docs/content/docs/en/tools/shopify.mdx | 449 ++++++++ apps/docs/content/docs/en/tools/ssh.mdx | 399 ++++++++ apps/docs/content/docs/en/tools/wordpress.mdx | 571 +++++++++++ apps/docs/content/docs/en/tools/zendesk.mdx | 3 + apps/docs/content/docs/en/tools/zoom.mdx | 255 +++++ .../api/auth/oauth2/callback/shopify/route.ts | 167 +++ .../api/auth/oauth2/shopify/store/route.ts | 96 ++ .../app/api/auth/shopify/authorize/route.ts | 215 ++++ .../tools/ssh/check-command-exists/route.ts | 104 ++ .../api/tools/ssh/check-file-exists/route.ts | 134 +++ .../api/tools/ssh/create-directory/route.ts | 110 ++ .../app/api/tools/ssh/delete-file/route.ts | 103 ++ .../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 | 104 ++ .../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 | 117 +++ .../api/tools/ssh/read-file-content/route.ts | 132 +++ .../app/api/tools/ssh/upload-file/route.ts | 129 +++ apps/sim/app/api/tools/ssh/utils.ts | 272 +++++ .../api/tools/ssh/write-file-content/route.ts | 152 +++ .../templates/components/template-card.tsx | 4 +- .../components/oauth-required-modal.tsx | 39 + .../panel/components/editor/editor.tsx | 1 + .../search-modal/search-modal.tsx | 4 +- apps/sim/blocks/blocks/ahrefs.ts | 458 +++++++++ apps/sim/blocks/blocks/datadog.ts | 599 +++++++++++ apps/sim/blocks/blocks/dropbox.ts | 368 +++++++ apps/sim/blocks/blocks/elasticsearch.ts | 435 ++++++++ apps/sim/blocks/blocks/gitlab.ts | 697 +++++++++++++ apps/sim/blocks/blocks/grafana.ts | 503 +++++++++ apps/sim/blocks/blocks/kalshi.ts | 399 ++++++++ apps/sim/blocks/blocks/polymarket.ts | 355 +++++++ apps/sim/blocks/blocks/shopify.ts | 845 +++++++++++++++ apps/sim/blocks/blocks/ssh.ts | 518 ++++++++++ apps/sim/blocks/blocks/wordpress.ts | 958 ++++++++++++++++++ apps/sim/blocks/blocks/zoom.ts | 572 +++++++++++ apps/sim/blocks/registry.ts | 26 + apps/sim/components/icons.tsx | 297 +++++- apps/sim/hooks/queries/oauth-connections.ts | 7 + apps/sim/lib/auth/auth.ts | 171 ++++ apps/sim/lib/core/config/env.ts | 8 + apps/sim/lib/oauth/oauth.ts | 159 +++ apps/sim/lib/uploads/utils/file-utils.ts | 19 +- apps/sim/lib/uploads/utils/validation.ts | 8 +- apps/sim/package.json | 2 + .../serializer/tests/dual-validation.test.ts | 4 +- apps/sim/tools/ahrefs/backlinks.ts | 116 +++ apps/sim/tools/ahrefs/backlinks_stats.ts | 95 ++ apps/sim/tools/ahrefs/broken_backlinks.ts | 121 +++ apps/sim/tools/ahrefs/domain_rating.ts | 74 ++ apps/sim/tools/ahrefs/index.ts | 17 + apps/sim/tools/ahrefs/keyword_overview.ts | 101 ++ apps/sim/tools/ahrefs/organic_keywords.ts | 127 +++ apps/sim/tools/ahrefs/referring_domains.ts | 125 +++ apps/sim/tools/ahrefs/top_pages.ts | 119 +++ apps/sim/tools/ahrefs/types.ts | 199 ++++ apps/sim/tools/datadog/cancel_downtime.ts | 76 ++ apps/sim/tools/datadog/create_downtime.ts | 178 ++++ apps/sim/tools/datadog/create_event.ts | 163 +++ 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 | 180 ++++ 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 | 94 ++ apps/sim/tools/datadog/submit_metrics.ts | 101 ++ apps/sim/tools/datadog/types.ts | 782 ++++++++++++++ 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 | 23 + 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 | 68 ++ apps/sim/tools/gitlab/create_issue.ts | 111 ++ apps/sim/tools/gitlab/create_issue_note.ts | 77 ++ apps/sim/tools/gitlab/create_merge_request.ts | 136 +++ .../tools/gitlab/create_merge_request_note.ts | 80 ++ apps/sim/tools/gitlab/create_pipeline.ts | 86 ++ apps/sim/tools/gitlab/delete_issue.ts | 64 ++ apps/sim/tools/gitlab/get_issue.ts | 65 ++ apps/sim/tools/gitlab/get_merge_request.ts | 71 ++ apps/sim/tools/gitlab/get_pipeline.ts | 66 ++ apps/sim/tools/gitlab/get_project.ts | 60 ++ apps/sim/tools/gitlab/index.ts | 45 + apps/sim/tools/gitlab/list_issues.ts | 124 +++ apps/sim/tools/gitlab/list_merge_requests.ts | 124 +++ apps/sim/tools/gitlab/list_pipelines.ts | 110 ++ apps/sim/tools/gitlab/list_projects.ts | 114 +++ apps/sim/tools/gitlab/merge_merge_request.ts | 110 ++ apps/sim/tools/gitlab/retry_pipeline.ts | 68 ++ apps/sim/tools/gitlab/types.ts | 651 ++++++++++++ apps/sim/tools/gitlab/update_issue.ts | 121 +++ apps/sim/tools/gitlab/update_merge_request.ts | 139 +++ 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/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/get_alert_rule.ts | 123 +++ apps/sim/tools/grafana/get_dashboard.ts | 76 ++ apps/sim/tools/grafana/get_data_source.ts | 125 +++ apps/sim/tools/grafana/index.ts | 48 + 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/types.ts | 451 +++++++++ apps/sim/tools/grafana/update_alert_rule.ts | 253 +++++ apps/sim/tools/grafana/update_annotation.ts | 126 +++ apps/sim/tools/grafana/update_dashboard.ts | 241 +++++ apps/sim/tools/index.ts | 16 +- apps/sim/tools/kalshi/get_balance.ts | 89 ++ apps/sim/tools/kalshi/get_candlesticks.ts | 120 +++ apps/sim/tools/kalshi/get_event.ts | 87 ++ apps/sim/tools/kalshi/get_events.ts | 116 +++ apps/sim/tools/kalshi/get_exchange_status.ts | 75 ++ apps/sim/tools/kalshi/get_fills.ts | 138 +++ apps/sim/tools/kalshi/get_market.ts | 73 ++ apps/sim/tools/kalshi/get_markets.ts | 115 +++ apps/sim/tools/kalshi/get_orderbook.ts | 93 ++ apps/sim/tools/kalshi/get_orders.ts | 131 +++ apps/sim/tools/kalshi/get_positions.ts | 134 +++ apps/sim/tools/kalshi/get_series_by_ticker.ts | 82 ++ apps/sim/tools/kalshi/get_trades.ts | 115 +++ apps/sim/tools/kalshi/index.ts | 13 + apps/sim/tools/kalshi/types.ts | 295 ++++++ apps/sim/tools/polymarket/get_event.ts | 88 ++ apps/sim/tools/polymarket/get_events.ts | 121 +++ .../tools/polymarket/get_last_trade_price.ts | 81 ++ 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_positions.ts | 95 ++ apps/sim/tools/polymarket/get_price.ts | 89 ++ .../sim/tools/polymarket/get_price_history.ts | 114 +++ 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 | 85 ++ apps/sim/tools/polymarket/get_trades.ts | 107 ++ apps/sim/tools/polymarket/index.ts | 18 + apps/sim/tools/polymarket/search.ts | 99 ++ apps/sim/tools/polymarket/types.ts | 184 ++++ apps/sim/tools/registry.ts | 384 +++++++ apps/sim/tools/shopify/adjust_inventory.ts | 160 +++ apps/sim/tools/shopify/cancel_order.ts | 147 +++ apps/sim/tools/shopify/create_customer.ts | 209 ++++ apps/sim/tools/shopify/create_fulfillment.ts | 240 +++++ apps/sim/tools/shopify/create_product.ts | 208 ++++ apps/sim/tools/shopify/delete_customer.ts | 114 +++ apps/sim/tools/shopify/delete_product.ts | 114 +++ apps/sim/tools/shopify/get_collection.ts | 224 ++++ apps/sim/tools/shopify/get_customer.ts | 133 +++ apps/sim/tools/shopify/get_inventory_level.ts | 154 +++ apps/sim/tools/shopify/get_order.ts | 203 ++++ apps/sim/tools/shopify/get_product.ts | 127 +++ apps/sim/tools/shopify/index.ts | 29 + apps/sim/tools/shopify/list_collections.ts | 187 ++++ apps/sim/tools/shopify/list_customers.ts | 140 +++ .../sim/tools/shopify/list_inventory_items.ts | 235 +++++ apps/sim/tools/shopify/list_locations.ts | 166 +++ apps/sim/tools/shopify/list_orders.ts | 186 ++++ apps/sim/tools/shopify/list_products.ts | 151 +++ apps/sim/tools/shopify/types.ts | 378 +++++++ apps/sim/tools/shopify/update_customer.ts | 200 ++++ apps/sim/tools/shopify/update_order.ts | 165 +++ apps/sim/tools/shopify/update_product.ts | 204 ++++ 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 | 218 ++++ apps/sim/tools/ssh/upload_file.ts | 124 +++ apps/sim/tools/ssh/write_file_content.ts | 117 +++ apps/sim/tools/types.ts | 3 +- apps/sim/tools/utils.test.ts | 14 +- apps/sim/tools/utils.ts | 2 +- apps/sim/tools/wordpress/create_category.ts | 117 +++ apps/sim/tools/wordpress/create_comment.ts | 137 +++ apps/sim/tools/wordpress/create_page.ts | 154 +++ apps/sim/tools/wordpress/create_post.ts | 166 +++ apps/sim/tools/wordpress/create_tag.ts | 105 ++ apps/sim/tools/wordpress/delete_comment.ts | 108 ++ apps/sim/tools/wordpress/delete_media.ts | 108 ++ apps/sim/tools/wordpress/delete_page.ts | 111 ++ apps/sim/tools/wordpress/delete_post.ts | 111 ++ apps/sim/tools/wordpress/get_current_user.ts | 90 ++ apps/sim/tools/wordpress/get_media.ts | 93 ++ apps/sim/tools/wordpress/get_page.ts | 97 ++ apps/sim/tools/wordpress/get_post.ts | 97 ++ apps/sim/tools/wordpress/get_user.ts | 93 ++ apps/sim/tools/wordpress/index.ts | 69 ++ apps/sim/tools/wordpress/list_categories.ts | 131 +++ apps/sim/tools/wordpress/list_comments.ts | 158 +++ apps/sim/tools/wordpress/list_media.ts | 157 +++ apps/sim/tools/wordpress/list_pages.ts | 161 +++ apps/sim/tools/wordpress/list_posts.ts | 189 ++++ apps/sim/tools/wordpress/list_tags.ts | 126 +++ apps/sim/tools/wordpress/list_users.ts | 143 +++ apps/sim/tools/wordpress/search_content.ts | 131 +++ apps/sim/tools/wordpress/types.ts | 627 ++++++++++++ apps/sim/tools/wordpress/update_comment.ts | 114 +++ apps/sim/tools/wordpress/update_page.ts | 159 +++ apps/sim/tools/wordpress/update_post.ts | 171 ++++ apps/sim/tools/wordpress/upload_media.ts | 151 +++ apps/sim/tools/zendesk/get_tickets.ts | 36 +- apps/sim/tools/zoom/create_meeting.ts | 236 +++++ apps/sim/tools/zoom/delete_meeting.ts | 99 ++ apps/sim/tools/zoom/delete_recording.ts | 93 ++ apps/sim/tools/zoom/get_meeting.ts | 132 +++ apps/sim/tools/zoom/get_meeting_invitation.ts | 72 ++ apps/sim/tools/zoom/get_meeting_recordings.ts | 131 +++ apps/sim/tools/zoom/index.ts | 41 + apps/sim/tools/zoom/list_meetings.ts | 138 +++ apps/sim/tools/zoom/list_past_participants.ts | 127 +++ apps/sim/tools/zoom/list_recordings.ts | 159 +++ apps/sim/tools/zoom/types.ts | 302 ++++++ apps/sim/tools/zoom/update_meeting.ts | 196 ++++ bun.lock | 20 + scripts/generate-docs.ts | 4 +- 272 files changed, 42658 insertions(+), 64 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/ahrefs.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/kalshi.mdx create mode 100644 apps/docs/content/docs/en/tools/polymarket.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/auth/oauth2/callback/shopify/route.ts create mode 100644 apps/sim/app/api/auth/oauth2/shopify/store/route.ts create mode 100644 apps/sim/app/api/auth/shopify/authorize/route.ts 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/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/kalshi.ts create mode 100644 apps/sim/blocks/blocks/polymarket.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/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/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 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 create mode 100644 apps/sim/tools/grafana/get_alert_rule.ts create mode 100644 apps/sim/tools/grafana/get_dashboard.ts create mode 100644 apps/sim/tools/grafana/get_data_source.ts create mode 100644 apps/sim/tools/grafana/index.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/types.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 create mode 100644 apps/sim/tools/kalshi/get_balance.ts create mode 100644 apps/sim/tools/kalshi/get_candlesticks.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_exchange_status.ts create mode 100644 apps/sim/tools/kalshi/get_fills.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_orderbook.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/get_series_by_ticker.ts create mode 100644 apps/sim/tools/kalshi/get_trades.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_last_trade_price.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_positions.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/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/index.ts create mode 100644 apps/sim/tools/polymarket/search.ts create mode 100644 apps/sim/tools/polymarket/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_fulfillment.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_collection.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_collections.ts create mode 100644 apps/sim/tools/shopify/list_customers.ts create mode 100644 apps/sim/tools/shopify/list_inventory_items.ts create mode 100644 apps/sim/tools/shopify/list_locations.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/delete_recording.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/get_meeting_recordings.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/list_past_participants.ts create mode 100644 apps/sim/tools/zoom/list_recordings.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 b85680a25..6564a6d21 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 ( + + + + + + + + ) +} + +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 0ba630bdf..86fd5822d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -4,6 +4,7 @@ import type { ComponentType, SVGProps } from 'react' import { + AhrefsIcon, AirtableIcon, ApifyIcon, ApolloIcon, @@ -14,14 +15,18 @@ import { CalendlyIcon, ClayIcon, ConfluenceIcon, + DatadogIcon, DiscordIcon, DocumentIcon, + DropboxIcon, DynamoDBIcon, + ElasticsearchIcon, ElevenLabsIcon, ExaAIIcon, EyeIcon, FirecrawlIcon, GithubIcon, + GitLabIcon, GmailIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -30,6 +35,7 @@ import { GoogleIcon, GoogleSheetsIcon, GoogleVaultIcon, + GrafanaIcon, HubspotIcon, HuggingFaceIcon, HunterIOIcon, @@ -38,6 +44,7 @@ import { IntercomIcon, JinaAIIcon, JiraIcon, + KalshiIcon, LinearIcon, LinkedInIcon, LinkupIcon, @@ -61,6 +68,7 @@ import { PerplexityIcon, PineconeIcon, PipedriveIcon, + PolymarketIcon, PostgresIcon, PosthogIcon, PylonIcon, @@ -74,8 +82,10 @@ import { SendgridIcon, SentryIcon, SerperIcon, + ShopifyIcon, SlackIcon, SmtpIcon, + SshIcon, STTIcon, StagehandIcon, StripeIcon, @@ -92,19 +102,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 +139,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, @@ -141,6 +157,7 @@ export const blockTypeToIconMap: Record = { pylon: PylonIcon, posthog: PosthogIcon, postgresql: PostgresIcon, + polymarket: PolymarketIcon, pipedrive: PipedriveIcon, pinecone: PineconeIcon, perplexity: PerplexityIcon, @@ -164,6 +181,7 @@ export const blockTypeToIconMap: Record = { linkedin: LinkedInIcon, linear: LinearIcon, knowledge: PackageSearchIcon, + kalshi: KalshiIcon, jira: JiraIcon, jina: JinaAIIcon, intercom: IntercomIcon, @@ -172,6 +190,7 @@ export const blockTypeToIconMap: Record = { hunter: HunterIOIcon, huggingface: HuggingFaceIcon, hubspot: HubspotIcon, + grafana: GrafanaIcon, google_vault: GoogleVaultIcon, google_sheets: GoogleSheetsIcon, google_forms: GoogleFormsIcon, @@ -180,13 +199,17 @@ 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, @@ -196,4 +219,5 @@ export const blockTypeToIconMap: Record = { 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 000000000..4943e936a --- /dev/null +++ b/apps/docs/content/docs/en/tools/ahrefs.mdx @@ -0,0 +1,204 @@ +--- +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\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | +| `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\) | +| `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 | + +#### 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\) | +| `date` | string | No | Date for historical data in YYYY-MM-DD format \(defaults to today\) | +| `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\) | +| `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 | + +#### 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\) | +| `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 | + +#### 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\) | +| `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 | + +#### 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\) | +| `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 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `brokenBacklinks` | array | List of broken backlinks | + + + +## Notes + +- Category: `tools` +- Type: `ahrefs` 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 000000000..45978068a --- /dev/null +++ b/apps/docs/content/docs/en/tools/datadog.mdx @@ -0,0 +1,307 @@ +--- +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 | +| `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 | +| `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 | +| `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 000000000..399d06a00 --- /dev/null +++ b/apps/docs/content/docs/en/tools/dropbox.mdx @@ -0,0 +1,224 @@ +--- +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 | +| --------- | ---- | -------- | ----------- | +| `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 + +| 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 000000000..6c2fd618f --- /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 000000000..f5b71f443 --- /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 number within the project \(the # shown in GitLab UI\) | + +#### 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 000000000..c79e5c37c --- /dev/null +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -0,0 +1,499 @@ +--- +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_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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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. Fetches the current dashboard and merges your changes. + +#### 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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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. Fetches the current rule and merges your changes. + +#### 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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | +| --------- | ---- | ----------- | +| `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 | + + + +## Notes + +- Category: `tools` +- Type: `grafana` 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 000000000..2aa55a8d8 --- /dev/null +++ b/apps/docs/content/docs/en/tools/kalshi.mdx @@ -0,0 +1,300 @@ +--- +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 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 integration in Sim, you can: + +- **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. + +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 */} + + +## Usage Instructions + +Integrate Kalshi prediction markets into the workflow. Can get markets, market, events, event, balance, positions, orders, orderbook, trades, candlesticks, fills, series, and exchange status. + + + +## 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 | + +### `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_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 + +- 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 8f853df94..8226a5746 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -1,6 +1,7 @@ { "pages": [ "index", + "ahrefs", "airtable", "apify", "apollo", @@ -10,13 +11,17 @@ "calendly", "clay", "confluence", + "datadog", "discord", + "dropbox", "dynamodb", + "elasticsearch", "elevenlabs", "exa", "file", "firecrawl", "github", + "gitlab", "gmail", "google_calendar", "google_docs", @@ -25,6 +30,7 @@ "google_search", "google_sheets", "google_vault", + "grafana", "hubspot", "huggingface", "hunter", @@ -33,6 +39,7 @@ "intercom", "jina", "jira", + "kalshi", "knowledge", "linear", "linkedin", @@ -56,6 +63,7 @@ "perplexity", "pinecone", "pipedrive", + "polymarket", "postgresql", "posthog", "pylon", @@ -70,8 +78,10 @@ "sentry", "serper", "sharepoint", + "shopify", "slack", "smtp", + "ssh", "stagehand", "stagehand_agent", "stripe", @@ -92,9 +102,11 @@ "webflow", "whatsapp", "wikipedia", + "wordpress", "x", "youtube", "zendesk", - "zep" + "zep", + "zoom" ] } 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 000000000..2ce9bb986 --- /dev/null +++ b/apps/docs/content/docs/en/tools/polymarket.mdx @@ -0,0 +1,357 @@ +--- +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, market, events, event, tags, series, orderbook, price, midpoint, price history, last trade price, spread, tick size, positions, trades, and search. + + + +## 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_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 + +#### 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 | + +### `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 + +- Category: `tools` +- Type: `polymarket` diff --git a/apps/docs/content/docs/en/tools/pylon.mdx b/apps/docs/content/docs/en/tools/pylon.mdx index ff81223f5..4ac96873f 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/shopify.mdx b/apps/docs/content/docs/en/tools/shopify.mdx new file mode 100644 index 000000000..327e935ae --- /dev/null +++ b/apps/docs/content/docs/en/tools/shopify.mdx @@ -0,0 +1,449 @@ +--- +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\) | +| `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 + +| 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, 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 | +| `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., "first_name:John" or "last_name:Smith" or "email:*@gmail.com" or "tag:vip"\) | + +#### 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_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 + +#### 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 | + +### `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 + +- 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 000000000..663ca7ac3 --- /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 000000000..214b7d0c1 --- /dev/null +++ b/apps/docs/content/docs/en/tools/wordpress.mdx @@ -0,0 +1,571 @@ +--- +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 WordPress.com sites via OAuth and self-hosted WordPress sites using Application Passwords authentication. + + + +## Tools + +### `wordpress_create_post` + +Create a new blog post in WordPress.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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\) | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | + +#### 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.com by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `post` | object | The retrieved post | + +### `wordpress_list_posts` + +List blog posts from WordPress.com with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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\) | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | + +#### 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.com by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | The retrieved page | + +### `wordpress_list_pages` + +List pages from WordPress.com with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `media` | object | The retrieved media item | + +### `wordpress_list_media` + +List media items from the WordPress.com media library + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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\) | + +#### 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.com post + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com with optional filters + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com (content or status) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | The updated comment | + +### `wordpress_delete_comment` + +Delete a comment from WordPress.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | + +#### 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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `slug` | string | No | URL slug for the category | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `category` | object | The created category | + +### `wordpress_list_categories` + +List categories from WordPress.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tag` | object | The created tag | + +### `wordpress_list_tags` + +List tags from WordPress.com + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The current user | + +### `wordpress_list_users` + +List users from WordPress.com (requires admin privileges) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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.com by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The retrieved user | + +### `wordpress_search_content` + +Search across all content types in WordPress.com (posts, pages, media) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `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 | +| `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/zendesk.mdx b/apps/docs/content/docs/en/tools/zendesk.mdx index 9be814476..a5b109981 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/apps/docs/content/docs/en/tools/zoom.mdx b/apps/docs/content/docs/en/tools/zoom.mdx new file mode 100644 index 000000000..60cf268e5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/zoom.mdx @@ -0,0 +1,255 @@ +--- +title: Zoom +description: Create and manage Zoom meetings and recordings +--- + +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, recordings, and participants. Manage cloud recordings 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 | + +### `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 + +- Category: `tools` +- Type: `zoom` diff --git a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts new file mode 100644 index 000000000..f1e0e24e4 --- /dev/null +++ b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts @@ -0,0 +1,167 @@ +import crypto from 'crypto' +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' + +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 + } + + const params: Record = {} + searchParams.forEach((value, key) => { + if (key !== 'hmac') { + params[key] = value + } + }) + + const message = Object.keys(params) + .sort() + .map((key) => `${key}=${params[key]}`) + .join('&') + + const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex') + + try { + return crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(generatedHmac, 'hex')) + } catch { + return false + } +} + +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') + + 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`) + } + + if (!validateHmac(searchParams, clientSecret)) { + logger.error('HMAC validation failed in Shopify OAuth callback') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_hmac_invalid`) + } + + 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`) + } + + const shopDomain = shop || storedShop + if (!shopDomain) { + logger.error('No shop domain available') + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_no_shop`) + } + + if (!SHOP_DOMAIN_REGEX.test(shopDomain)) { + logger.error('Invalid shop domain format:', { shopDomain }) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_invalid_shop`) + } + + 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 + + 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`) + } + + const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/shopify/store`) + + const response = NextResponse.redirect(storeUrl) + + response.cookies.set('shopify_pending_token', accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60, + 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: '/', + }) + + 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/oauth2/shopify/store/route.ts b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts new file mode 100644 index 000000000..84f4ad06e --- /dev/null +++ b/apps/sim/app/api/auth/oauth2/shopify/store/route.ts @@ -0,0 +1,96 @@ +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' + +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`) + } + + 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`) + } + + 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 + + const existing = await db.query.account.findFirst({ + where: and(eq(account.userId, session.user.id), eq(account.providerId, 'shopify')), + }) + + const now = new Date() + + const accountData = { + accessToken: accessToken, + accountId: shopInfo.id?.toString() || shopDomain, + scope: scope || '', + updatedAt: now, + 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 }) + } + + 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) { + logger.error('Error storing Shopify token:', error) + return NextResponse.redirect(`${baseUrl}/workspace?error=shopify_store_error`) + } +} 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 000000000..aa06b2c7b --- /dev/null +++ b/apps/sim/app/api/auth/shopify/authorize/route.ts @@ -0,0 +1,215 @@ +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' + +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) { + 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 }) + } + + const shopDomain = request.nextUrl.searchParams.get('shop') + const returnUrl = request.nextUrl.searchParams.get('returnUrl') + + if (!shopDomain) { + const returnUrlParam = returnUrl ? encodeURIComponent(returnUrl) : '' + 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', + }, + } + ) + } + + 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/oauth2/callback/shopify` + + const state = crypto.randomUUID() + + const oauthUrl = + `https://${cleanShop}/admin/oauth/authorize?` + + 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, + path: '/', + }) + + response.cookies.set('shopify_shop_domain', cleanShop, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + 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) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} 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 000000000..abed8abbf --- /dev/null +++ b/apps/sim/app/api/tools/ssh/check-command-exists/route.ts @@ -0,0 +1,104 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, escapeShellArg, 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().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), + 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) + + 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 { + const escapedCommand = escapeShellArg(params.commandName) + + const result = await executeSSHCommand( + client, + `command -v '${escapedCommand}' 2>/dev/null || which '${escapedCommand}' 2>/dev/null` + ) + + const exists = result.exitCode === 0 && result.stdout.trim().length > 0 + const path = exists ? result.stdout.trim() : undefined + + let version: string | undefined + if (exists) { + try { + const versionResult = await executeSSHCommand( + client, + `'${escapedCommand}' --version 2>&1 | head -1 || '${escapedCommand}' -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 000000000..0830488db --- /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().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'), +}) + +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 000000000..06f7c412f --- /dev/null +++ b/apps/sim/app/api/tools/ssh/create-directory/route.ts @@ -0,0 +1,110 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + createSSHConnection, + escapeShellArg, + 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().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'), +}) + +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) + const escapedPath = escapeShellArg(dirPath) + + // Check if directory already exists + const checkResult = await executeSSHCommand( + client, + `test -d '${escapedPath}' && 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} '${escapedPath}'` + 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 000000000..a1cb694fa --- /dev/null +++ b/apps/sim/app/api/tools/ssh/delete-file/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, + escapeShellArg, + 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().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), +}) + +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) + const escapedPath = escapeShellArg(filePath) + + // Check if path 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 }) + } + + // Build delete command + let command: string + if (params.recursive) { + command = params.force ? `rm -rf '${escapedPath}'` : `rm -r '${escapedPath}'` + } else { + command = params.force ? `rm -f '${escapedPath}'` : `rm '${escapedPath}'` + } + + 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 000000000..03f5b2cfa --- /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().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), + 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 000000000..c553b3554 --- /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().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), + command: z.string().min(1, 'Command is required'), + workingDirectory: z.string().nullish(), +}) + +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 000000000..0e7e44abf --- /dev/null +++ b/apps/sim/app/api/tools/ssh/execute-script/route.ts @@ -0,0 +1,104 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createSSHConnection, escapeShellArg, 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().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().nullish(), +}) + +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` + const escapedScriptPath = escapeShellArg(scriptPath) + const escapedInterpreter = escapeShellArg(params.interpreter) + + // Build the command to create, execute, and clean up the script + // Note: heredoc with quoted delimiter ('SIMEOF') prevents variable expansion + let command = `cat > '${escapedScriptPath}' << 'SIMEOF' +${params.script} +SIMEOF +chmod +x '${escapedScriptPath}'` + + if (params.workingDirectory) { + const escapedWorkDir = escapeShellArg(params.workingDirectory) + command += ` +cd '${escapedWorkDir}'` + } + + command += ` +'${escapedInterpreter}' '${escapedScriptPath}' +exit_code=$? +rm -f '${escapedScriptPath}' +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 000000000..9f88415e5 --- /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().nullish(), + privateKey: z.string().nullish(), + passphrase: z.string().nullish(), +}) + +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 000000000..dbcfeb78e --- /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().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), +}) + +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 000000000..d02839966 --- /dev/null +++ b/apps/sim/app/api/tools/ssh/move-rename/route.ts @@ -0,0 +1,117 @@ +import { randomUUID } from 'crypto' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + createSSHConnection, + escapeShellArg, + 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().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), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = MoveRenameSchema.parse(body) + + 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) + const escapedSource = escapeShellArg(sourcePath) + const escapedDest = escapeShellArg(destPath) + + const sourceCheck = await executeSSHCommand( + client, + `test -e '${escapedSource}' && echo "exists"` + ) + if (sourceCheck.stdout.trim() !== 'exists') { + return NextResponse.json( + { error: `Source path does not exist: ${sourcePath}` }, + { status: 404 } + ) + } + + if (!params.overwrite) { + 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}` }, + { status: 409 } + ) + } + } + + const command = params.overwrite + ? `mv -f '${escapedSource}' '${escapedDest}'` + : `mv '${escapedSource}' '${escapedDest}'` + 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 000000000..3c8cb25dd --- /dev/null +++ b/apps/sim/app/api/tools/ssh/read-file-content/route.ts @@ -0,0 +1,132 @@ +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().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 +}) + +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) + + 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 + + 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 } + ) + } + + 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 000000000..7166856cb --- /dev/null +++ b/apps/sim/app/api/tools/ssh/upload-file/route.ts @@ -0,0 +1,129 @@ +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().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().nullish(), + 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) + + 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) + + 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 } + ) + } + } + + let content: Buffer + try { + content = Buffer.from(params.fileContent, 'base64') + const reEncoded = content.toString('base64') + if (reEncoded !== params.fileContent) { + content = Buffer.from(params.fileContent, 'utf-8') + } + } catch { + content = Buffer.from(params.fileContent, 'utf-8') + } + + 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 000000000..76e1d2d8d --- /dev/null +++ b/apps/sim/app/api/tools/ssh/utils.ts @@ -0,0 +1,272 @@ +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 | null + privateKey?: string | null + passphrase?: string | null + timeout?: number + keepaliveInterval?: number + readyTimeout?: number +} + +export interface SSHCommandResult { + stdout: string + stderr: string + 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 + + if (!host || host.trim() === '') { + reject(new Error('Host is required. Please provide a valid hostname or IP address.')) + return + } + + 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: host.trim(), + port, + username: config.username, + } + + if (config.readyTimeout !== undefined) { + connectConfig.readyTimeout = config.readyTimeout + } + if (config.keepaliveInterval !== undefined) { + connectConfig.keepaliveInterval = config.keepaliveInterval + } + + if (hasPrivateKey) { + connectConfig.privateKey = config.privateKey! + if (config.passphrase && config.passphrase.trim() !== '') { + connectConfig.passphrase = config.passphrase + } + } else if (hasPassword) { + connectConfig.password = config.password! + } + + client.on('ready', () => { + resolve(client) + }) + + client.on('error', (err) => { + reject(formatSSHError(err, { host, port })) + }) + + try { + client.connect(connectConfig) + } catch (err) { + reject(formatSSHError(err instanceof Error ? err : new Error(String(err)), { host, port })) + } + }) +} + +/** + * 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 { + return command.trim() +} + +/** + * Sanitize file path - removes null bytes and trims whitespace + */ +export function sanitizePath(path: string): string { + let sanitized = path.replace(/\0/g, '') + + sanitized = sanitized.trim() + + 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 + */ +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 000000000..5ba727401 --- /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().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().nullish(), +}) + +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/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 0bd1686ef..f6fe4ead2 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/[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 e98c3a880..13d8c6555 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,38 @@ 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 (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', + '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', + // 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 { @@ -289,6 +321,13 @@ export function OAuthRequiredModal({ return } + if (providerId === 'shopify') { + // 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 + } + await client.oauth2.link({ providerId, callbackURL: window.location.href, 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 531207547..a26190a92 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/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 25cce42b2..a7cdba45c 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({
= { + 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, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + }, + { + 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', + 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' }, + 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' }, + }, + 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/datadog.ts b/apps/sim/blocks/blocks/datadog.ts new file mode 100644 index 000000000..d0da31eb6 --- /dev/null +++ b/apps/sim/blocks/blocks/datadog.ts @@ -0,0 +1,599 @@ +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, + }, + // 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, + }, + { + 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) => params.operation, + params: (params) => { + // Base params that are always needed + const baseParams: Record = { + apiKey: params.apiKey, + applicationKey: params.applicationKey, + site: params.site, + } + + // Only include params relevant to each operation + switch (params.operation) { + case 'datadog_submit_metrics': + return { ...baseParams, series: params.series } + + case 'datadog_query_timeseries': + return { + ...baseParams, + query: params.query, + from: params.from ? Number(params.from) : undefined, + to: params.to ? Number(params.to) : undefined, + } + + case 'datadog_create_event': + return { + ...baseParams, + title: params.title, + text: params.text, + alertType: params.alertType, + priority: params.priority, + tags: params.tags, + } + + 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, + } + + 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 + } + }, + }, + }, + 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 000000000..459f1b8f3 --- /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 000000000..5c4d196e0 --- /dev/null +++ b/apps/sim/blocks/blocks/elasticsearch.ts @@ -0,0 +1,435 @@ +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', + 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: 'code', + 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: 'code', + placeholder: '{ "field_to_update": "new_value" }', + required: true, + condition: { field: 'operation', value: 'elasticsearch_update_document' }, + }, + + // Search query + { + id: 'query', + title: 'Search Query', + type: 'code', + placeholder: '{ "match": { "field": "search term" } }', + condition: { field: 'operation', value: 'elasticsearch_search' }, + }, + + // Count query + { + id: 'query', + title: 'Query', + type: 'code', + 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: 'code', + 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: 'code', + 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: 'code', + placeholder: '{ "number_of_shards": 1, "number_of_replicas": 1 }', + condition: { field: 'operation', value: 'elasticsearch_create_index' }, + }, + + // Index mappings + { + id: 'mappings', + title: 'Index Mappings', + type: 'code', + 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 (seconds)', + type: 'short-input', + placeholder: '30', + 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) + } + // 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' + }, + }, + }, + + 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 000000000..743d79391 --- /dev/null +++ b/apps/sim/blocks/blocks/gitlab.ts @@ -0,0 +1,697 @@ +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.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.', + 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: '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) + { + 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 Number (IID) - the # shown in GitLab UI + { + id: 'issueIid', + title: 'Issue Number', + type: 'short-input', + placeholder: 'Enter issue number (e.g., 1 for issue #1)', + required: true, + condition: { + field: 'operation', + value: [ + 'gitlab_get_issue', + 'gitlab_update_issue', + 'gitlab_delete_issue', + 'gitlab_create_issue_note', + ], + }, + }, + // Merge Request Number (IID) - the ! number shown in GitLab UI + { + id: 'mergeRequestIid', + title: 'MR Number', + type: 'short-input', + placeholder: 'Enter MR number (e.g., 1 for !1)', + 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 = { + accessToken: params.accessToken, + } + + 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 Number 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 000000000..9b164169f --- /dev/null +++ b/apps/sim/blocks/blocks/grafana.ts @@ -0,0 +1,503 @@ +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: [ + // 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_list_dashboards', + }, + + // 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 operations + { + id: 'dataSourceId', + title: 'Data Source ID', + type: 'short-input', + placeholder: 'Enter data source ID or UID', + required: true, + condition: { + field: 'operation', + value: '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_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/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts new file mode 100644 index 000000000..b685b3379 --- /dev/null +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -0,0 +1,399 @@ +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, 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', + 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' }, + { 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 by Ticker', id: 'get_series_by_ticker' }, + { label: 'Get Exchange Status', id: 'get_exchange_status' }, + ], + 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', 'get_fills'], + }, + 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', 'get_fills'], + }, + 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', 'get_orderbook'] }, + }, + // 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'] }, + }, + // 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 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', + 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', + 'get_trades', + 'get_fills', + ], + }, + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor', + condition: { + field: 'operation', + value: [ + 'get_markets', + 'get_events', + 'get_positions', + 'get_orders', + 'get_trades', + 'get_fills', + ], + }, + }, + ], + tools: { + access: [ + 'kalshi_get_markets', + 'kalshi_get_market', + 'kalshi_get_events', + 'kalshi_get_event', + 'kalshi_get_balance', + 'kalshi_get_positions', + 'kalshi_get_orders', + 'kalshi_get_orderbook', + 'kalshi_get_trades', + 'kalshi_get_candlesticks', + 'kalshi_get_fills', + 'kalshi_get_series_by_ticker', + 'kalshi_get_exchange_status', + ], + 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' + 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_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, + tickerTrades, + tickerFills, + tickerCandlesticks, + seriesTickerCandlesticks, + seriesTickerGet, + ...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 + } + + // 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 + } + + 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 000000000..57baa363c --- /dev/null +++ b/apps/sim/blocks/blocks/polymarket.ts @@ -0,0 +1,355 @@ +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, 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', + 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 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', + }, + // 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'] }, + }, + // 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', + 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', + 'get_last_trade_price', + 'get_spread', + 'get_tick_size', + ], + }, + }, + // 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', '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', 'get_tags', 'search', 'get_series', 'get_trades'], + }, + }, + ], + tools: { + access: [ + 'polymarket_get_markets', + '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) => { + 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_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': + return 'polymarket_get_price' + case 'get_midpoint': + 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' + } + }, + 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' }, + 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' }, + 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 new file mode 100644 index 000000000..3d98542f4 --- /dev/null +++ b/apps/sim/blocks/blocks/shopify.ts @@ -0,0 +1,845 @@ +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: '#FFFFFF', + 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: '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', + }, + { + id: 'credential', + title: 'Shopify Account', + type: 'oauth-input', + serviceId: 'shopify', + requiredScopes: [ + 'write_products', + 'write_orders', + 'write_customers', + 'write_inventory', + 'read_locations', + 'write_merchant_managed_fulfillment_orders', + ], + placeholder: 'Select Shopify account', + required: true, + }, + { + id: 'shopDomain', + title: 'Shop Domain', + type: 'short-input', + placeholder: 'Auto-detected from OAuth or enter manually', + hidden: true, // Auto-detected from OAuth credential's idToken field + }, + // 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: 'productQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'Filter products (optional)', + condition: { + field: 'operation', + 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 + { + 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'], + }, + }, + // 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: [ + '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_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) => { + 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.productQuery?.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.customerQuery?.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_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.') + } + 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), + } + + // 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 + } + }, + }, + }, + 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 + 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 + 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 + 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 + 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 000000000..924b26c45 --- /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: '#000000', + 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 000000000..d96e38824 --- /dev/null +++ b/apps/sim/blocks/blocks/wordpress.ts @@ -0,0 +1,958 @@ +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.OAuth, + longDescription: + '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', + 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', + }, + + // 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: 'siteId', + title: 'Site ID or Domain', + type: 'short-input', + placeholder: 'e.g., 12345678 or yoursite.wordpress.com', + description: 'Your WordPress.com site ID or domain. Find it in Settings → General.', + 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', + ], + }, + }, + ], + 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) => { + // OAuth authentication for WordPress.com + const baseParams: Record = { + credential: params.credential, + siteId: params.siteId, + } + + 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, + 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, + 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, + 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' }, + 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' }, + 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 000000000..543615141 --- /dev/null +++ b/apps/sim/blocks/blocks/zoom.ts @@ -0,0 +1,572 @@ +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 and recordings', + authMode: AuthMode.OAuth, + longDescription: + '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', + 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' }, + { 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', + }, + { + id: 'credential', + title: 'Zoom Account', + type: 'oauth-input', + serviceId: 'zoom', + requiredScopes: [ + '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, + }, + // 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', 'zoom_list_recordings'], + }, + }, + // Meeting ID for get/update/delete/invitation/recordings/participants 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', + 'zoom_get_meeting_recordings', + 'zoom_delete_recording', + 'zoom_list_past_participants', + ], + }, + }, + // 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', 'zoom_list_recordings', 'zoom_list_past_participants'], + }, + }, + { + id: 'nextPageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token for next page', + condition: { + field: 'operation', + 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 + { + 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', + 'zoom_list_recordings', + 'zoom_get_meeting_recordings', + 'zoom_delete_recording', + 'zoom_list_past_participants', + ], + 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(), + } + + 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 + } + }, + }, + }, + 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' }, + 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 + 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' }, + // 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 + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index d0353d797..3a411614e 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' // TODO: Box OAuth integration 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' @@ -41,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' @@ -67,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' @@ -84,8 +93,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 +122,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 +143,18 @@ export const registry: Record = { apollo: ApolloBlock, arxiv: ArxivBlock, asana: AsanaBlock, + // box: BoxBlock, // TODO: Box OAuth integration 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 +162,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, @@ -163,6 +183,7 @@ export const registry: Record = { intercom: IntercomBlock, jina: JinaBlock, jira: JiraBlock, + kalshi: KalshiBlock, knowledge: KnowledgeBlock, linear: LinearBlock, linkedin: LinkedInBlock, @@ -189,6 +210,7 @@ export const registry: Record = { perplexity: PerplexityBlock, pinecone: PineconeBlock, pipedrive: PipedriveBlock, + polymarket: PolymarketBlock, postgresql: PostgreSQLBlock, posthog: PostHogBlock, pylon: PylonBlock, @@ -207,8 +229,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 +258,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 e8d254975..573d1b449 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 ( + + + + + + + + ) +} + +export function KalshiIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + +export function PolymarketIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/hooks/queries/oauth-connections.ts b/apps/sim/hooks/queries/oauth-connections.ts index 70b9932e3..56569f7b9 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 650d9acd0..f656407de 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, @@ -1634,6 +1694,117 @@ 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 + } + }, + }, + + // 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 66cdb0ad4..dd94c92d6 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -210,6 +210,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 +221,12 @@ 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 + 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 69d65ffc8..4379b1595 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -4,6 +4,7 @@ import { AsanaIcon, ConfluenceIcon, // DiscordIcon, + DropboxIcon, GithubIcon, GmailIcon, GoogleCalendarIcon, @@ -27,12 +28,15 @@ import { PipedriveIcon, RedditIcon, SalesforceIcon, + ShopifyIcon, SlackIcon, // SupabaseIcon, TrelloIcon, WealthboxIcon, WebflowIcon, + WordpressIcon, xIcon, + ZoomIcon, } from '@/components/icons' import { env } from '@/lib/core/config/env' import { createLogger } from '@/lib/logs/console/logger' @@ -49,6 +53,7 @@ export type OAuthProvider = | 'notion' | 'jira' // | 'discord' + | 'dropbox' | 'microsoft' | 'linear' | 'slack' @@ -61,6 +66,9 @@ export type OAuthProvider = | 'hubspot' | 'salesforce' | 'linkedin' + | 'shopify' + | 'zoom' + | 'wordpress' | string export type OAuthService = @@ -80,6 +88,7 @@ export type OAuthService = | 'notion' | 'jira' // | 'discord' + | 'dropbox' | 'microsoft-excel' | 'microsoft-teams' | 'microsoft-planner' @@ -97,6 +106,9 @@ export type OAuthService = | 'hubspot' | 'salesforce' | 'linkedin' + | 'shopify' + | 'zoom' + | 'wordpress' export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -538,6 +550,55 @@ 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: [ + 'write_products', + 'write_orders', + 'write_customers', + 'write_inventory', + 'read_locations', + 'write_merchant_managed_fulfillment_orders', + ], + }, + }, + defaultService: 'shopify', + }, slack: { id: 'slack', name: 'Slack', @@ -769,6 +830,52 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'salesforce', }, + 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: [ + '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', + ], + }, + }, + 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( @@ -1149,6 +1256,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 +1384,46 @@ 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 '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, + } + } + 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/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index b3fa6326e..946ed02f3 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -1,6 +1,6 @@ import type { Logger } from '@/lib/logs/console/logger' import type { StorageContext } from '@/lib/uploads' -import { ACCEPTED_FILE_TYPES } from '@/lib/uploads/utils/validation' +import { ACCEPTED_FILE_TYPES, SUPPORTED_DOCUMENT_EXTENSIONS } from '@/lib/uploads/utils/validation' import type { UserFile } from '@/executor/types' export interface FileAttachment { @@ -258,11 +258,22 @@ export function validateKnowledgeBaseFile( return `File "${file.name}" is too large. Maximum size is ${maxSizeMB}MB.` } - if (!ACCEPTED_FILE_TYPES.includes(file.type)) { - return `File "${file.name}" has an unsupported format. Please use PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML, JSON, YAML, or YML files.` + // Check MIME type first + if (ACCEPTED_FILE_TYPES.includes(file.type)) { + return null } - return null + // Fallback: check file extension (browsers often misidentify file types like .md) + const extension = getFileExtension(file.name) + if ( + SUPPORTED_DOCUMENT_EXTENSIONS.includes( + extension as (typeof SUPPORTED_DOCUMENT_EXTENSIONS)[number] + ) + ) { + return null + } + + return `File "${file.name}" has an unsupported format. Please use PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML, JSON, YAML, or YML files.` } /** diff --git a/apps/sim/lib/uploads/utils/validation.ts b/apps/sim/lib/uploads/utils/validation.ts index b3f87c99d..46125f9b4 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/package.json b/apps/sim/package.json index 44ead7cd2..12d1cdfa5 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/serializer/tests/dual-validation.test.ts b/apps/sim/serializer/tests/dual-validation.test.ts index 08cd74f27..aecedba69 100644 --- a/apps/sim/serializer/tests/dual-validation.test.ts +++ b/apps/sim/serializer/tests/dual-validation.test.ts @@ -195,7 +195,7 @@ describe('Validation Integration Tests', () => { } 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/backlinks.ts b/apps/sim/tools/ahrefs/backlinks.ts new file mode 100644 index 000000000..e2521bd5c --- /dev/null +++ b/apps/sim/tools/ahrefs/backlinks.ts @@ -0,0 +1,116 @@ +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)', + }, + 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, + 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) + // 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)) + 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 || data.error || '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 ?? link.domain_rating ?? 0, + isDofollow: link.is_dofollow ?? link.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 000000000..73331d029 --- /dev/null +++ b/apps/sim/tools/ahrefs/backlinks_stats.ts @@ -0,0 +1,95 @@ +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)', + }, + 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, + 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) + // 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() + }, + 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 || data.error || 'Failed to get backlinks stats') + } + + return { + success: true, + output: { + stats: { + 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, + }, + }, + } + }, + + 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 000000000..0bf5d9f1d --- /dev/null +++ b/apps/sim/tools/ahrefs/broken_backlinks.ts @@ -0,0 +1,121 @@ +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)', + }, + 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, + 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) + // 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)) + 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 || data.error || 'Failed to get broken backlinks') + } + + const brokenBacklinks = (data.backlinks || data.broken_backlinks || []).map((link: any) => ({ + urlFrom: link.url_from || '', + urlTo: link.url_to || '', + httpCode: link.http_code ?? link.status_code ?? 404, + anchor: link.anchor || '', + domainRatingSource: link.domain_rating_source ?? link.domain_rating ?? 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 000000000..75066ae0c --- /dev/null +++ b/apps/sim/tools/ahrefs/domain_rating.ts @@ -0,0 +1,74 @@ +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)', + }, + 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, + visibility: 'user-only', + description: 'Ahrefs API Key', + }, + }, + + request: { + 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', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || '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 000000000..f83ae3d01 --- /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 000000000..a37fd7f0f --- /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 || data.error || 'Failed to get keyword overview') + } + + return { + success: true, + output: { + overview: { + keyword: data.keyword || '', + searchVolume: data.volume ?? 0, + keywordDifficulty: data.keyword_difficulty ?? data.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 000000000..ae9929052 --- /dev/null +++ b/apps/sim/tools/ahrefs/organic_keywords.ts @@ -0,0 +1,127 @@ +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)', + }, + 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, + 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') + // 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)) + 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 || data.error || 'Failed to get organic keywords') + } + + 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 ?? kw.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 000000000..fb1b6ff02 --- /dev/null +++ b/apps/sim/tools/ahrefs/referring_domains.ts @@ -0,0 +1,125 @@ +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)', + }, + 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, + 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/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] + 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)) + 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 || data.error || 'Failed to get referring domains') + } + + 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, + 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 000000000..fb35f4d9d --- /dev/null +++ b/apps/sim/tools/ahrefs/top_pages.ts @@ -0,0 +1,119 @@ +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)', + }, + 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, + 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') + // 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)) + 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 || data.error || 'Failed to get top pages') + } + + const pages = (data.pages || data.top_pages || []).map((page: any) => ({ + url: page.url || '', + traffic: page.traffic ?? 0, + keywords: page.keywords ?? page.keyword_count ?? 0, + topKeyword: page.top_keyword || '', + value: page.value ?? page.traffic_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 000000000..797aed604 --- /dev/null +++ b/apps/sim/tools/ahrefs/types.ts @@ -0,0 +1,199 @@ +// Common types for Ahrefs API tools +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 +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/datadog/cancel_downtime.ts b/apps/sim/tools/datadog/cancel_downtime.ts new file mode 100644 index 000000000..95338b67c --- /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 000000000..6b84d7170 --- /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 000000000..35737f1c1 --- /dev/null +++ b/apps/sim/tools/datadog/create_event.ts @@ -0,0 +1,163 @@ +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', + }, + 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 000000000..416e2a731 --- /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 000000000..1025b93f7 --- /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 000000000..2568a9bb3 --- /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 000000000..7a5de1fe4 --- /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 000000000..53cb4e0ed --- /dev/null +++ b/apps/sim/tools/datadog/list_monitors.ts @@ -0,0 +1,180 @@ +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() + const url = `https://api.${site}/api/v1/monitor${queryString ? `?${queryString}` : ''}` + console.log( + '[Datadog List Monitors] URL:', + url, + 'Site param:', + params.site, + 'API Key present:', + !!params.apiKey, + 'App Key present:', + !!params.applicationKey + ) + return url + }, + 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 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, + 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 000000000..99c78837e --- /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 000000000..ed6406c54 --- /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 000000000..acf57395b --- /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 000000000..1dd505567 --- /dev/null +++ b/apps/sim/tools/datadog/send_logs.ts @@ -0,0 +1,94 @@ +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', + }, + 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 000000000..7b4df3cf9 --- /dev/null +++ b/apps/sim/tools/datadog/submit_metrics.ts @@ -0,0 +1,101 @@ +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', + }, + 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 000000000..b2fc5e80c --- /dev/null +++ b/apps/sim/tools/datadog/types.ts @@ -0,0 +1,782 @@ +// 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' + +// Base parameters for write-only operations (only need API key) +export interface DatadogWriteOnlyParams { + apiKey: string + site?: DatadogSite +} + +// Base parameters for read/manage operations (need both API key and Application key) +export interface DatadogBaseParams extends DatadogWriteOnlyParams { + applicationKey: string +} + +// ======================== +// 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 DatadogWriteOnlyParams { + 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 DatadogWriteOnlyParams { + 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 DatadogWriteOnlyParams { + 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 000000000..8a6471f03 --- /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 000000000..a107747c7 --- /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 000000000..e27df0451 --- /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 000000000..d1e6a67c8 --- /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 000000000..e489b3d21 --- /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 000000000..88df55d24 --- /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 000000000..6fea87497 --- /dev/null +++ b/apps/sim/tools/dropbox/index.ts @@ -0,0 +1,23 @@ +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/dropbox/list_folder.ts b/apps/sim/tools/dropbox/list_folder.ts new file mode 100644 index 000000000..2ba7b983d --- /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 000000000..bbdf57081 --- /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 000000000..eaf127ce6 --- /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 000000000..b48f30cc8 --- /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 000000000..557322763 --- /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 000000000..368bd2f3d --- /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 000000000..22b346be8 --- /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 000000000..97de2a5b2 --- /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 000000000..3ae586ea4 --- /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 000000000..d9d5fb8af --- /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 000000000..17d8b22f7 --- /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 000000000..e2b839db3 --- /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 000000000..e79bc099e --- /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 000000000..b53eb5888 --- /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 000000000..e625add91 --- /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 000000000..6081c0620 --- /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 000000000..754f489e0 --- /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 000000000..9a5516994 --- /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 000000000..3d1a1d468 --- /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 000000000..ef48a5bb7 --- /dev/null +++ b/apps/sim/tools/gitlab/cancel_pipeline.ts @@ -0,0 +1,68 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..417f614bf --- /dev/null +++ b/apps/sim/tools/gitlab/create_issue.ts @@ -0,0 +1,111 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..10a1620e4 --- /dev/null +++ b/apps/sim/tools/gitlab/create_issue_note.ts @@ -0,0 +1,77 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..45e45d30e --- /dev/null +++ b/apps/sim/tools/gitlab/create_merge_request.ts @@ -0,0 +1,136 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..693880773 --- /dev/null +++ b/apps/sim/tools/gitlab/create_merge_request_note.ts @@ -0,0 +1,80 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..3213e976f --- /dev/null +++ b/apps/sim/tools/gitlab/create_pipeline.ts @@ -0,0 +1,86 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..0787321c4 --- /dev/null +++ b/apps/sim/tools/gitlab/delete_issue.ts @@ -0,0 +1,64 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..acbe21e38 --- /dev/null +++ b/apps/sim/tools/gitlab/get_issue.ts @@ -0,0 +1,65 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + projectId: { + type: 'string', + required: true, + description: 'Project ID or URL-encoded path', + }, + issueIid: { + type: 'number', + required: true, + description: 'Issue number within the project (the # shown in GitLab UI)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId)) + return `https://gitlab.com/api/v4/projects/${encodedId}/issues/${params.issueIid}` + }, + method: 'GET', + headers: (params) => ({ + 'PRIVATE-TOKEN': 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 000000000..7b136961b --- /dev/null +++ b/apps/sim/tools/gitlab/get_merge_request.ts @@ -0,0 +1,71 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..e1ae7a69f --- /dev/null +++ b/apps/sim/tools/gitlab/get_pipeline.ts @@ -0,0 +1,66 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..cabfcefd8 --- /dev/null +++ b/apps/sim/tools/gitlab/get_project.ts @@ -0,0 +1,60 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..0133af843 --- /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 000000000..837601971 --- /dev/null +++ b/apps/sim/tools/gitlab/list_issues.ts @@ -0,0 +1,124 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..d40dbb156 --- /dev/null +++ b/apps/sim/tools/gitlab/list_merge_requests.ts @@ -0,0 +1,124 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..71c507125 --- /dev/null +++ b/apps/sim/tools/gitlab/list_pipelines.ts @@ -0,0 +1,110 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..0840ba3b2 --- /dev/null +++ b/apps/sim/tools/gitlab/list_projects.ts @@ -0,0 +1,114 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..f16e01361 --- /dev/null +++ b/apps/sim/tools/gitlab/merge_merge_request.ts @@ -0,0 +1,110 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..bc24688b0 --- /dev/null +++ b/apps/sim/tools/gitlab/retry_pipeline.ts @@ -0,0 +1,68 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'PRIVATE-TOKEN': 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 000000000..60b535533 --- /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 000000000..699664077 --- /dev/null +++ b/apps/sim/tools/gitlab/update_issue.ts @@ -0,0 +1,121 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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 000000000..9fd3a028a --- /dev/null +++ b/apps/sim/tools/gitlab/update_merge_request.ts @@ -0,0 +1,139 @@ +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', + + params: { + accessToken: { + type: 'string', + required: true, + description: 'GitLab Personal Access Token', + }, + 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) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': 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/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts new file mode 100644 index 000000000..24029f093 --- /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 000000000..e0e846c5d --- /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 000000000..8286ca9ac --- /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 000000000..c4a289c76 --- /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/delete_alert_rule.ts b/apps/sim/tools/grafana/delete_alert_rule.ts new file mode 100644 index 000000000..a5b4cfff5 --- /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 000000000..109d6da9d --- /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 000000000..8b70ef847 --- /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/get_alert_rule.ts b/apps/sim/tools/grafana/get_alert_rule.ts new file mode 100644 index 000000000..313dcb3c0 --- /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 000000000..a112bb7d3 --- /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/get_data_source.ts b/apps/sim/tools/grafana/get_data_source.ts new file mode 100644 index 000000000..c2711246f --- /dev/null +++ b/apps/sim/tools/grafana/get_data_source.ts @@ -0,0 +1,125 @@ +import type { + GrafanaGetDataSourceParams, + GrafanaGetDataSourceResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +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/index.ts b/apps/sim/tools/grafana/index.ts new file mode 100644 index 000000000..d911a193a --- /dev/null +++ b/apps/sim/tools/grafana/index.ts @@ -0,0 +1,48 @@ +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 { 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 { 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' + +// 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/list_alert_rules.ts b/apps/sim/tools/grafana/list_alert_rules.ts new file mode 100644 index 000000000..5a8d7540e --- /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 000000000..5d68434e1 --- /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 000000000..3cde333bd --- /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 000000000..068abb024 --- /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 000000000..f835cf3a4 --- /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 000000000..c3f6f3a33 --- /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/types.ts b/apps/sim/tools/grafana/types.ts new file mode 100644 index 000000000..1d0568ce7 --- /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/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts new file mode 100644 index 000000000..6054c1194 --- /dev/null +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -0,0 +1,253 @@ +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. Fetches the current rule and merges your changes.', + 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: { + // First, GET the existing alert rule + 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) => { + // 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.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 { + updatedRule.labels = { + ...(existingRule.labels || {}), + ...JSON.parse(params.labels), + } + } catch { + // Keep existing labels if parse fails + } + } + + // 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 + } + + 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, + 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 000000000..f3b475ced --- /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 000000000..121f64246 --- /dev/null +++ b/apps/sim/tools/grafana/update_dashboard.ts @@ -0,0 +1,241 @@ +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. Fetches the current dashboard and merges your changes.', + 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: { + // 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', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + 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, + }, + } + }, + + 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 (!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.panels) { + try { + updatedDashboard.panels = JSON.parse(params.panels) + } catch { + // Keep existing panels if parse fails + } + } + + // Increment version for update + if (existingDashboard.version) { + updatedDashboard.version = existingDashboard.version + } + + // 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, + 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', + }, + }, +} diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 7624c3385..ff9d4e1b2 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/kalshi/get_balance.ts b/apps/sim/tools/kalshi/get_balance.ts new file mode 100644 index 000000000..027963504 --- /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_candlesticks.ts b/apps/sim/tools/kalshi/get_candlesticks.ts new file mode 100644 index 000000000..206be1d24 --- /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_event.ts b/apps/sim/tools/kalshi/get_event.ts new file mode 100644 index 000000000..148c12846 --- /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 000000000..6a2344a6c --- /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_exchange_status.ts b/apps/sim/tools/kalshi/get_exchange_status.ts new file mode 100644 index 000000000..b8091cc7b --- /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 000000000..defd87e5d --- /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_market.ts b/apps/sim/tools/kalshi/get_market.ts new file mode 100644 index 000000000..a2de39ec7 --- /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 000000000..cb8034571 --- /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_orderbook.ts b/apps/sim/tools/kalshi/get_orderbook.ts new file mode 100644 index 000000000..496f95f9b --- /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_orders.ts b/apps/sim/tools/kalshi/get_orders.ts new file mode 100644 index 000000000..3f5b3b041 --- /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 000000000..80114bfe9 --- /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/get_series_by_ticker.ts b/apps/sim/tools/kalshi/get_series_by_ticker.ts new file mode 100644 index 000000000..f6567046e --- /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 000000000..89118ffb5 --- /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 new file mode 100644 index 000000000..3bae8d37d --- /dev/null +++ b/apps/sim/tools/kalshi/index.ts @@ -0,0 +1,13 @@ +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 { 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 new file mode 100644 index 000000000..bd63d0815 --- /dev/null +++ b/apps/sim/tools/kalshi/types.ts @@ -0,0 +1,295 @@ +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 +} + +// 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 +} + +// Settlement source type +export interface KalshiSettlementSource { + name: string + url: string +} + +// Series type +export interface KalshiSeries { + ticker: string + title: string + frequency: string + category: string + tags?: 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 +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}` +} + +// 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, + 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 + + // 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 +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 000000000..7beba8687 --- /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 000000000..c54be4d4e --- /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_last_trade_price.ts b/apps/sim/tools/polymarket/get_last_trade_price.ts new file mode 100644 index 000000000..451d9a03e --- /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_market.ts b/apps/sim/tools/polymarket/get_market.ts new file mode 100644 index 000000000..4d674e25a --- /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 000000000..209992e27 --- /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 000000000..d39e6e85b --- /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 000000000..707c230fc --- /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_positions.ts b/apps/sim/tools/polymarket/get_positions.ts new file mode 100644 index 000000000..1e4c17fcf --- /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_price.ts b/apps/sim/tools/polymarket/get_price.ts new file mode 100644 index 000000000..3dbc37bfa --- /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 000000000..a7fb4c4cb --- /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/get_series.ts b/apps/sim/tools/polymarket/get_series.ts new file mode 100644 index 000000000..f1c9d61f9 --- /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 000000000..8a9258563 --- /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 000000000..940925584 --- /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 000000000..93032e4b2 --- /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 000000000..8a3a9788c --- /dev/null +++ b/apps/sim/tools/polymarket/get_tick_size.ts @@ -0,0 +1,85 @@ +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') + } + + // 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: String(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 000000000..3eb371b04 --- /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 new file mode 100644 index 000000000..a52e9ac37 --- /dev/null +++ b/apps/sim/tools/polymarket/index.ts @@ -0,0 +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 000000000..99c32a604 --- /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 new file mode 100644 index 000000000..5d16e2958 --- /dev/null +++ b/apps/sim/tools/polymarket/types.ts @@ -0,0 +1,184 @@ +// 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' +export const POLYMARKET_DATA_URL = 'https://data-api.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}` +} + +// 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 + 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 +} + +// 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}` + throw new Error(`Polymarket API error (${status}): ${errorMessage}`) +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d9b94cd2c..5a0cb06b9 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, @@ -71,6 +81,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 +132,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 +152,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 +237,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 +304,27 @@ import { listMattersHoldsTool, listMattersTool, } from '@/tools/google_vault' +import { + grafanaCreateAlertRuleTool, + grafanaCreateAnnotationTool, + grafanaCreateDashboardTool, + grafanaCreateFolderTool, + grafanaDeleteAlertRuleTool, + grafanaDeleteAnnotationTool, + grafanaDeleteDashboardTool, + grafanaGetAlertRuleTool, + grafanaGetDashboardTool, + grafanaGetDataSourceTool, + 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 { @@ -348,6 +440,21 @@ import { jiraUpdateWorklogTool, jiraWriteTool, } from '@/tools/jira' +import { + kalshiGetBalanceTool, + kalshiGetCandlesticksTool, + kalshiGetEventsTool, + kalshiGetEventTool, + kalshiGetExchangeStatusTool, + kalshiGetFillsTool, + kalshiGetMarketsTool, + kalshiGetMarketTool, + kalshiGetOrderbookTool, + kalshiGetOrdersTool, + kalshiGetPositionsTool, + kalshiGetSeriesByTickerTool, + kalshiGetTradesTool, +} from '@/tools/kalshi' import { knowledgeCreateDocumentTool, knowledgeSearchTool, @@ -640,6 +747,25 @@ import { pipedriveUpdateDealTool, pipedriveUpdateLeadTool, } from '@/tools/pipedrive' +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, executeTool as postgresExecuteTool, @@ -833,6 +959,29 @@ import { sharepointUpdateListItemTool, sharepointUploadFileTool, } from '@/tools/sharepoint' +import { + shopifyAdjustInventoryTool, + shopifyCancelOrderTool, + shopifyCreateCustomerTool, + shopifyCreateFulfillmentTool, + shopifyCreateProductTool, + shopifyDeleteCustomerTool, + shopifyDeleteProductTool, + shopifyGetCollectionTool, + shopifyGetCustomerTool, + shopifyGetInventoryLevelTool, + shopifyGetOrderTool, + shopifyGetProductTool, + shopifyListCollectionsTool, + shopifyListCustomersTool, + shopifyListInventoryItemsTool, + shopifyListLocationsTool, + shopifyListOrdersTool, + shopifyListProductsTool, + shopifyUpdateCustomerTool, + shopifyUpdateOrderTool, + shopifyUpdateProductTool, +} from '@/tools/shopify' import { slackAddReactionTool, slackCanvasTool, @@ -844,6 +993,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 +1162,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 +1241,18 @@ import { zepGetUserThreadsTool, zepGetUserTool, } from '@/tools/zep' +import { + zoomCreateMeetingTool, + zoomDeleteMeetingTool, + zoomDeleteRecordingTool, + zoomGetMeetingInvitationTool, + zoomGetMeetingRecordingsTool, + zoomGetMeetingTool, + zoomListMeetingsTool, + zoomListPastParticipantsTool, + zoomListRecordingsTool, + zoomUpdateMeetingTool, +} from '@/tools/zoom' // Registry of all available tools export const tools: Record = { @@ -1073,6 +1277,25 @@ export const tools: Record = { firecrawl_crawl: crawlTool, firecrawl_map: mapTool, firecrawl_extract: extractTool, + 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 +1321,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, @@ -1129,6 +1365,36 @@ 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, + kalshi_get_orderbook: kalshiGetOrderbookTool, + kalshi_get_trades: kalshiGetTradesTool, + kalshi_get_candlesticks: kalshiGetCandlesticksTool, + kalshi_get_fills: kalshiGetFillsTool, + kalshi_get_series_by_ticker: kalshiGetSeriesByTickerTool, + kalshi_get_exchange_status: kalshiGetExchangeStatusTool, + 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, + 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, @@ -1253,6 +1519,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 +1595,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 +1745,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 +1870,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 +1985,27 @@ 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, + 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, @@ -1702,6 +2050,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 +2383,14 @@ 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, + zoom_list_recordings: zoomListRecordingsTool, + zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool, + zoom_delete_recording: zoomDeleteRecordingTool, + zoom_list_past_participants: zoomListPastParticipantsTool, } diff --git a/apps/sim/tools/shopify/adjust_inventory.ts b/apps/sim/tools/shopify/adjust_inventory.ts new file mode 100644 index 000000000..b80db6aae --- /dev/null +++ b/apps/sim/tools/shopify/adjust_inventory.ts @@ -0,0 +1,160 @@ +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 || 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) => { + 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 000000000..532286b7d --- /dev/null +++ b/apps/sim/tools/shopify/cancel_order.ts @@ -0,0 +1,147 @@ +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, DECLINED, FRAUD, INVENTORY, STAFF, 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 || 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) => { + 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 || null, + }, + } + }, + }, + + 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 000000000..53bd1a117 --- /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 || 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) => { + // 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 + amountSpent { + 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_fulfillment.ts b/apps/sim/tools/shopify/create_fulfillment.ts new file mode 100644 index 000000000..f7632440a --- /dev/null +++ b/apps/sim/tools/shopify/create_fulfillment.ts @@ -0,0 +1,240 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { ShopifyBaseParams } from './types' + +interface ShopifyCreateFulfillmentParams extends ShopifyBaseParams { + fulfillmentOrderId: string + trackingNumber?: string + trackingCompany?: string + trackingUrl?: string + notifyCustomer?: boolean +} + +interface ShopifyCreateFulfillmentResponse extends ToolResponse { + output: { + fulfillment?: { + id: string + status: string + createdAt: string + updatedAt: string + trackingInfo: Array<{ + company: string | null + number: string | null + url: string | null + }> + 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 new file mode 100644 index 000000000..5d4ffe4c0 --- /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 || 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) => { + 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 + 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 000000000..994a26326 --- /dev/null +++ b/apps/sim/tools/shopify/delete_customer.ts @@ -0,0 +1,114 @@ +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 || 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) => { + 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 000000000..7b0bfed16 --- /dev/null +++ b/apps/sim/tools/shopify/delete_product.ts @@ -0,0 +1,114 @@ +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 || 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) => { + 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_collection.ts b/apps/sim/tools/shopify/get_collection.ts new file mode 100644 index 000000000..4747d99d5 --- /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 new file mode 100644 index 000000000..b1aa0f9b5 --- /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 || 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) => { + 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 + amountSpent { + 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 000000000..3761748a9 --- /dev/null +++ b/apps/sim/tools/shopify/get_inventory_level.ts @@ -0,0 +1,154 @@ +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 || 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) => { + 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 + quantities(names: ["available", "on_hand", "committed", "incoming", "reserved"]) { + name + quantity + } + 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: { + 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 { + 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 000000000..103c160a9 --- /dev/null +++ b/apps/sim/tools/shopify/get_order.ts @@ -0,0 +1,203 @@ +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 || 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) => { + 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 000000000..321bad456 --- /dev/null +++ b/apps/sim/tools/shopify/get_product.ts @@ -0,0 +1,127 @@ +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 || 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) => { + 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 + inventoryQuantity + } + } + } + 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 000000000..438199559 --- /dev/null +++ b/apps/sim/tools/shopify/index.ts @@ -0,0 +1,29 @@ +// Product Tools + +export { shopifyAdjustInventoryTool } from './adjust_inventory' +export { shopifyCancelOrderTool } from './cancel_order' +// Customer Tools +export { shopifyCreateCustomerTool } from './create_customer' +// Fulfillment Tools +export { shopifyCreateFulfillmentTool } from './create_fulfillment' +export { shopifyCreateProductTool } from './create_product' +export { shopifyDeleteCustomerTool } from './delete_customer' +export { shopifyDeleteProductTool } from './delete_product' +export { shopifyGetCollectionTool } from './get_collection' +export { shopifyGetCustomerTool } from './get_customer' +export { shopifyGetInventoryLevelTool } from './get_inventory_level' +// Order Tools +export { shopifyGetOrderTool } from './get_order' +export { shopifyGetProductTool } from './get_product' +// Collection Tools +export { shopifyListCollectionsTool } from './list_collections' +export { shopifyListCustomersTool } from './list_customers' +// Inventory Tools +export { shopifyListInventoryItemsTool } from './list_inventory_items' +// Location Tools +export { shopifyListLocationsTool } from './list_locations' +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_collections.ts b/apps/sim/tools/shopify/list_collections.ts new file mode 100644 index 000000000..352dbdfaa --- /dev/null +++ b/apps/sim/tools/shopify/list_collections.ts @@ -0,0 +1,187 @@ +import type { ToolConfig, ToolResponse } from '@/tools/types' +import type { ShopifyBaseParams } from './types' + +interface ShopifyListCollectionsParams extends ShopifyBaseParams { + first?: number + query?: string +} + +interface ShopifyCollectionsResponse extends ToolResponse { + output: { + collections?: Array<{ + 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 + }> + 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 new file mode 100644 index 000000000..1a6ecc9bc --- /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., "first_name:John" or "last_name:Smith" or "email:*@gmail.com" or "tag:vip")', + }, + }, + + 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 listCustomers($first: Int!, $query: String) { + customers(first: $first, query: $query) { + edges { + node { + id + email + firstName + lastName + phone + createdAt + updatedAt + note + tags + amountSpent { + 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_inventory_items.ts b/apps/sim/tools/shopify/list_inventory_items.ts new file mode 100644 index 000000000..228f1287e --- /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 000000000..21eb9dce3 --- /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 new file mode 100644 index 000000000..ed6294f22 --- /dev/null +++ b/apps/sim/tools/shopify/list_orders.ts @@ -0,0 +1,186 @@ +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)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by order status (open, closed, cancelled, any)', + }, + 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 || 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) + + // 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) { + 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: queryString, + }, + } + }, + }, + + 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 000000000..4e96217db --- /dev/null +++ b/apps/sim/tools/shopify/list_products.ts @@ -0,0 +1,151 @@ +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 || 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 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 000000000..93bd8354f --- /dev/null +++ b/apps/sim/tools/shopify/types.ts @@ -0,0 +1,378 @@ +// 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 + inventoryQuantity: number +} + +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[] + amountSpent: { + 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 + onHand: number + committed: number + incoming: number + reserved: 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 + idToken?: string // Shop domain from OAuth, used as fallback +} + +// 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 + status?: string + 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 000000000..f60c15798 --- /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 || 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) => { + 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 + amountSpent { + 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 000000000..13cd6bccf --- /dev/null +++ b/apps/sim/tools/shopify/update_order.ts @@ -0,0 +1,165 @@ +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 || 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) => { + 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 000000000..b817cbc81 --- /dev/null +++ b/apps/sim/tools/shopify/update_product.ts @@ -0,0 +1,204 @@ +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 || 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) => { + 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 000000000..dc27f4190 --- /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 000000000..b1a7b4efd --- /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 000000000..3c26685ab --- /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 000000000..8527ad9ff --- /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 000000000..e7ed207f0 --- /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 000000000..f6fb5af77 --- /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 000000000..87694da5c --- /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 000000000..b786211a0 --- /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 000000000..be79a5e0e --- /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 000000000..dd6a0daec --- /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 000000000..8ec95be2f --- /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 000000000..c7e72d997 --- /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 000000000..e68f6302f --- /dev/null +++ b/apps/sim/tools/ssh/types.ts @@ -0,0 +1,218 @@ +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 + } +} + +export interface SSHResponse extends ToolResponse { + output: { + stdout?: string + stderr?: string + exitCode?: number + success?: boolean + + uploaded?: boolean + downloaded?: boolean + fileContent?: string + fileName?: string + remotePath?: string + localPath?: string + size?: number + + entries?: SSHFileInfo[] + totalFiles?: number + totalDirectories?: number + + exists?: boolean + type?: 'file' | 'directory' | 'symlink' | 'not_found' + permissions?: string + modified?: string + + hostname?: string + os?: string + architecture?: string + uptime?: number + memory?: { + total: number + free: number + used: number + } + diskSpace?: { + total: number + free: number + used: number + } + + content?: string + lines?: number + + created?: boolean + deleted?: boolean + written?: boolean + moved?: boolean + alreadyExists?: boolean + + commandExists?: boolean + commandPath?: string + version?: string + + scriptPath?: string + + 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 000000000..10cd389cb --- /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 000000000..90b0e1250 --- /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 1a3bfd10f..3d152cc4c 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

{ @@ -92,7 +93,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/utils.test.ts b/apps/sim/tools/utils.test.ts index 89b6ef25c..cd7f259a5 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 8cf07a530..e24be3778 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}`) } } } diff --git a/apps/sim/tools/wordpress/create_category.ts b/apps/sim/tools/wordpress/create_category.ts new file mode 100644 index 000000000..ca8687a63 --- /dev/null +++ b/apps/sim/tools/wordpress/create_category.ts @@ -0,0 +1,117 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreateCategoryParams, + type WordPressCreateCategoryResponse, +} from './types' + +export const createCategoryTool: ToolConfig< + WordPressCreateCategoryParams, + WordPressCreateCategoryResponse +> = { + id: 'wordpress_create_category', + name: 'WordPress Create Category', + description: 'Create a new category in WordPress.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/categories`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..0c3bcd2a1 --- /dev/null +++ b/apps/sim/tools/wordpress/create_comment.ts @@ -0,0 +1,137 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressCreateCommentParams, + type 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.com post', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..461d91e3b --- /dev/null +++ b/apps/sim/tools/wordpress/create_page.ts @@ -0,0 +1,154 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..53f71f925 --- /dev/null +++ b/apps/sim/tools/wordpress/create_post.ts @@ -0,0 +1,166 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..80c920d67 --- /dev/null +++ b/apps/sim/tools/wordpress/create_tag.ts @@ -0,0 +1,105 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/tags`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..3cdc2a766 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_comment.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressDeleteCommentParams, + type WordPressDeleteCommentResponse, +} from './types' + +export const deleteCommentTool: ToolConfig< + WordPressDeleteCommentParams, + WordPressDeleteCommentResponse +> = { + id: 'wordpress_delete_comment', + name: 'WordPress Delete Comment', + description: 'Delete a comment from WordPress.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 forceParam = params.force ? '?force=true' : '' + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments/${params.commentId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..cbb8afd84 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_media.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => { + // Media deletion requires force=true to actually delete + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/media/${params.mediaId}?force=true` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..9bba21051 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_page.ts @@ -0,0 +1,111 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 forceParam = params.force ? '?force=true' : '' + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..e31a8bdd2 --- /dev/null +++ b/apps/sim/tools/wordpress/delete_post.ts @@ -0,0 +1,111 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 forceParam = params.force ? '?force=true' : '' + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}${forceParam}` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..3ff1bb0b0 --- /dev/null +++ b/apps/sim/tools/wordpress/get_current_user.ts @@ -0,0 +1,90 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressGetCurrentUserParams, + type 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.com user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + }, + + request: { + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/users/me`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..2da7af424 --- /dev/null +++ b/apps/sim/tools/wordpress/get_media.ts @@ -0,0 +1,93 @@ +import type { ToolConfig } from '@/tools/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.com by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + mediaId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the media item to retrieve', + }, + }, + + request: { + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/media/${params.mediaId}`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..48e4d84b1 --- /dev/null +++ b/apps/sim/tools/wordpress/get_page.ts @@ -0,0 +1,97 @@ +import type { ToolConfig } from '@/tools/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.com by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + pageId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the page to retrieve', + }, + }, + + request: { + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..2f12c6eeb --- /dev/null +++ b/apps/sim/tools/wordpress/get_post.ts @@ -0,0 +1,97 @@ +import type { ToolConfig } from '@/tools/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.com by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + postId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the post to retrieve', + }, + }, + + request: { + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..f733bfb13 --- /dev/null +++ b/apps/sim/tools/wordpress/get_user.ts @@ -0,0 +1,93 @@ +import type { ToolConfig } from '@/tools/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.com by ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + userId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the user to retrieve', + }, + }, + + request: { + url: (params) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/users/${params.userId}`, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..3889208a5 --- /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 000000000..3cd76d871 --- /dev/null +++ b/apps/sim/tools/wordpress/list_categories.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListCategoriesParams, + type WordPressListCategoriesResponse, +} from './types' + +export const listCategoriesTool: ToolConfig< + WordPressListCategoriesParams, + WordPressListCategoriesResponse +> = { + id: 'wordpress_list_categories', + name: 'WordPress List Categories', + description: 'List categories from WordPress.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + 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.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/categories${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..efbef5b83 --- /dev/null +++ b/apps/sim/tools/wordpress/list_comments.ts @@ -0,0 +1,158 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressListCommentsParams, + type WordPressListCommentsResponse, +} from './types' + +export const listCommentsTool: ToolConfig< + WordPressListCommentsParams, + WordPressListCommentsResponse +> = { + id: 'wordpress_list_comments', + name: 'WordPress List Comments', + description: 'List comments from WordPress.com with optional filters', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 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 `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..d794ee9fb --- /dev/null +++ b/apps/sim/tools/wordpress/list_media.ts @@ -0,0 +1,157 @@ +import type { ToolConfig } from '@/tools/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.com media library', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 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 `${WORDPRESS_COM_API_BASE}/${params.siteId}/media${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..09bc8da27 --- /dev/null +++ b/apps/sim/tools/wordpress/list_pages.ts @@ -0,0 +1,161 @@ +import type { ToolConfig } from '@/tools/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.com with optional filters', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 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 `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..ac02eca89 --- /dev/null +++ b/apps/sim/tools/wordpress/list_posts.ts @@ -0,0 +1,189 @@ +import type { ToolConfig } from '@/tools/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.com with optional filters', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 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 `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..91ba9717e --- /dev/null +++ b/apps/sim/tools/wordpress/list_tags.ts @@ -0,0 +1,126 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + 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.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/tags${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..982c86fe0 --- /dev/null +++ b/apps/sim/tools/wordpress/list_users.ts @@ -0,0 +1,143 @@ +import type { ToolConfig } from '@/tools/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.com (requires admin privileges)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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', + }, + order: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Order direction: asc or desc', + }, + }, + + request: { + url: (params) => { + 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.order) queryParams.append('order', params.order) + + const queryString = queryParams.toString() + return `${WORDPRESS_COM_API_BASE}/${params.siteId}/users${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..f26d77909 --- /dev/null +++ b/apps/sim/tools/wordpress/search_content.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressSearchContentParams, + type 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.com (posts, pages, media)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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 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 `${WORDPRESS_COM_API_BASE}/${params.siteId}/search?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + 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 000000000..faa2bd339 --- /dev/null +++ b/apps/sim/tools/wordpress/types.ts @@ -0,0 +1,627 @@ +// Common types for WordPress REST API tools +import type { ToolResponse } from '@/tools/types' + +// 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 { + 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' + +// 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 + 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 + 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 + 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 000000000..bda66c92f --- /dev/null +++ b/apps/sim/tools/wordpress/update_comment.ts @@ -0,0 +1,114 @@ +import type { ToolConfig } from '@/tools/types' +import { + WORDPRESS_COM_API_BASE, + type WordPressUpdateCommentParams, + type WordPressUpdateCommentResponse, +} from './types' + +export const updateCommentTool: ToolConfig< + WordPressUpdateCommentParams, + WordPressUpdateCommentResponse +> = { + id: 'wordpress_update_comment', + name: 'WordPress Update Comment', + description: 'Update a comment in WordPress.com (content or status)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/comments/${params.commentId}`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..48f3390f6 --- /dev/null +++ b/apps/sim/tools/wordpress/update_page.ts @@ -0,0 +1,159 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/pages/${params.pageId}`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..050caf103 --- /dev/null +++ b/apps/sim/tools/wordpress/update_post.ts @@ -0,0 +1,171 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/posts/${params.postId}`, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + 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 000000000..2d91e0534 --- /dev/null +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -0,0 +1,151 @@ +import type { ToolConfig } from '@/tools/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.com', + version: '1.0.0', + + oauth: { + required: true, + provider: 'wordpress', + requiredScopes: ['global'], + }, + + params: { + siteId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'WordPress.com site ID or domain (e.g., 12345678 or mysite.wordpress.com)', + }, + 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) => `${WORDPRESS_COM_API_BASE}/${params.siteId}/media`, + method: 'POST', + headers: (params) => { + // 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: `Bearer ${params.accessToken}`, + } + }, + 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/zendesk/get_tickets.ts b/apps/sim/tools/zendesk/get_tickets.ts index 97846c68b..ca651eaaf 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 = + { + id: 'zoom_create_meeting', + name: 'Zoom Create Meeting', + description: 'Create a new Zoom meeting', + version: '1.0.0', + + oauth: { + required: true, + provider: 'zoom', + requiredScopes: ['meeting:write:meeting'], + }, + + 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 000000000..f05e5b983 --- /dev/null +++ b/apps/sim/tools/zoom/delete_meeting.ts @@ -0,0 +1,99 @@ +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', + requiredScopes: ['meeting:delete:meeting'], + }, + + 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/delete_recording.ts b/apps/sim/tools/zoom/delete_recording.ts new file mode 100644 index 000000000..c49d8d01d --- /dev/null +++ b/apps/sim/tools/zoom/delete_recording.ts @@ -0,0 +1,93 @@ +import type { ToolConfig } from '@/tools/types' +import type { ZoomDeleteRecordingParams, ZoomDeleteRecordingResponse } from '@/tools/zoom/types' + +export const zoomDeleteRecordingTool: ToolConfig< + ZoomDeleteRecordingParams, + ZoomDeleteRecordingResponse +> = { + 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 new file mode 100644 index 000000000..20b9d422b --- /dev/null +++ b/apps/sim/tools/zoom/get_meeting.ts @@ -0,0 +1,132 @@ +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', + requiredScopes: ['meeting:read:meeting'], + }, + + 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 000000000..7a3fd6265 --- /dev/null +++ b/apps/sim/tools/zoom/get_meeting_invitation.ts @@ -0,0 +1,72 @@ +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', + requiredScopes: ['meeting:read:invitation'], + }, + + 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/get_meeting_recordings.ts b/apps/sim/tools/zoom/get_meeting_recordings.ts new file mode 100644 index 000000000..18350ada8 --- /dev/null +++ b/apps/sim/tools/zoom/get_meeting_recordings.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomGetMeetingRecordingsParams, + ZoomGetMeetingRecordingsResponse, +} from '@/tools/zoom/types' + +export const zoomGetMeetingRecordingsTool: ToolConfig< + ZoomGetMeetingRecordingsParams, + ZoomGetMeetingRecordingsResponse +> = { + 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 new file mode 100644 index 000000000..3da1cda3b --- /dev/null +++ b/apps/sim/tools/zoom/index.ts @@ -0,0 +1,41 @@ +// 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, +} 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 000000000..db4eb8c10 --- /dev/null +++ b/apps/sim/tools/zoom/list_meetings.ts @@ -0,0 +1,138 @@ +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', + requiredScopes: ['meeting:read:list_meetings'], + }, + + 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/list_past_participants.ts b/apps/sim/tools/zoom/list_past_participants.ts new file mode 100644 index 000000000..d0c1ad12b --- /dev/null +++ b/apps/sim/tools/zoom/list_past_participants.ts @@ -0,0 +1,127 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZoomListPastParticipantsParams, + ZoomListPastParticipantsResponse, +} from '@/tools/zoom/types' + +export const zoomListPastParticipantsTool: ToolConfig< + ZoomListPastParticipantsParams, + ZoomListPastParticipantsResponse +> = { + 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 000000000..a0d90f3f7 --- /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 new file mode 100644 index 000000000..645464471 --- /dev/null +++ b/apps/sim/tools/zoom/types.ts @@ -0,0 +1,302 @@ +// 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 + } +} + +// 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 + | ZoomListMeetingsResponse + | ZoomGetMeetingResponse + | 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 new file mode 100644 index 000000000..18748bbe7 --- /dev/null +++ b/apps/sim/tools/zoom/update_meeting.ts @@ -0,0 +1,196 @@ +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', + requiredScopes: ['meeting:update:meeting'], + }, + + 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 8a698a9ea..9ae8476ad 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=="], diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index c2885e4ef..f863e53fe 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) {