From b4086a29b9afa0bb37e402d682b12192e5d82de5 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 17 Jan 2026 20:28:49 -0800 Subject: [PATCH 1/4] improvement(tools): added visibility for tools that were missing it, added new google tools --- apps/docs/content/docs/en/tools/github.mdx | 796 ++++++++++++++++++ .../content/docs/en/tools/google_calendar.mdx | 139 +++ .../content/docs/en/tools/google_drive.mdx | 485 ++++++++--- .../content/docs/en/tools/google_forms.mdx | 164 +++- .../content/docs/en/tools/google_groups.mdx | 187 ++++ .../content/docs/en/tools/google_sheets.mdx | 191 ++++- .../content/docs/en/tools/google_slides.mdx | 155 +++- apps/docs/content/docs/en/tools/knowledge.mdx | 16 +- .../content/docs/en/tools/microsoft_excel.mdx | 8 +- apps/docs/content/docs/en/tools/slack.mdx | 6 +- apps/sim/blocks/blocks/github.ts | 611 ++++++++++++++ apps/sim/blocks/blocks/google_calendar.ts | 247 +++++- apps/sim/blocks/blocks/google_drive.ts | 521 +++++++++++- apps/sim/blocks/blocks/google_form.ts | 279 +++++- apps/sim/blocks/blocks/google_groups.ts | 79 ++ apps/sim/blocks/blocks/google_sheets.ts | 724 +++++++++++++++- apps/sim/blocks/blocks/google_sheets_v2.ts | 360 -------- apps/sim/blocks/blocks/google_slides.ts | 395 ++++++++- apps/sim/blocks/blocks/microsoft_excel.ts | 262 +++++- apps/sim/blocks/blocks/microsoft_excel_v2.ts | 261 ------ apps/sim/blocks/registry.ts | 6 +- .../emcn/components/combobox/combobox.tsx | 11 +- apps/sim/tools/a2a/cancel_task.ts | 3 + .../sim/tools/a2a/delete_push_notification.ts | 4 + apps/sim/tools/a2a/get_agent_card.ts | 2 + apps/sim/tools/a2a/get_push_notification.ts | 3 + apps/sim/tools/a2a/get_task.ts | 4 + apps/sim/tools/a2a/resubscribe.ts | 3 + apps/sim/tools/a2a/send_message.ts | 7 + apps/sim/tools/a2a/set_push_notification.ts | 5 + apps/sim/tools/github/check_star.ts | 115 +++ apps/sim/tools/github/compare_commits.ts | 256 ++++++ .../tools/github/create_comment_reaction.ts | 138 +++ apps/sim/tools/github/create_gist.ts | 183 ++++ .../sim/tools/github/create_issue_reaction.ts | 138 +++ apps/sim/tools/github/create_milestone.ts | 182 ++++ .../tools/github/delete_comment_reaction.ts | 128 +++ apps/sim/tools/github/delete_gist.ts | 103 +++ .../sim/tools/github/delete_issue_reaction.ts | 128 +++ apps/sim/tools/github/delete_milestone.ts | 118 +++ apps/sim/tools/github/fork_gist.ts | 131 +++ apps/sim/tools/github/fork_repo.ts | 179 ++++ apps/sim/tools/github/get_commit.ts | 202 +++++ apps/sim/tools/github/get_gist.ts | 177 ++++ apps/sim/tools/github/get_milestone.ts | 167 ++++ apps/sim/tools/github/index.ts | 152 +++- apps/sim/tools/github/list_commits.ts | 243 ++++++ apps/sim/tools/github/list_forks.ts | 201 +++++ apps/sim/tools/github/list_gists.ts | 201 +++++ apps/sim/tools/github/list_milestones.ts | 226 +++++ apps/sim/tools/github/list_stargazers.ts | 172 ++++ apps/sim/tools/github/search_code.ts | 211 +++++ apps/sim/tools/github/search_commits.ts | 224 +++++ apps/sim/tools/github/search_issues.ts | 240 ++++++ apps/sim/tools/github/search_repos.ts | 226 +++++ apps/sim/tools/github/search_users.ts | 193 +++++ apps/sim/tools/github/star_gist.ts | 104 +++ apps/sim/tools/github/star_repo.ts | 116 +++ apps/sim/tools/github/unstar_gist.ts | 103 +++ apps/sim/tools/github/unstar_repo.ts | 115 +++ apps/sim/tools/github/update_gist.ts | 164 ++++ apps/sim/tools/github/update_milestone.ts | 186 ++++ apps/sim/tools/gitlab/cancel_pipeline.ts | 3 + apps/sim/tools/gitlab/create_issue.ts | 9 + apps/sim/tools/gitlab/create_issue_note.ts | 4 + apps/sim/tools/gitlab/create_merge_request.ts | 12 + .../tools/gitlab/create_merge_request_note.ts | 4 + apps/sim/tools/gitlab/create_pipeline.ts | 4 + apps/sim/tools/gitlab/delete_issue.ts | 3 + apps/sim/tools/gitlab/get_issue.ts | 3 + apps/sim/tools/gitlab/get_merge_request.ts | 3 + apps/sim/tools/gitlab/get_pipeline.ts | 3 + apps/sim/tools/gitlab/get_project.ts | 2 + apps/sim/tools/gitlab/list_issues.ts | 11 + apps/sim/tools/gitlab/list_merge_requests.ts | 10 + apps/sim/tools/gitlab/list_pipelines.ts | 8 + apps/sim/tools/gitlab/list_projects.ts | 9 + apps/sim/tools/gitlab/merge_merge_request.ts | 8 + apps/sim/tools/gitlab/retry_pipeline.ts | 3 + apps/sim/tools/gitlab/update_issue.ts | 11 + apps/sim/tools/gitlab/update_merge_request.ts | 13 + apps/sim/tools/google_calendar/delete.ts | 135 +++ apps/sim/tools/google_calendar/index.ts | 15 + apps/sim/tools/google_calendar/instances.ts | 268 ++++++ .../tools/google_calendar/list_calendars.ts | 280 ++++++ apps/sim/tools/google_calendar/move.ts | 208 +++++ apps/sim/tools/google_calendar/types.ts | 90 ++ apps/sim/tools/google_calendar/update.ts | 255 ++++++ apps/sim/tools/google_drive/copy.ts | 111 +++ apps/sim/tools/google_drive/delete.ts | 78 ++ apps/sim/tools/google_drive/get_about.ts | 137 +++ apps/sim/tools/google_drive/get_file.ts | 99 +++ apps/sim/tools/google_drive/index.ts | 20 + .../tools/google_drive/list_permissions.ts | 124 +++ apps/sim/tools/google_drive/share.ts | 178 ++++ apps/sim/tools/google_drive/trash.ts | 87 ++ apps/sim/tools/google_drive/unshare.ts | 93 ++ apps/sim/tools/google_drive/untrash.ts | 87 ++ apps/sim/tools/google_drive/update.ts | 140 +++ apps/sim/tools/google_form/batch_update.ts | 118 +++ apps/sim/tools/google_form/create_form.ts | 106 +++ apps/sim/tools/google_form/create_watch.ts | 120 +++ apps/sim/tools/google_form/delete_watch.ts | 76 ++ apps/sim/tools/google_form/get_form.ts | 119 +++ apps/sim/tools/google_form/index.ts | 18 + apps/sim/tools/google_form/list_watches.ts | 99 +++ apps/sim/tools/google_form/renew_watch.ts | 87 ++ .../tools/google_form/set_publish_settings.ts | 119 +++ apps/sim/tools/google_form/types.ts | 247 ++++++ apps/sim/tools/google_form/utils.ts | 52 ++ apps/sim/tools/google_groups/add_alias.ts | 75 ++ apps/sim/tools/google_groups/get_settings.ts | 151 ++++ apps/sim/tools/google_groups/index.ts | 10 + apps/sim/tools/google_groups/list_aliases.ts | 74 ++ apps/sim/tools/google_groups/remove_alias.ts | 68 ++ apps/sim/tools/google_groups/types.ts | 188 +++++ .../tools/google_groups/update_settings.ts | 396 +++++++++ apps/sim/tools/google_sheets/append.ts | 196 +++++ apps/sim/tools/google_sheets/append_v2.ts | 199 ----- apps/sim/tools/google_sheets/batch_clear.ts | 112 +++ apps/sim/tools/google_sheets/batch_get.ts | 143 ++++ apps/sim/tools/google_sheets/batch_update.ts | 149 ++++ .../google_sheets/{read_v2.ts => clear.ts} | 41 +- apps/sim/tools/google_sheets/copy_sheet.ts | 119 +++ .../tools/google_sheets/create_spreadsheet.ts | 139 +++ .../tools/google_sheets/get_spreadsheet.ts | 112 +++ apps/sim/tools/google_sheets/index.ts | 26 +- apps/sim/tools/google_sheets/read.ts | 115 ++- apps/sim/tools/google_sheets/types.ts | 154 ++++ apps/sim/tools/google_sheets/update.ts | 182 ++++ apps/sim/tools/google_sheets/update_v2.ts | 185 ---- apps/sim/tools/google_sheets/write.ts | 179 +++- apps/sim/tools/google_sheets/write_v2.ts | 177 ---- apps/sim/tools/google_slides/add_image.ts | 7 + apps/sim/tools/google_slides/add_slide.ts | 4 + apps/sim/tools/google_slides/create_shape.ts | 358 ++++++++ apps/sim/tools/google_slides/create_table.ts | 227 +++++ apps/sim/tools/google_slides/delete_object.ts | 131 +++ .../tools/google_slides/duplicate_object.ts | 152 ++++ apps/sim/tools/google_slides/get_page.ts | 142 ++++ apps/sim/tools/google_slides/get_thumbnail.ts | 4 + apps/sim/tools/google_slides/index.ts | 14 + apps/sim/tools/google_slides/insert_text.ts | 159 ++++ .../tools/google_slides/replace_all_text.ts | 5 + .../google_slides/update_slides_position.ts | 167 ++++ apps/sim/tools/google_slides/write.ts | 3 + apps/sim/tools/guardrails/validate.ts | 11 + apps/sim/tools/http/request.ts | 7 + apps/sim/tools/http/webhook_request.ts | 8 +- apps/sim/tools/kalshi/amend_order.ts | 11 + apps/sim/tools/kalshi/cancel_order.ts | 1 + apps/sim/tools/kalshi/create_order.ts | 17 + apps/sim/tools/kalshi/get_candlesticks.ts | 5 + apps/sim/tools/kalshi/get_event.ts | 2 + apps/sim/tools/kalshi/get_events.ts | 5 + apps/sim/tools/kalshi/get_fills.ts | 6 + apps/sim/tools/kalshi/get_market.ts | 1 + apps/sim/tools/kalshi/get_markets.ts | 5 + apps/sim/tools/kalshi/get_order.ts | 1 + apps/sim/tools/kalshi/get_orderbook.ts | 1 + apps/sim/tools/kalshi/get_orders.ts | 5 + apps/sim/tools/kalshi/get_positions.ts | 5 + apps/sim/tools/kalshi/get_series_by_ticker.ts | 1 + apps/sim/tools/kalshi/get_trades.ts | 2 + apps/sim/tools/memory/add.ts | 2 + apps/sim/tools/memory/delete.ts | 2 + apps/sim/tools/memory/get.ts | 2 + apps/sim/tools/microsoft_excel/index.ts | 6 +- apps/sim/tools/microsoft_excel/read.ts | 125 +++ apps/sim/tools/microsoft_excel/read_v2.ts | 133 --- apps/sim/tools/microsoft_excel/write.ts | 180 ++++ apps/sim/tools/microsoft_excel/write_v2.ts | 184 ---- apps/sim/tools/polymarket/get_event.ts | 2 + apps/sim/tools/polymarket/get_events.ts | 6 + .../tools/polymarket/get_last_trade_price.ts | 1 + apps/sim/tools/polymarket/get_market.ts | 2 + apps/sim/tools/polymarket/get_markets.ts | 6 + apps/sim/tools/polymarket/get_midpoint.ts | 1 + apps/sim/tools/polymarket/get_orderbook.ts | 1 + apps/sim/tools/polymarket/get_positions.ts | 2 + apps/sim/tools/polymarket/get_price.ts | 2 + .../sim/tools/polymarket/get_price_history.ts | 5 + apps/sim/tools/polymarket/get_series.ts | 2 + apps/sim/tools/polymarket/get_series_by_id.ts | 1 + apps/sim/tools/polymarket/get_spread.ts | 1 + apps/sim/tools/polymarket/get_tags.ts | 2 + apps/sim/tools/polymarket/get_tick_size.ts | 1 + apps/sim/tools/polymarket/get_trades.ts | 4 + apps/sim/tools/polymarket/search.ts | 3 + apps/sim/tools/qdrant/upsert_points.ts | 1 + apps/sim/tools/registry.ts | 249 +++++- apps/sim/tools/schema-enrichers.ts | 2 - apps/sim/tools/spotify/add_playlist_cover.ts | 2 + apps/sim/tools/spotify/check_following.ts | 2 + .../tools/spotify/check_playlist_followers.ts | 2 + apps/sim/tools/spotify/check_saved_albums.ts | 1 + .../tools/spotify/check_saved_audiobooks.ts | 1 + .../sim/tools/spotify/check_saved_episodes.ts | 1 + apps/sim/tools/spotify/check_saved_shows.ts | 1 + apps/sim/tools/spotify/follow_playlist.ts | 2 + apps/sim/tools/spotify/get_audiobook.ts | 2 + .../tools/spotify/get_audiobook_chapters.ts | 4 + apps/sim/tools/spotify/get_audiobooks.ts | 2 + apps/sim/tools/spotify/get_episode.ts | 2 + apps/sim/tools/spotify/get_episodes.ts | 2 + apps/sim/tools/spotify/get_playlist_cover.ts | 1 + apps/sim/tools/spotify/get_saved_albums.ts | 3 + .../sim/tools/spotify/get_saved_audiobooks.ts | 2 + apps/sim/tools/spotify/get_saved_episodes.ts | 3 + apps/sim/tools/spotify/get_saved_shows.ts | 2 + apps/sim/tools/spotify/get_show.ts | 2 + apps/sim/tools/spotify/get_show_episodes.ts | 4 + apps/sim/tools/spotify/get_shows.ts | 2 + apps/sim/tools/spotify/get_user_profile.ts | 1 + apps/sim/tools/spotify/remove_saved_albums.ts | 1 + .../tools/spotify/remove_saved_audiobooks.ts | 1 + .../tools/spotify/remove_saved_episodes.ts | 1 + apps/sim/tools/spotify/remove_saved_shows.ts | 1 + .../tools/spotify/reorder_playlist_items.ts | 5 + .../tools/spotify/replace_playlist_items.ts | 2 + apps/sim/tools/spotify/save_albums.ts | 1 + apps/sim/tools/spotify/save_audiobooks.ts | 1 + apps/sim/tools/spotify/save_episodes.ts | 1 + apps/sim/tools/spotify/save_shows.ts | 1 + apps/sim/tools/spotify/unfollow_playlist.ts | 1 + apps/sim/tools/spotify/update_playlist.ts | 4 + 226 files changed, 18923 insertions(+), 1771 deletions(-) delete mode 100644 apps/sim/blocks/blocks/google_sheets_v2.ts delete mode 100644 apps/sim/blocks/blocks/microsoft_excel_v2.ts create mode 100644 apps/sim/tools/github/check_star.ts create mode 100644 apps/sim/tools/github/compare_commits.ts create mode 100644 apps/sim/tools/github/create_comment_reaction.ts create mode 100644 apps/sim/tools/github/create_gist.ts create mode 100644 apps/sim/tools/github/create_issue_reaction.ts create mode 100644 apps/sim/tools/github/create_milestone.ts create mode 100644 apps/sim/tools/github/delete_comment_reaction.ts create mode 100644 apps/sim/tools/github/delete_gist.ts create mode 100644 apps/sim/tools/github/delete_issue_reaction.ts create mode 100644 apps/sim/tools/github/delete_milestone.ts create mode 100644 apps/sim/tools/github/fork_gist.ts create mode 100644 apps/sim/tools/github/fork_repo.ts create mode 100644 apps/sim/tools/github/get_commit.ts create mode 100644 apps/sim/tools/github/get_gist.ts create mode 100644 apps/sim/tools/github/get_milestone.ts create mode 100644 apps/sim/tools/github/list_commits.ts create mode 100644 apps/sim/tools/github/list_forks.ts create mode 100644 apps/sim/tools/github/list_gists.ts create mode 100644 apps/sim/tools/github/list_milestones.ts create mode 100644 apps/sim/tools/github/list_stargazers.ts create mode 100644 apps/sim/tools/github/search_code.ts create mode 100644 apps/sim/tools/github/search_commits.ts create mode 100644 apps/sim/tools/github/search_issues.ts create mode 100644 apps/sim/tools/github/search_repos.ts create mode 100644 apps/sim/tools/github/search_users.ts create mode 100644 apps/sim/tools/github/star_gist.ts create mode 100644 apps/sim/tools/github/star_repo.ts create mode 100644 apps/sim/tools/github/unstar_gist.ts create mode 100644 apps/sim/tools/github/unstar_repo.ts create mode 100644 apps/sim/tools/github/update_gist.ts create mode 100644 apps/sim/tools/github/update_milestone.ts create mode 100644 apps/sim/tools/google_calendar/delete.ts create mode 100644 apps/sim/tools/google_calendar/instances.ts create mode 100644 apps/sim/tools/google_calendar/list_calendars.ts create mode 100644 apps/sim/tools/google_calendar/move.ts create mode 100644 apps/sim/tools/google_calendar/update.ts create mode 100644 apps/sim/tools/google_drive/copy.ts create mode 100644 apps/sim/tools/google_drive/delete.ts create mode 100644 apps/sim/tools/google_drive/get_about.ts create mode 100644 apps/sim/tools/google_drive/get_file.ts create mode 100644 apps/sim/tools/google_drive/list_permissions.ts create mode 100644 apps/sim/tools/google_drive/share.ts create mode 100644 apps/sim/tools/google_drive/trash.ts create mode 100644 apps/sim/tools/google_drive/unshare.ts create mode 100644 apps/sim/tools/google_drive/untrash.ts create mode 100644 apps/sim/tools/google_drive/update.ts create mode 100644 apps/sim/tools/google_form/batch_update.ts create mode 100644 apps/sim/tools/google_form/create_form.ts create mode 100644 apps/sim/tools/google_form/create_watch.ts create mode 100644 apps/sim/tools/google_form/delete_watch.ts create mode 100644 apps/sim/tools/google_form/get_form.ts create mode 100644 apps/sim/tools/google_form/list_watches.ts create mode 100644 apps/sim/tools/google_form/renew_watch.ts create mode 100644 apps/sim/tools/google_form/set_publish_settings.ts create mode 100644 apps/sim/tools/google_groups/add_alias.ts create mode 100644 apps/sim/tools/google_groups/get_settings.ts create mode 100644 apps/sim/tools/google_groups/list_aliases.ts create mode 100644 apps/sim/tools/google_groups/remove_alias.ts create mode 100644 apps/sim/tools/google_groups/update_settings.ts delete mode 100644 apps/sim/tools/google_sheets/append_v2.ts create mode 100644 apps/sim/tools/google_sheets/batch_clear.ts create mode 100644 apps/sim/tools/google_sheets/batch_get.ts create mode 100644 apps/sim/tools/google_sheets/batch_update.ts rename apps/sim/tools/google_sheets/{read_v2.ts => clear.ts} (66%) create mode 100644 apps/sim/tools/google_sheets/copy_sheet.ts create mode 100644 apps/sim/tools/google_sheets/create_spreadsheet.ts create mode 100644 apps/sim/tools/google_sheets/get_spreadsheet.ts delete mode 100644 apps/sim/tools/google_sheets/update_v2.ts delete mode 100644 apps/sim/tools/google_sheets/write_v2.ts create mode 100644 apps/sim/tools/google_slides/create_shape.ts create mode 100644 apps/sim/tools/google_slides/create_table.ts create mode 100644 apps/sim/tools/google_slides/delete_object.ts create mode 100644 apps/sim/tools/google_slides/duplicate_object.ts create mode 100644 apps/sim/tools/google_slides/get_page.ts create mode 100644 apps/sim/tools/google_slides/insert_text.ts create mode 100644 apps/sim/tools/google_slides/update_slides_position.ts delete mode 100644 apps/sim/tools/microsoft_excel/read_v2.ts delete mode 100644 apps/sim/tools/microsoft_excel/write_v2.ts diff --git a/apps/docs/content/docs/en/tools/github.mdx b/apps/docs/content/docs/en/tools/github.mdx index ef05e6ae91..ba06eb6a4a 100644 --- a/apps/docs/content/docs/en/tools/github.mdx +++ b/apps/docs/content/docs/en/tools/github.mdx @@ -1535,4 +1535,800 @@ Delete a GitHub Project V2. This action is permanent and cannot be undone. Requi | `number` | number | Deleted project number | | `url` | string | Deleted project URL | +### `github_search_code` + +Search for code across GitHub repositories. Use qualifiers like repo:owner/name, language:js, path:src, extension:py + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query with optional qualifiers \(repo:, language:, path:, extension:, user:, org:\) | +| `sort` | string | No | Sort by indexed date \(default: best match\) | +| `order` | string | No | Sort order: asc or desc \(default: desc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `total_count` | number | Total matching results | +| `incomplete_results` | boolean | Whether results are incomplete | +| `items` | array | Array of code matches from GitHub API | +| ↳ `name` | string | File name | +| ↳ `path` | string | File path | +| ↳ `sha` | string | Blob SHA | +| ↳ `html_url` | string | GitHub web URL | +| ↳ `repository` | object | Repository object | + +### `github_search_commits` + +Search for commits across GitHub. Use qualifiers like repo:owner/name, author:user, committer:user, author-date:>2023-01-01 + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query with optional qualifiers \(repo:, author:, committer:, author-date:, committer-date:, merge:true/false\) | +| `sort` | string | No | Sort by: author-date or committer-date \(default: best match\) | +| `order` | string | No | Sort order: asc or desc \(default: desc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `total_count` | number | Total matching results | +| `incomplete_results` | boolean | Whether results are incomplete | +| `items` | array | Array of commit objects from GitHub API | +| ↳ `sha` | string | Commit SHA | +| ↳ `html_url` | string | Web URL | +| ↳ `commit` | object | Commit data | +| ↳ `author` | object | GitHub user | +| ↳ `committer` | object | GitHub user | +| ↳ `repository` | object | Repository | + +### `github_search_issues` + +Search for issues and pull requests across GitHub. Use qualifiers like repo:owner/name, is:issue, is:pr, state:open, label:bug, author:user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query with optional qualifiers \(repo:, is:issue, is:pr, state:, label:, author:, assignee:\) | +| `sort` | string | No | Sort by: comments, reactions, created, updated, interactions \(default: best match\) | +| `order` | string | No | Sort order: asc or desc \(default: desc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `total_count` | number | Total matching results | +| `incomplete_results` | boolean | Whether results are incomplete | +| `items` | array | Array of issue/PR objects from GitHub API | +| ↳ `id` | number | Issue ID | +| ↳ `number` | number | Issue number | +| ↳ `title` | string | Title | +| ↳ `state` | string | State | +| ↳ `html_url` | string | Web URL | +| ↳ `body` | string | Body text | +| ↳ `user` | object | Author | +| ↳ `labels` | array | Labels | +| ↳ `assignees` | array | Assignees | +| ↳ `created_at` | string | Creation date | +| ↳ `updated_at` | string | Update date | +| ↳ `closed_at` | string | Close date | + +### `github_search_repos` + +Search for repositories across GitHub. Use qualifiers like language:python, stars:>1000, topic:react, user:owner, org:name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query with optional qualifiers \(language:, stars:, forks:, topic:, user:, org:, in:name,description,readme\) | +| `sort` | string | No | Sort by: stars, forks, help-wanted-issues, updated \(default: best match\) | +| `order` | string | No | Sort order: asc or desc \(default: desc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `total_count` | number | Total matching results | +| `incomplete_results` | boolean | Whether results are incomplete | +| `items` | array | Array of repository objects from GitHub API | +| ↳ `id` | number | Repository ID | +| ↳ `full_name` | string | Full name | +| ↳ `description` | string | Description | +| ↳ `html_url` | string | Web URL | +| ↳ `stargazers_count` | number | Stars | +| ↳ `forks_count` | number | Forks | +| ↳ `open_issues_count` | number | Open issues | +| ↳ `language` | string | Language | +| ↳ `topics` | array | Topics | +| ↳ `owner` | object | Owner | + +### `github_search_users` + +Search for users and organizations on GitHub. Use qualifiers like type:user, type:org, followers:>1000, repos:>10, location:city + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query with optional qualifiers \(type:user/org, followers:, repos:, location:, language:, created:\) | +| `sort` | string | No | Sort by: followers, repositories, joined \(default: best match\) | +| `order` | string | No | Sort order: asc or desc \(default: desc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `total_count` | number | Total matching results | +| `incomplete_results` | boolean | Whether results are incomplete | +| `items` | array | Array of user objects from GitHub API | +| ↳ `id` | number | User ID | +| ↳ `login` | string | Username | +| ↳ `html_url` | string | Profile URL | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | Is site admin | + +### `github_list_commits` + +List commits in a repository with optional filtering by SHA, path, author, committer, or date range + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `sha` | string | No | SHA or branch to start listing commits from | +| `path` | string | No | Only commits containing this file path | +| `author` | string | No | GitHub login or email address to filter by author | +| `committer` | string | No | GitHub login or email address to filter by committer | +| `since` | string | No | Only commits after this date \(ISO 8601 format\) | +| `until` | string | No | Only commits before this date \(ISO 8601 format\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of commit objects from GitHub API | +| ↳ `sha` | string | Commit SHA | +| ↳ `html_url` | string | Web URL | +| ↳ `commit` | object | Commit data | +| ↳ `author` | object | GitHub user | +| ↳ `committer` | object | GitHub user | +| ↳ `parents` | array | Parent commits | +| `count` | number | Number of commits returned | + +### `github_get_commit` + +Get detailed information about a specific commit including files changed and stats + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `ref` | string | Yes | Commit SHA, branch name, or tag name | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sha` | string | Commit SHA | +| `html_url` | string | Web URL | +| `commit` | object | Commit data | +| `author` | object | GitHub user | +| `committer` | object | GitHub user | +| `stats` | object | Change stats | +| `files` | array | Changed files | +| `parents` | array | Parent commits | + +### `github_compare_commits` + +Compare two commits or branches to see the diff, commits between them, and changed files + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `base` | string | Yes | Base branch/tag/SHA for comparison | +| `head` | string | Yes | Head branch/tag/SHA for comparison | +| `per_page` | number | No | Results per page for files \(max 100, default: 30\) | +| `page` | number | No | Page number for files \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Comparison status | +| `ahead_by` | number | Commits ahead | +| `behind_by` | number | Commits behind | +| `total_commits` | number | Total commits | +| `html_url` | string | Web URL | +| `diff_url` | string | Diff URL | +| `patch_url` | string | Patch URL | +| `base_commit` | object | Base commit | +| `merge_base_commit` | object | Merge base | +| `commits` | array | Commits between | +| `files` | array | Changed files | + +### `github_create_gist` + +Create a new gist with one or more files + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `description` | string | No | Description of the gist | +| `files` | json | Yes | JSON object with filenames as keys and content as values. Example: \{"file.txt": \{"content": "Hello"\}\} | +| `Example` | string | No | No description | +| `public` | boolean | No | Whether the gist is public \(default: false\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Gist ID | +| `html_url` | string | Web URL | +| `git_pull_url` | string | Git pull URL | +| `git_push_url` | string | Git push URL | +| `description` | string | Description | +| `public` | boolean | Is public | +| `created_at` | string | Creation date | +| `updated_at` | string | Update date | +| `files` | object | Files in gist | +| `owner` | object | Owner info | + +### `github_get_gist` + +Get a gist by ID including its file contents + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Gist ID | +| `html_url` | string | Web URL | +| `description` | string | Description | +| `public` | boolean | Is public | +| `created_at` | string | Creation date | +| `updated_at` | string | Update date | +| `files` | object | Files with content | +| `owner` | object | Owner info | +| `comments` | number | Comment count | + +### `github_list_gists` + +List gists for a user or the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `username` | string | No | GitHub username \(omit for authenticated user's gists\) | +| `since` | string | No | Only gists updated after this time \(ISO 8601\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of gist objects from GitHub API | +| ↳ `id` | string | Gist ID | +| ↳ `html_url` | string | Web URL | +| ↳ `description` | string | Description | +| ↳ `public` | boolean | Is public | +| ↳ `files` | object | Files | +| ↳ `owner` | object | Owner | +| `count` | number | Number of gists returned | + +### `github_update_gist` + +Update a gist description or files. To delete a file, set its value to null in files object + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID to update | +| `description` | string | No | New description for the gist | +| `files` | json | No | JSON object with filenames as keys. Set to null to delete, or provide content to update/add | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Gist ID | +| `html_url` | string | Web URL | +| `description` | string | Description | +| `public` | boolean | Is public | +| `updated_at` | string | Update date | +| `files` | object | Current files | + +### `github_delete_gist` + +Delete a gist by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID to delete | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether deletion succeeded | +| `gist_id` | string | The deleted gist ID | + +### `github_fork_gist` + +Fork a gist to create your own copy + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID to fork | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | New gist ID | +| `html_url` | string | Web URL | +| `description` | string | Description | +| `public` | boolean | Is public | +| `created_at` | string | Creation date | +| `owner` | object | Owner info | +| `files` | object | Files | + +### `github_star_gist` + +Star a gist + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID to star | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `starred` | boolean | Whether starring succeeded | +| `gist_id` | string | The gist ID | + +### `github_unstar_gist` + +Unstar a gist + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `gist_id` | string | Yes | The gist ID to unstar | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `unstarred` | boolean | Whether unstarring succeeded | +| `gist_id` | string | The gist ID | + +### `github_fork_repo` + +Fork a repository to your account or an organization + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner to fork from | +| `repo` | string | Yes | Repository name to fork | +| `organization` | string | No | Organization to fork into \(omit to fork to your account\) | +| `name` | string | No | Custom name for the forked repository | +| `default_branch_only` | boolean | No | Only fork the default branch \(default: false\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Repository ID | +| `full_name` | string | Full name | +| `html_url` | string | Web URL | +| `clone_url` | string | Clone URL | +| `ssh_url` | string | SSH URL | +| `default_branch` | string | Default branch | +| `fork` | boolean | Is a fork | +| `parent` | object | Parent repository | +| `owner` | object | Owner | + +### `github_list_forks` + +List forks of a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `sort` | string | No | Sort by: newest, oldest, stargazers, watchers \(default: newest\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of fork repository objects from GitHub API | +| ↳ `id` | number | Repository ID | +| ↳ `full_name` | string | Full name | +| ↳ `html_url` | string | Web URL | +| ↳ `owner` | object | Owner | +| ↳ `stargazers_count` | number | Stars | +| ↳ `forks_count` | number | Forks | +| `count` | number | Number of forks returned | + +### `github_create_milestone` + +Create a milestone in a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `title` | string | Yes | Milestone title | +| `state` | string | No | State: open or closed \(default: open\) | +| `description` | string | No | Milestone description | +| `due_on` | string | No | Due date \(ISO 8601 format, e.g., 2024-12-31T23:59:59Z\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `number` | number | Milestone number | +| `title` | string | Title | +| `description` | string | Description | +| `state` | string | State | +| `html_url` | string | Web URL | +| `due_on` | string | Due date | +| `open_issues` | number | Open issues | +| `closed_issues` | number | Closed issues | +| `creator` | object | Creator | + +### `github_get_milestone` + +Get a specific milestone by number + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `milestone_number` | number | Yes | Milestone number | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `number` | number | Milestone number | +| `title` | string | Title | +| `description` | string | Description | +| `state` | string | State | +| `html_url` | string | Web URL | +| `due_on` | string | Due date | +| `open_issues` | number | Open issues | +| `closed_issues` | number | Closed issues | +| `closed_at` | string | Close date | +| `creator` | object | Creator | + +### `github_list_milestones` + +List milestones in a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `state` | string | No | Filter by state: open, closed, all \(default: open\) | +| `sort` | string | No | Sort by: due_on or completeness \(default: due_on\) | +| `direction` | string | No | Sort direction: asc or desc \(default: asc\) | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of milestone objects from GitHub API | +| ↳ `number` | number | Milestone number | +| ↳ `title` | string | Title | +| ↳ `state` | string | State | +| ↳ `html_url` | string | Web URL | +| ↳ `open_issues` | number | Open issues | +| ↳ `closed_issues` | number | Closed issues | +| `count` | number | Number of milestones returned | + +### `github_update_milestone` + +Update a milestone in a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `milestone_number` | number | Yes | Milestone number to update | +| `title` | string | No | New milestone title | +| `state` | string | No | New state: open or closed | +| `description` | string | No | New description | +| `due_on` | string | No | New due date \(ISO 8601 format\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `number` | number | Milestone number | +| `title` | string | Title | +| `description` | string | Description | +| `state` | string | State | +| `html_url` | string | Web URL | +| `due_on` | string | Due date | +| `open_issues` | number | Open issues | +| `closed_issues` | number | Closed issues | + +### `github_delete_milestone` + +Delete a milestone from a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `milestone_number` | number | Yes | Milestone number to delete | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether deletion succeeded | +| `milestone_number` | number | The deleted milestone number | + +### `github_create_issue_reaction` + +Add a reaction to an issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `issue_number` | number | Yes | Issue number | +| `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Reaction ID | +| `user` | object | User who reacted | +| `content` | string | Reaction type | +| `created_at` | string | Creation date | + +### `github_delete_issue_reaction` + +Remove a reaction from an issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `issue_number` | number | Yes | Issue number | +| `reaction_id` | number | Yes | Reaction ID to delete | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether deletion succeeded | +| `reaction_id` | number | The deleted reaction ID | + +### `github_create_comment_reaction` + +Add a reaction to an issue comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `comment_id` | number | Yes | Comment ID | +| `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Reaction ID | +| `user` | object | User who reacted | +| `content` | string | Reaction type | +| `created_at` | string | Creation date | + +### `github_delete_comment_reaction` + +Remove a reaction from an issue comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `comment_id` | number | Yes | Comment ID | +| `reaction_id` | number | Yes | Reaction ID to delete | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether deletion succeeded | +| `reaction_id` | number | The deleted reaction ID | + +### `github_star_repo` + +Star a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `starred` | boolean | Whether starring succeeded | +| `owner` | string | Repository owner | +| `repo` | string | Repository name | + +### `github_unstar_repo` + +Remove star from a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `unstarred` | boolean | Whether unstarring succeeded | +| `owner` | string | Repository owner | +| `repo` | string | Repository name | + +### `github_check_star` + +Check if you have starred a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `starred` | boolean | Whether you have starred the repo | +| `owner` | string | Repository owner | +| `repo` | string | Repository name | + +### `github_list_stargazers` + +List users who have starred a repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `per_page` | number | No | Results per page \(max 100, default: 30\) | +| `page` | number | No | Page number \(default: 1\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of user objects from GitHub API | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| `count` | number | Number of stargazers returned | + diff --git a/apps/docs/content/docs/en/tools/google_calendar.mdx b/apps/docs/content/docs/en/tools/google_calendar.mdx index 535c32f678..57d5df9673 100644 --- a/apps/docs/content/docs/en/tools/google_calendar.mdx +++ b/apps/docs/content/docs/en/tools/google_calendar.mdx @@ -119,6 +119,145 @@ Get a specific event from Google Calendar. Returns API-aligned fields only. | `creator` | json | Event creator | | `organizer` | json | Event organizer | +### `google_calendar_update` + +Update an existing event in Google Calendar. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Calendar ID \(defaults to primary\) | +| `eventId` | string | Yes | Event ID to update | +| `summary` | string | No | New event title/summary | +| `description` | string | No | New event description | +| `location` | string | No | New event location | +| `startDateTime` | string | No | New start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter | +| `endDateTime` | string | No | New end date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter | +| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. | +| `attendees` | array | No | Array of attendee email addresses \(replaces existing attendees\) | +| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | + +### `google_calendar_delete` + +Delete an event from Google Calendar. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Calendar ID \(defaults to primary\) | +| `eventId` | string | Yes | Event ID to delete | +| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventId` | string | Deleted event ID | +| `deleted` | boolean | Whether deletion was successful | + +### `google_calendar_move` + +Move an event to a different calendar. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Source calendar ID \(defaults to primary\) | +| `eventId` | string | Yes | Event ID to move | +| `destinationCalendarId` | string | Yes | Destination calendar ID | +| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | + +### `google_calendar_instances` + +Get instances of a recurring event from Google Calendar. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Calendar ID \(defaults to primary\) | +| `eventId` | string | Yes | Recurring event ID to get instances of | +| `timeMin` | string | No | Lower bound for instances \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) | +| `timeMax` | string | No | Upper bound for instances \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) | +| `maxResults` | number | No | Maximum number of instances to return \(default 250, max 2500\) | +| `pageToken` | string | No | Token for retrieving subsequent pages of results | +| `showDeleted` | boolean | No | Include deleted instances | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `nextPageToken` | string | Next page token | +| `timeZone` | string | Calendar time zone | +| `instances` | json | List of recurring event instances | + +### `google_calendar_list_calendars` + +List all calendars in the user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `minAccessRole` | string | No | Minimum access role for returned calendars: freeBusyReader, reader, writer, or owner | +| `maxResults` | number | No | Maximum number of calendars to return \(default 100, max 250\) | +| `pageToken` | string | No | Token for retrieving subsequent pages of results | +| `showDeleted` | boolean | No | Include deleted calendars | +| `showHidden` | boolean | No | Include hidden calendars | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `nextPageToken` | string | Next page token | +| `calendars` | array | List of calendars | +| ↳ `id` | string | Calendar ID | +| ↳ `summary` | string | Calendar title | +| ↳ `description` | string | Calendar description | +| ↳ `location` | string | Calendar location | +| ↳ `timeZone` | string | Calendar time zone | +| ↳ `accessRole` | string | Access role for the calendar | +| ↳ `backgroundColor` | string | Calendar background color | +| ↳ `foregroundColor` | string | Calendar foreground color | +| ↳ `primary` | boolean | Whether this is the primary calendar | +| ↳ `hidden` | boolean | Whether the calendar is hidden | +| ↳ `selected` | boolean | Whether the calendar is selected | + ### `google_calendar_quick_add` Create events from natural language text. Returns API-aligned fields only. diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index 6dc71fee68..c68a86f345 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -1,6 +1,6 @@ --- title: Google Drive -description: Create, upload, and list files +description: Manage files, folders, and permissions --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -40,84 +40,122 @@ In Sim, the Google Drive integration enables your agents to interact directly wi ## Usage Instructions -Integrate Google Drive into the workflow. Can create, upload, and list files. +Integrate Google Drive into the workflow. Can create, upload, download, copy, move, delete, share files and manage permissions. ## Tools -### `google_drive_upload` +### `google_drive_list` -Upload a file to Google Drive with complete metadata returned +List files and folders in Google Drive with complete metadata #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `fileName` | string | Yes | The name of the file to upload | -| `file` | file | No | Binary file to upload \(UserFile object\) | -| `content` | string | No | Text content to upload \(use this OR file, not both\) | -| `mimeType` | string | No | The MIME type of the file to upload \(auto-detected from file if not provided\) | -| `folderSelector` | string | No | Select the folder to upload the file to | -| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | +| `folderSelector` | string | No | Select the folder to list files from | +| `folderId` | string | No | The ID of the folder to list files from \(internal use\) | +| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. | +| `pageSize` | number | No | The maximum number of files to return \(default: 100\) | +| `pageToken` | string | No | The page token to use for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | object | Complete uploaded file metadata from Google Drive | +| `files` | array | Array of file metadata objects from Google Drive | +| ↳ `id` | string | Google Drive file ID | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `kind` | string | Resource type identifier | +| ↳ `description` | string | File description | +| ↳ `originalFilename` | string | Original uploaded filename | +| ↳ `fullFileExtension` | string | Full file extension | +| ↳ `fileExtension` | string | File extension | +| ↳ `owners` | json | List of file owners | +| ↳ `permissions` | json | File permissions | +| ↳ `permissionIds` | json | Permission IDs | +| ↳ `shared` | boolean | Whether file is shared | +| ↳ `ownedByMe` | boolean | Whether owned by current user | +| ↳ `writersCanShare` | boolean | Whether writers can share | +| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy | +| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission | +| ↳ `sharingUser` | json | User who shared the file | +| ↳ `starred` | boolean | Whether file is starred | +| ↳ `trashed` | boolean | Whether file is in trash | +| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed | +| ↳ `appProperties` | json | App-specific properties | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `modifiedByMeTime` | string | When modified by current user | +| ↳ `viewedByMeTime` | string | When last viewed by current user | +| ↳ `sharedWithMeTime` | string | When shared with current user | +| ↳ `lastModifyingUser` | json | User who last modified the file | +| ↳ `viewedByMe` | boolean | Whether viewed by current user | +| ↳ `modifiedByMe` | boolean | Whether modified by current user | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `webContentLink` | string | Direct download URL | +| ↳ `iconLink` | string | URL to file icon | +| ↳ `thumbnailLink` | string | URL to thumbnail | +| ↳ `exportLinks` | json | Export format links | +| ↳ `size` | string | File size in bytes | +| ↳ `quotaBytesUsed` | string | Storage quota used | +| ↳ `md5Checksum` | string | MD5 hash | +| ↳ `sha1Checksum` | string | SHA-1 hash | +| ↳ `sha256Checksum` | string | SHA-256 hash | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `spaces` | json | Spaces containing file | +| ↳ `driveId` | string | Shared drive ID | +| ↳ `capabilities` | json | User capabilities on file | +| ↳ `version` | string | Version number | +| ↳ `headRevisionId` | string | Head revision ID | +| ↳ `hasThumbnail` | boolean | Whether has thumbnail | +| ↳ `thumbnailVersion` | string | Thumbnail version | +| ↳ `imageMediaMetadata` | json | Image-specific metadata | +| ↳ `videoMediaMetadata` | json | Video-specific metadata | +| ↳ `isAppAuthorized` | boolean | Whether created by requesting app | +| ↳ `contentRestrictions` | json | Content restrictions | +| ↳ `linkShareMetadata` | json | Link share metadata | +| `nextPageToken` | string | Token for fetching the next page of results | + +### `google_drive_get_file` + +Get metadata for a specific file in Google Drive by its ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The file metadata | | ↳ `id` | string | Google Drive file ID | | ↳ `name` | string | File name | | ↳ `mimeType` | string | MIME type | -| ↳ `kind` | string | Resource type identifier | | ↳ `description` | string | File description | -| ↳ `originalFilename` | string | Original uploaded filename | -| ↳ `fullFileExtension` | string | Full file extension | -| ↳ `fileExtension` | string | File extension | -| ↳ `owners` | json | List of file owners | -| ↳ `permissions` | json | File permissions | -| ↳ `permissionIds` | json | Permission IDs | -| ↳ `shared` | boolean | Whether file is shared | -| ↳ `ownedByMe` | boolean | Whether owned by current user | -| ↳ `writersCanShare` | boolean | Whether writers can share | -| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy | -| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission | -| ↳ `sharingUser` | json | User who shared the file | +| ↳ `size` | string | File size in bytes | | ↳ `starred` | boolean | Whether file is starred | | ↳ `trashed` | boolean | Whether file is in trash | -| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed | -| ↳ `appProperties` | json | App-specific properties | -| ↳ `createdTime` | string | File creation time | -| ↳ `modifiedTime` | string | Last modification time | -| ↳ `modifiedByMeTime` | string | When modified by current user | -| ↳ `viewedByMeTime` | string | When last viewed by current user | -| ↳ `sharedWithMeTime` | string | When shared with current user | -| ↳ `lastModifyingUser` | json | User who last modified the file | -| ↳ `viewedByMe` | boolean | Whether viewed by current user | -| ↳ `modifiedByMe` | boolean | Whether modified by current user | | ↳ `webViewLink` | string | URL to view in browser | | ↳ `webContentLink` | string | Direct download URL | | ↳ `iconLink` | string | URL to file icon | | ↳ `thumbnailLink` | string | URL to thumbnail | -| ↳ `exportLinks` | json | Export format links | -| ↳ `size` | string | File size in bytes | -| ↳ `quotaBytesUsed` | string | Storage quota used | -| ↳ `md5Checksum` | string | MD5 hash | -| ↳ `sha1Checksum` | string | SHA-1 hash | -| ↳ `sha256Checksum` | string | SHA-256 hash | | ↳ `parents` | json | Parent folder IDs | -| ↳ `spaces` | json | Spaces containing file | -| ↳ `driveId` | string | Shared drive ID | +| ↳ `owners` | json | List of file owners | +| ↳ `permissions` | json | File permissions | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `lastModifyingUser` | json | User who last modified the file | +| ↳ `shared` | boolean | Whether file is shared | +| ↳ `ownedByMe` | boolean | Whether owned by current user | | ↳ `capabilities` | json | User capabilities on file | +| ↳ `md5Checksum` | string | MD5 hash | | ↳ `version` | string | Version number | -| ↳ `headRevisionId` | string | Head revision ID | -| ↳ `hasThumbnail` | boolean | Whether has thumbnail | -| ↳ `thumbnailVersion` | string | Thumbnail version | -| ↳ `imageMediaMetadata` | json | Image-specific metadata | -| ↳ `videoMediaMetadata` | json | Video-specific metadata | -| ↳ `isAppAuthorized` | boolean | Whether created by requesting app | -| ↳ `contentRestrictions` | json | Content restrictions | -| ↳ `linkShareMetadata` | json | Link share metadata | ### `google_drive_create_folder` @@ -174,6 +212,79 @@ Create a new folder in Google Drive with complete metadata returned | ↳ `contentRestrictions` | json | Content restrictions | | ↳ `linkShareMetadata` | json | Link share metadata | +### `google_drive_upload` + +Upload a file to Google Drive with complete metadata returned + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileName` | string | Yes | The name of the file to upload | +| `file` | file | No | Binary file to upload \(UserFile object\) | +| `content` | string | No | Text content to upload \(use this OR file, not both\) | +| `mimeType` | string | No | The MIME type of the file to upload \(auto-detected from file if not provided\) | +| `folderSelector` | string | No | Select the folder to upload the file to | +| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | Complete uploaded file metadata from Google Drive | +| ↳ `id` | string | Google Drive file ID | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `kind` | string | Resource type identifier | +| ↳ `description` | string | File description | +| ↳ `originalFilename` | string | Original uploaded filename | +| ↳ `fullFileExtension` | string | Full file extension | +| ↳ `fileExtension` | string | File extension | +| ↳ `owners` | json | List of file owners | +| ↳ `permissions` | json | File permissions | +| ↳ `permissionIds` | json | Permission IDs | +| ↳ `shared` | boolean | Whether file is shared | +| ↳ `ownedByMe` | boolean | Whether owned by current user | +| ↳ `writersCanShare` | boolean | Whether writers can share | +| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy | +| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission | +| ↳ `sharingUser` | json | User who shared the file | +| ↳ `starred` | boolean | Whether file is starred | +| ↳ `trashed` | boolean | Whether file is in trash | +| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed | +| ↳ `appProperties` | json | App-specific properties | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `modifiedByMeTime` | string | When modified by current user | +| ↳ `viewedByMeTime` | string | When last viewed by current user | +| ↳ `sharedWithMeTime` | string | When shared with current user | +| ↳ `lastModifyingUser` | json | User who last modified the file | +| ↳ `viewedByMe` | boolean | Whether viewed by current user | +| ↳ `modifiedByMe` | boolean | Whether modified by current user | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `webContentLink` | string | Direct download URL | +| ↳ `iconLink` | string | URL to file icon | +| ↳ `thumbnailLink` | string | URL to thumbnail | +| ↳ `exportLinks` | json | Export format links | +| ↳ `size` | string | File size in bytes | +| ↳ `quotaBytesUsed` | string | Storage quota used | +| ↳ `md5Checksum` | string | MD5 hash | +| ↳ `sha1Checksum` | string | SHA-1 hash | +| ↳ `sha256Checksum` | string | SHA-256 hash | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `spaces` | json | Spaces containing file | +| ↳ `driveId` | string | Shared drive ID | +| ↳ `capabilities` | json | User capabilities on file | +| ↳ `version` | string | Version number | +| ↳ `headRevisionId` | string | Head revision ID | +| ↳ `hasThumbnail` | boolean | Whether has thumbnail | +| ↳ `thumbnailVersion` | string | Thumbnail version | +| ↳ `imageMediaMetadata` | json | Image-specific metadata | +| ↳ `videoMediaMetadata` | json | Video-specific metadata | +| ↳ `isAppAuthorized` | boolean | Whether created by requesting app | +| ↳ `contentRestrictions` | json | Content restrictions | +| ↳ `linkShareMetadata` | json | Link share metadata | + ### `google_drive_download` Download a file from Google Drive with complete metadata (exports Google Workspace files automatically) @@ -251,77 +362,229 @@ Download a file from Google Drive with complete metadata (exports Google Workspa | ↳ `linkShareMetadata` | json | Link share metadata | | ↳ `revisions` | json | File revision history \(first 100 revisions only\) | -### `google_drive_list` +### `google_drive_copy` -List files and folders in Google Drive with complete metadata +Create a copy of a file in Google Drive #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `folderSelector` | string | No | Select the folder to list files from | -| `folderId` | string | No | The ID of the folder to list files from \(internal use\) | -| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. | -| `pageSize` | number | No | The maximum number of files to return \(default: 100\) | -| `pageToken` | string | No | The page token to use for pagination | +| `fileId` | string | Yes | The ID of the file to copy | +| `newName` | string | No | Name for the copied file \(defaults to "Copy of \[original name\]"\) | +| `destinationFolderId` | string | No | ID of the folder to place the copy in \(defaults to same location as original\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `files` | array | Array of file metadata objects from Google Drive | -| ↳ `id` | string | Google Drive file ID | -| ↳ `name` | string | File name | -| ↳ `mimeType` | string | MIME type | -| ↳ `kind` | string | Resource type identifier | -| ↳ `description` | string | File description | -| ↳ `originalFilename` | string | Original uploaded filename | -| ↳ `fullFileExtension` | string | Full file extension | -| ↳ `fileExtension` | string | File extension | -| ↳ `owners` | json | List of file owners | -| ↳ `permissions` | json | File permissions | -| ↳ `permissionIds` | json | Permission IDs | -| ↳ `shared` | boolean | Whether file is shared | -| ↳ `ownedByMe` | boolean | Whether owned by current user | -| ↳ `writersCanShare` | boolean | Whether writers can share | -| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy | -| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission | -| ↳ `sharingUser` | json | User who shared the file | -| ↳ `starred` | boolean | Whether file is starred | -| ↳ `trashed` | boolean | Whether file is in trash | -| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed | -| ↳ `appProperties` | json | App-specific properties | -| ↳ `createdTime` | string | File creation time | -| ↳ `modifiedTime` | string | Last modification time | -| ↳ `modifiedByMeTime` | string | When modified by current user | -| ↳ `viewedByMeTime` | string | When last viewed by current user | -| ↳ `sharedWithMeTime` | string | When shared with current user | -| ↳ `lastModifyingUser` | json | User who last modified the file | -| ↳ `viewedByMe` | boolean | Whether viewed by current user | -| ↳ `modifiedByMe` | boolean | Whether modified by current user | -| ↳ `webViewLink` | string | URL to view in browser | -| ↳ `webContentLink` | string | Direct download URL | -| ↳ `iconLink` | string | URL to file icon | -| ↳ `thumbnailLink` | string | URL to thumbnail | -| ↳ `exportLinks` | json | Export format links | -| ↳ `size` | string | File size in bytes | -| ↳ `quotaBytesUsed` | string | Storage quota used | -| ↳ `md5Checksum` | string | MD5 hash | -| ↳ `sha1Checksum` | string | SHA-1 hash | -| ↳ `sha256Checksum` | string | SHA-256 hash | -| ↳ `parents` | json | Parent folder IDs | -| ↳ `spaces` | json | Spaces containing file | -| ↳ `driveId` | string | Shared drive ID | -| ↳ `capabilities` | json | User capabilities on file | -| ↳ `version` | string | Version number | -| ↳ `headRevisionId` | string | Head revision ID | -| ↳ `hasThumbnail` | boolean | Whether has thumbnail | -| ↳ `thumbnailVersion` | string | Thumbnail version | -| ↳ `imageMediaMetadata` | json | Image-specific metadata | -| ↳ `videoMediaMetadata` | json | Video-specific metadata | -| ↳ `isAppAuthorized` | boolean | Whether created by requesting app | -| ↳ `contentRestrictions` | json | Content restrictions | -| ↳ `linkShareMetadata` | json | Link share metadata | -| `nextPageToken` | string | Token for fetching the next page of results | +| `file` | json | The copied file metadata | +| ↳ `id` | string | Google Drive file ID of the copy | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `createdTime` | string | File creation time | +| ↳ `modifiedTime` | string | Last modification time | +| ↳ `owners` | json | List of file owners | +| ↳ `size` | string | File size in bytes | + +### `google_drive_update` + +Update file metadata in Google Drive (rename, move, star, add description) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to update | +| `name` | string | No | New name for the file | +| `description` | string | No | New description for the file | +| `addParents` | string | No | Comma-separated list of parent folder IDs to add \(moves file to these folders\) | +| `removeParents` | string | No | Comma-separated list of parent folder IDs to remove | +| `starred` | boolean | No | Whether to star or unstar the file | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The updated file metadata | +| ↳ `id` | string | Google Drive file ID | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `description` | string | File description | +| ↳ `starred` | boolean | Whether file is starred | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `parents` | json | Parent folder IDs | +| ↳ `modifiedTime` | string | Last modification time | + +### `google_drive_trash` + +Move a file to the trash in Google Drive (can be restored later) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to move to trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The trashed file metadata | +| ↳ `id` | string | Google Drive file ID | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `trashed` | boolean | Whether file is in trash \(should be true\) | +| ↳ `trashedTime` | string | When file was trashed | +| ↳ `webViewLink` | string | URL to view in browser | + +### `google_drive_untrash` + +Restore a file from the trash in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to restore from trash | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | json | The restored file metadata | +| ↳ `id` | string | Google Drive file ID | +| ↳ `name` | string | File name | +| ↳ `mimeType` | string | MIME type | +| ↳ `trashed` | boolean | Whether file is in trash \(should be false\) | +| ↳ `webViewLink` | string | URL to view in browser | +| ↳ `parents` | json | Parent folder IDs | + +### `google_drive_delete` + +Permanently delete a file from Google Drive (bypasses trash) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to permanently delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the file was successfully deleted | +| `fileId` | string | The ID of the deleted file | + +### `google_drive_share` + +Share a file with a user, group, domain, or make it public + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to share | +| `type` | string | Yes | Type of grantee: user, group, domain, or anyone | +| `role` | string | Yes | Permission role: owner \(transfer ownership\), organizer \(shared drive only\), fileOrganizer \(shared drive only\), writer \(edit\), commenter \(view and comment\), reader \(view only\) | +| `email` | string | No | Email address of the user or group \(required for type=user or type=group\) | +| `domain` | string | No | Domain to share with \(required for type=domain\) | +| `transferOwnership` | boolean | No | Required when role is owner. Transfers ownership to the specified user. | +| `moveToNewOwnersRoot` | boolean | No | When transferring ownership, move the file to the new owner's My Drive root folder. | +| `sendNotification` | boolean | No | Whether to send an email notification \(default: true\) | +| `emailMessage` | string | No | Custom message to include in the notification email | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `permission` | json | The created permission details | +| ↳ `id` | string | Permission ID | +| ↳ `type` | string | Grantee type \(user, group, domain, anyone\) | +| ↳ `role` | string | Permission role | +| ↳ `emailAddress` | string | Email of the grantee | +| ↳ `displayName` | string | Display name of the grantee | +| ↳ `domain` | string | Domain of the grantee | +| ↳ `expirationTime` | string | Expiration time | +| ↳ `deleted` | boolean | Whether grantee is deleted | + +### `google_drive_unshare` + +Remove a permission from a file (revoke access) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to modify permissions on | +| `permissionId` | string | Yes | The ID of the permission to remove \(use list_permissions to find this\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `removed` | boolean | Whether the permission was successfully removed | +| `fileId` | string | The ID of the file | +| `permissionId` | string | The ID of the removed permission | + +### `google_drive_list_permissions` + +List all permissions (who has access) for a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to list permissions for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `permissions` | array | List of permissions on the file | +| ↳ `id` | string | Permission ID \(use to remove permission\) | +| ↳ `type` | string | Grantee type \(user, group, domain, anyone\) | +| ↳ `role` | string | Permission role \(owner, organizer, fileOrganizer, writer, commenter, reader\) | +| ↳ `emailAddress` | string | Email of the grantee | +| ↳ `displayName` | string | Display name of the grantee | +| ↳ `photoLink` | string | Photo URL of the grantee | +| ↳ `domain` | string | Domain of the grantee | +| ↳ `expirationTime` | string | When permission expires | +| ↳ `deleted` | boolean | Whether grantee account is deleted | +| ↳ `allowFileDiscovery` | boolean | Whether file is discoverable by grantee | +| ↳ `pendingOwner` | boolean | Whether ownership transfer is pending | +| ↳ `permissionDetails` | json | Details about inherited permissions | + +### `google_drive_get_about` + +Get information about the user and their Google Drive (storage quota, capabilities) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | json | Information about the authenticated user | +| ↳ `displayName` | string | User display name | +| ↳ `emailAddress` | string | User email address | +| ↳ `photoLink` | string | URL to user profile photo | +| ↳ `permissionId` | string | User permission ID | +| ↳ `me` | boolean | Whether this is the authenticated user | +| `storageQuota` | json | Storage quota information in bytes | +| ↳ `limit` | string | Total storage limit in bytes \(null for unlimited\) | +| ↳ `usage` | string | Total storage used in bytes | +| ↳ `usageInDrive` | string | Storage used by Drive files in bytes | +| ↳ `usageInDriveTrash` | string | Storage used by trashed files in bytes | +| `canCreateDrives` | boolean | Whether user can create shared drives | +| `importFormats` | json | Map of MIME types that can be imported and their target formats | +| `exportFormats` | json | Map of Google Workspace MIME types and their exportable formats | +| `maxUploadSize` | string | Maximum upload size in bytes | diff --git a/apps/docs/content/docs/en/tools/google_forms.mdx b/apps/docs/content/docs/en/tools/google_forms.mdx index 52ee3053ab..8e05a62dd5 100644 --- a/apps/docs/content/docs/en/tools/google_forms.mdx +++ b/apps/docs/content/docs/en/tools/google_forms.mdx @@ -1,6 +1,6 @@ --- title: Google Forms -description: Read responses from a Google Form +description: Manage Google Forms and responses --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -29,7 +29,7 @@ In Sim, the Google Forms integration enables your agents to programmatically acc ## Usage Instructions -Integrate Google Forms into your workflow. Provide a Form ID to list responses, or specify a Response ID to fetch a single response. Requires OAuth. +Integrate Google Forms into your workflow. Read form structure, get responses, create forms, update content, and manage notification watches. @@ -46,6 +46,164 @@ Integrate Google Forms into your workflow. Provide a Form ID to list responses, | Parameter | Type | Description | | --------- | ---- | ----------- | -| `data` | json | Response or list of responses | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_get_form` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_create_form` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_batch_update` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_set_publish_settings` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_create_watch` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_list_watches` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_delete_watch` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | + +### `google_forms_renew_watch` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `response` | json | Operation response data | +| `formId` | string | Form ID | +| `title` | string | Form title | +| `responderUri` | string | Form responder URL | +| `items` | json | Form items | +| `responses` | json | Form responses | +| `watches` | json | Form watches | diff --git a/apps/docs/content/docs/en/tools/google_groups.mdx b/apps/docs/content/docs/en/tools/google_groups.mdx index 793bc90134..f967ebf270 100644 --- a/apps/docs/content/docs/en/tools/google_groups.mdx +++ b/apps/docs/content/docs/en/tools/google_groups.mdx @@ -215,4 +215,191 @@ Check if a user is a member of a Google Group | --------- | ---- | ----------- | | `isMember` | boolean | Whether the user is a member of the group | +### `google_groups_list_aliases` + +List all email aliases for a Google Group + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupKey` | string | Yes | Group email address or unique group ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `aliases` | array | List of email aliases for the group | +| ↳ `id` | string | Unique group identifier | +| ↳ `primaryEmail` | string | Group | +| ↳ `alias` | string | Alias email address | +| ↳ `kind` | string | API resource type | +| ↳ `etag` | string | Resource version identifier | + +### `google_groups_add_alias` + +Add an email alias to a Google Group + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupKey` | string | Yes | Group email address or unique group ID | +| `alias` | string | Yes | The email alias to add to the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique group identifier | +| `primaryEmail` | string | Group | +| `alias` | string | The alias that was added | +| `kind` | string | API resource type | +| `etag` | string | Resource version identifier | + +### `google_groups_remove_alias` + +Remove an email alias from a Google Group + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupKey` | string | Yes | Group email address or unique group ID | +| `alias` | string | Yes | The email alias to remove from the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the alias was successfully deleted | + +### `google_groups_get_settings` + +Get the settings for a Google Group including access permissions, moderation, and posting options + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupEmail` | string | Yes | The email address of the group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The group | +| `name` | string | The group name \(max 75 characters\) | +| `description` | string | The group description \(max 4096 characters\) | +| `whoCanJoin` | string | Who can join the group \(ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN\) | +| `whoCanViewMembership` | string | Who can view group membership | +| `whoCanViewGroup` | string | Who can view group messages | +| `whoCanPostMessage` | string | Who can post messages to the group | +| `allowExternalMembers` | string | Whether external users can be members | +| `allowWebPosting` | string | Whether web posting is allowed | +| `primaryLanguage` | string | The group | +| `isArchived` | string | Whether messages are archived | +| `archiveOnly` | string | Whether the group is archive-only \(inactive\) | +| `messageModerationLevel` | string | Message moderation level | +| `spamModerationLevel` | string | Spam handling level \(ALLOW, MODERATE, SILENTLY_MODERATE, REJECT\) | +| `replyTo` | string | Default reply destination | +| `customReplyTo` | string | Custom email for replies | +| `includeCustomFooter` | string | Whether to include custom footer | +| `customFooterText` | string | Custom footer text \(max 1000 characters\) | +| `sendMessageDenyNotification` | string | Whether to send rejection notifications | +| `defaultMessageDenyNotificationText` | string | Default rejection message text | +| `membersCanPostAsTheGroup` | string | Whether members can post as the group | +| `includeInGlobalAddressList` | string | Whether included in Global Address List | +| `whoCanLeaveGroup` | string | Who can leave the group | +| `whoCanContactOwner` | string | Who can contact the group owner | +| `favoriteRepliesOnTop` | string | Whether favorite replies appear at top | +| `whoCanApproveMembers` | string | Who can approve new members | +| `whoCanBanUsers` | string | Who can ban users | +| `whoCanModerateMembers` | string | Who can manage members | +| `whoCanModerateContent` | string | Who can moderate content | +| `whoCanAssistContent` | string | Who can assist with content metadata | +| `enableCollaborativeInbox` | string | Whether collaborative inbox is enabled | +| `whoCanDiscoverGroup` | string | Who can discover the group | +| `defaultSender` | string | Default sender identity \(DEFAULT_SELF or GROUP\) | + +### `google_groups_update_settings` + +Update the settings for a Google Group including access permissions, moderation, and posting options + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupEmail` | string | Yes | The email address of the group | +| `name` | string | No | The group name \(max 75 characters\) | +| `description` | string | No | The group description \(max 4096 characters\) | +| `whoCanJoin` | string | No | Who can join: ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN | +| `whoCanViewMembership` | string | No | Who can view membership: ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW | +| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW | +| `whoCanPostMessage` | string | No | Who can post: NONE_CAN_POST, ALL_MANAGERS_CAN_POST, ALL_MEMBERS_CAN_POST, ALL_OWNERS_CAN_POST, ALL_IN_DOMAIN_CAN_POST, ANYONE_CAN_POST | +| `allowExternalMembers` | string | No | Whether external users can be members: true or false | +| `allowWebPosting` | string | No | Whether web posting is allowed: true or false | +| `primaryLanguage` | string | No | The group's primary language \(e.g., en\) | +| `isArchived` | string | No | Whether messages are archived: true or false | +| `archiveOnly` | string | No | Whether the group is archive-only \(inactive\): true or false | +| `messageModerationLevel` | string | No | Message moderation: MODERATE_ALL_MESSAGES, MODERATE_NON_MEMBERS, MODERATE_NEW_MEMBERS, MODERATE_NONE | +| `spamModerationLevel` | string | No | Spam handling: ALLOW, MODERATE, SILENTLY_MODERATE, REJECT | +| `replyTo` | string | No | Default reply: REPLY_TO_CUSTOM, REPLY_TO_SENDER, REPLY_TO_LIST, REPLY_TO_OWNER, REPLY_TO_IGNORE, REPLY_TO_MANAGERS | +| `customReplyTo` | string | No | Custom email for replies \(when replyTo is REPLY_TO_CUSTOM\) | +| `includeCustomFooter` | string | No | Whether to include custom footer: true or false | +| `customFooterText` | string | No | Custom footer text \(max 1000 characters\) | +| `sendMessageDenyNotification` | string | No | Whether to send rejection notifications: true or false | +| `defaultMessageDenyNotificationText` | string | No | Default rejection message text | +| `membersCanPostAsTheGroup` | string | No | Whether members can post as the group: true or false | +| `includeInGlobalAddressList` | string | No | Whether included in Global Address List: true or false | +| `whoCanLeaveGroup` | string | No | Who can leave: ALL_MANAGERS_CAN_LEAVE, ALL_MEMBERS_CAN_LEAVE, NONE_CAN_LEAVE | +| `whoCanContactOwner` | string | No | Who can contact owner: ALL_IN_DOMAIN_CAN_CONTACT, ALL_MANAGERS_CAN_CONTACT, ALL_MEMBERS_CAN_CONTACT, ANYONE_CAN_CONTACT | +| `favoriteRepliesOnTop` | string | No | Whether favorite replies appear at top: true or false | +| `whoCanApproveMembers` | string | No | Who can approve members: ALL_OWNERS_CAN_APPROVE, ALL_MANAGERS_CAN_APPROVE, ALL_MEMBERS_CAN_APPROVE, NONE_CAN_APPROVE | +| `whoCanBanUsers` | string | No | Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE | +| `whoCanModerateMembers` | string | No | Who can manage members: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | +| `whoCanModerateContent` | string | No | Who can moderate content: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | +| `whoCanAssistContent` | string | No | Who can assist with content metadata: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE | +| `enableCollaborativeInbox` | string | No | Whether collaborative inbox is enabled: true or false | +| `whoCanDiscoverGroup` | string | No | Who can discover: ANYONE_CAN_DISCOVER, ALL_IN_DOMAIN_CAN_DISCOVER, ALL_MEMBERS_CAN_DISCOVER | +| `defaultSender` | string | No | Default sender: DEFAULT_SELF or GROUP | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The group | +| `name` | string | The group name | +| `description` | string | The group description | +| `whoCanJoin` | string | Who can join the group | +| `whoCanViewMembership` | string | Who can view group membership | +| `whoCanViewGroup` | string | Who can view group messages | +| `whoCanPostMessage` | string | Who can post messages to the group | +| `allowExternalMembers` | string | Whether external users can be members | +| `allowWebPosting` | string | Whether web posting is allowed | +| `primaryLanguage` | string | The group | +| `isArchived` | string | Whether messages are archived | +| `archiveOnly` | string | Whether the group is archive-only | +| `messageModerationLevel` | string | Message moderation level | +| `spamModerationLevel` | string | Spam handling level | +| `replyTo` | string | Default reply destination | +| `customReplyTo` | string | Custom email for replies | +| `includeCustomFooter` | string | Whether to include custom footer | +| `customFooterText` | string | Custom footer text | +| `sendMessageDenyNotification` | string | Whether to send rejection notifications | +| `defaultMessageDenyNotificationText` | string | Default rejection message text | +| `membersCanPostAsTheGroup` | string | Whether members can post as the group | +| `includeInGlobalAddressList` | string | Whether included in Global Address List | +| `whoCanLeaveGroup` | string | Who can leave the group | +| `whoCanContactOwner` | string | Who can contact the group owner | +| `favoriteRepliesOnTop` | string | Whether favorite replies appear at top | +| `whoCanApproveMembers` | string | Who can approve new members | +| `whoCanBanUsers` | string | Who can ban users | +| `whoCanModerateMembers` | string | Who can manage members | +| `whoCanModerateContent` | string | Who can moderate content | +| `whoCanAssistContent` | string | Who can assist with content metadata | +| `enableCollaborativeInbox` | string | Whether collaborative inbox is enabled | +| `whoCanDiscoverGroup` | string | Who can discover the group | +| `defaultSender` | string | Default sender identity | + diff --git a/apps/docs/content/docs/en/tools/google_sheets.mdx b/apps/docs/content/docs/en/tools/google_sheets.mdx index b48647ce85..73ae75fa09 100644 --- a/apps/docs/content/docs/en/tools/google_sheets.mdx +++ b/apps/docs/content/docs/en/tools/google_sheets.mdx @@ -28,7 +28,7 @@ In Sim, the Google Sheets integration empowers your agents to automate reading f ## Usage Instructions -Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, and update data in specific sheets. +Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, update, clear data, create spreadsheets, get spreadsheet info, and copy sheets. @@ -42,9 +42,8 @@ Read data from a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `spreadsheetId` | string | Yes | The ID of the spreadsheet | -| `sheetName` | string | Yes | The name of the sheet/tab to read from | -| `cellRange` | string | No | The cell range to read \(e.g. "A1:D10"\). Defaults to "A1:Z1000" if not specified. | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet \(found in the URL: docs.google.com/spreadsheets/d/\{SPREADSHEET_ID\}/edit\). | +| `range` | string | No | The A1 notation range to read \(e.g. "Sheet1!A1:D10", "A1:B5"\). Defaults to first sheet A1:Z1000 if not specified. | #### Output @@ -66,8 +65,7 @@ Write data to a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet | -| `sheetName` | string | Yes | The name of the sheet/tab to write to | -| `cellRange` | string | No | The cell range to write to \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | +| `range` | string | No | The A1 notation range to write to \(e.g. "Sheet1!A1:D10", "A1:B5"\) | | `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to write | | `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | @@ -93,8 +91,7 @@ Update data in a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to update | -| `sheetName` | string | Yes | The name of the sheet/tab to update | -| `cellRange` | string | No | The cell range to update \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. | +| `range` | string | No | The A1 notation range to update \(e.g. "Sheet1!A1:D10", "A1:B5"\) | | `values` | array | Yes | The data to update as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to update | | `includeValuesInResponse` | boolean | No | Whether to include the updated values in the response | @@ -120,7 +117,7 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to | -| `sheetName` | string | Yes | The name of the sheet/tab to append to | +| `range` | string | No | The A1 notation range to append after \(e.g. "Sheet1", "Sheet1!A:D"\) | | `values` | array | Yes | The data to append as a 2D array \(e.g. \[\["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | | `valueInputOption` | string | No | The format of the data to append | | `insertDataOption` | string | No | How to insert the data \(OVERWRITE or INSERT_ROWS\) | @@ -139,4 +136,180 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet | ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | | ↳ `spreadsheetUrl` | string | Spreadsheet URL | +### `google_sheets_clear` + +Clear values from a specific range in a Google Sheets spreadsheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `sheetName` | string | Yes | The name of the sheet/tab to clear | +| `cellRange` | string | No | The cell range to clear \(e.g. "A1:D10"\). Clears entire sheet if not specified. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `clearedRange` | string | The range that was cleared | +| `sheetName` | string | Name of the sheet that was cleared | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_get_spreadsheet` + +Get metadata about a Google Sheets spreadsheet including title and sheet list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `includeGridData` | boolean | No | Whether to include grid data \(cell values\). Defaults to false. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The spreadsheet ID | +| `title` | string | The title of the spreadsheet | +| `locale` | string | The locale of the spreadsheet | +| `timeZone` | string | The time zone of the spreadsheet | +| `spreadsheetUrl` | string | URL to the spreadsheet | +| `sheets` | array | List of sheets in the spreadsheet | +| ↳ `sheetId` | number | The sheet ID | +| ↳ `title` | string | The sheet title/name | +| ↳ `index` | number | The sheet index \(position\) | +| ↳ `rowCount` | number | Number of rows in the sheet | +| ↳ `columnCount` | number | Number of columns in the sheet | +| ↳ `hidden` | boolean | Whether the sheet is hidden | + +### `google_sheets_create_spreadsheet` + +Create a new Google Sheets spreadsheet + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `title` | string | Yes | The title of the new spreadsheet | +| `sheetTitles` | json | No | Array of sheet names to create \(e.g., \["Sheet1", "Data", "Summary"\]\). Defaults to a single "Sheet1". | +| `locale` | string | No | The locale of the spreadsheet \(e.g., "en_US"\) | +| `timeZone` | string | No | The time zone of the spreadsheet \(e.g., "America/New_York"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The ID of the created spreadsheet | +| `title` | string | The title of the created spreadsheet | +| `spreadsheetUrl` | string | URL to the created spreadsheet | +| `sheets` | array | List of sheets created in the spreadsheet | +| ↳ `sheetId` | number | The sheet ID | +| ↳ `title` | string | The sheet title/name | +| ↳ `index` | number | The sheet index \(position\) | + +### `google_sheets_batch_get` + +Read multiple ranges from a Google Sheets spreadsheet in a single request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `ranges` | json | Yes | Array of ranges to read \(e.g., \["Sheet1!A1:D10", "Sheet2!A1:B5"\]\). Each range should include sheet name. | +| `majorDimension` | string | No | The major dimension of values: "ROWS" \(default\) or "COLUMNS" | +| `valueRenderOption` | string | No | How values should be rendered: "FORMATTED_VALUE" \(default\), "UNFORMATTED_VALUE", or "FORMULA" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The spreadsheet ID | +| `valueRanges` | array | Array of value ranges read from the spreadsheet | +| ↳ `range` | string | The range that was read | +| ↳ `majorDimension` | string | Major dimension \(ROWS or COLUMNS\) | +| ↳ `values` | array | The cell values as a 2D array | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_batch_update` + +Update multiple ranges in a Google Sheets spreadsheet in a single request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `data` | json | Yes | Array of value ranges to update. Each item should have "range" \(e.g., "Sheet1!A1:D10"\) and "values" \(2D array\). | +| `valueInputOption` | string | No | How input data should be interpreted: "RAW" or "USER_ENTERED" \(default\). USER_ENTERED parses formulas. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The spreadsheet ID | +| `totalUpdatedRows` | number | Total number of rows updated | +| `totalUpdatedColumns` | number | Total number of columns updated | +| `totalUpdatedCells` | number | Total number of cells updated | +| `totalUpdatedSheets` | number | Total number of sheets updated | +| `responses` | array | Array of update responses for each range | +| ↳ `spreadsheetId` | string | The spreadsheet ID | +| ↳ `updatedRange` | string | The range that was updated | +| ↳ `updatedRows` | number | Number of rows updated in this range | +| ↳ `updatedColumns` | number | Number of columns updated in this range | +| ↳ `updatedCells` | number | Number of cells updated in this range | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_batch_clear` + +Clear multiple ranges in a Google Sheets spreadsheet in a single request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `spreadsheetId` | string | Yes | The ID of the spreadsheet | +| `ranges` | json | Yes | Array of ranges to clear \(e.g., \["Sheet1!A1:D10", "Sheet2!A1:B5"\]\). Each range should include sheet name. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `spreadsheetId` | string | The spreadsheet ID | +| `clearedRanges` | array | Array of ranges that were cleared | +| `metadata` | json | Spreadsheet metadata including ID and URL | +| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID | +| ↳ `spreadsheetUrl` | string | Spreadsheet URL | + +### `google_sheets_copy_sheet` + +Copy a sheet from one spreadsheet to another + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `sourceSpreadsheetId` | string | Yes | The ID of the source spreadsheet | +| `sheetId` | number | Yes | The ID of the sheet to copy \(numeric ID, not the sheet name\). Use Get Spreadsheet to find sheet IDs. | +| `destinationSpreadsheetId` | string | Yes | The ID of the destination spreadsheet where the sheet will be copied | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `sheetId` | number | The ID of the newly created sheet in the destination | +| `title` | string | The title of the copied sheet | +| `index` | number | The index \(position\) of the copied sheet | +| `sheetType` | string | The type of the sheet \(GRID, CHART, etc.\) | +| `destinationSpreadsheetId` | string | The ID of the destination spreadsheet | +| `destinationSpreadsheetUrl` | string | URL to the destination spreadsheet | + diff --git a/apps/docs/content/docs/en/tools/google_slides.mdx b/apps/docs/content/docs/en/tools/google_slides.mdx index b8a290b01b..20df0ba2a5 100644 --- a/apps/docs/content/docs/en/tools/google_slides.mdx +++ b/apps/docs/content/docs/en/tools/google_slides.mdx @@ -30,7 +30,7 @@ In Sim, the Google Slides integration enables your agents to interact directly w ## Usage Instructions -Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails. +Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text. @@ -177,4 +177,157 @@ Generate a thumbnail image of a specific slide in a Google Slides presentation | `height` | number | Height of the thumbnail in pixels | | `metadata` | json | Operation metadata including presentation ID and page object ID | +### `google_slides_get_page` + +Get detailed information about a specific slide/page in a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `pageObjectId` | string | Yes | The object ID of the slide/page to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `objectId` | string | The object ID of the page | +| `pageType` | string | The type of page \(SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER\) | +| `pageElements` | json | Array of page elements \(shapes, images, tables, etc.\) on this page | +| `slideProperties` | json | Properties specific to slides \(layout, master, notes\) | +| `metadata` | json | Operation metadata including presentation ID and URL | + +### `google_slides_delete_object` + +Delete a page element (shape, image, table, etc.) or an entire slide from a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `objectId` | string | Yes | The object ID of the element or slide to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the object was successfully deleted | +| `objectId` | string | The object ID that was deleted | +| `metadata` | json | Operation metadata including presentation ID and URL | + +### `google_slides_duplicate_object` + +Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `objectId` | string | Yes | The object ID of the element or slide to duplicate | +| `objectIds` | string | No | Optional JSON object mapping source object IDs \(within the slide being duplicated\) to new object IDs for the duplicates. Format: \{"sourceId1":"newId1","sourceId2":"newId2"\} | +| `Format` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `duplicatedObjectId` | string | The object ID of the newly created duplicate | +| `metadata` | json | Operation metadata including presentation ID and source object ID | + +### `google_slides_update_slides_position` + +Move one or more slides to a new position in a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `slideObjectIds` | string | Yes | Comma-separated list of slide object IDs to move. The slides will maintain their relative order. | +| `insertionIndex` | number | Yes | The zero-based index where the slides should be moved. All slides with indices greater than or equal to this will be shifted right. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `moved` | boolean | Whether the slides were successfully moved | +| `slideObjectIds` | array | The slide object IDs that were moved | +| `insertionIndex` | number | The index where the slides were moved to | +| `metadata` | json | Operation metadata including presentation ID and URL | + +### `google_slides_create_table` + +Create a new table on a slide in a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `pageObjectId` | string | Yes | The object ID of the slide/page to add the table to | +| `rows` | number | Yes | Number of rows in the table \(minimum 1\) | +| `columns` | number | Yes | Number of columns in the table \(minimum 1\) | +| `width` | number | No | Width of the table in points \(default: 400\) | +| `height` | number | No | Height of the table in points \(default: 200\) | +| `positionX` | number | No | X position from the left edge in points \(default: 100\) | +| `positionY` | number | No | Y position from the top edge in points \(default: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tableId` | string | The object ID of the newly created table | +| `rows` | number | Number of rows in the table | +| `columns` | number | Number of columns in the table | +| `metadata` | json | Operation metadata including presentation ID and page object ID | + +### `google_slides_create_shape` + +Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides presentation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `pageObjectId` | string | Yes | The object ID of the slide/page to add the shape to | +| `shapeType` | string | Yes | The type of shape to create. Common types: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, DIAMOND, STAR_5, ARROW_EAST, HEART, CLOUD | +| `width` | number | No | Width of the shape in points \(default: 200\) | +| `height` | number | No | Height of the shape in points \(default: 100\) | +| `positionX` | number | No | X position from the left edge in points \(default: 100\) | +| `positionY` | number | No | Y position from the top edge in points \(default: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `shapeId` | string | The object ID of the newly created shape | +| `shapeType` | string | The type of shape that was created | +| `metadata` | json | Operation metadata including presentation ID and page object ID | + +### `google_slides_insert_text` + +Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or table cells. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `presentationId` | string | Yes | The ID of the presentation | +| `objectId` | string | Yes | The object ID of the shape or table cell to insert text into. For table cells, use the cell object ID. | +| `text` | string | Yes | The text to insert | +| `insertionIndex` | number | No | The zero-based index at which to insert the text. If not specified, text is inserted at the beginning \(index 0\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inserted` | boolean | Whether the text was successfully inserted | +| `objectId` | string | The object ID where text was inserted | +| `text` | string | The text that was inserted | +| `metadata` | json | Operation metadata including presentation ID and URL | + diff --git a/apps/docs/content/docs/en/tools/knowledge.mdx b/apps/docs/content/docs/en/tools/knowledge.mdx index 21405f8150..44f5db36a7 100644 --- a/apps/docs/content/docs/en/tools/knowledge.mdx +++ b/apps/docs/content/docs/en/tools/knowledge.mdx @@ -51,6 +51,7 @@ Search for similar content in a knowledge base using vector similarity | `properties` | string | No | No description | | `tagName` | string | No | No description | | `tagValue` | string | No | No description | +| `tagFilters` | string | No | No description | #### Output @@ -108,19 +109,8 @@ Create a new document in a knowledge base | `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document | | `name` | string | Yes | Name of the document | | `content` | string | Yes | Content of the document | -| `tag1` | string | No | Tag 1 value for the document | -| `tag2` | string | No | Tag 2 value for the document | -| `tag3` | string | No | Tag 3 value for the document | -| `tag4` | string | No | Tag 4 value for the document | -| `tag5` | string | No | Tag 5 value for the document | -| `tag6` | string | No | Tag 6 value for the document | -| `tag7` | string | No | Tag 7 value for the document | -| `documentTagsData` | array | No | Structured tag data with names, types, and values | -| `items` | object | No | No description | -| `properties` | string | No | No description | -| `tagName` | string | No | No description | -| `tagValue` | string | No | No description | -| `tagType` | string | No | No description | +| `documentTags` | object | No | Document tags | +| `documentTags` | string | No | No description | #### Output diff --git a/apps/docs/content/docs/en/tools/microsoft_excel.mdx b/apps/docs/content/docs/en/tools/microsoft_excel.mdx index d981eabc9e..65a8eaa5dc 100644 --- a/apps/docs/content/docs/en/tools/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_excel.mdx @@ -45,8 +45,7 @@ Read data from a specific sheet in a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from | -| `sheetName` | string | Yes | The name of the sheet/tab to read from | -| `cellRange` | string | No | The cell range to read \(e.g., "A1:D10"\). If not specified, reads the entire used range. | +| `range` | string | No | The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet. | #### Output @@ -68,9 +67,8 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to | -| `sheetName` | string | Yes | The name of the sheet/tab to write to | -| `cellRange` | string | No | The cell range to write to \(e.g., "A1:D10", "A1"\). Defaults to "A1" if not specified. | -| `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. | +| `range` | string | No | The range of cells to write to | +| `values` | array | Yes | The data to write to the spreadsheet | | `valueInputOption` | string | No | The format of the data to write | | `includeValuesInResponse` | boolean | No | Whether to include the written values in the response | diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 4462adba61..6ae9192b8e 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -84,9 +84,10 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `authMethod` | string | No | Authentication method: oauth or bot_token | +| `destinationType` | string | No | Destination type: channel or dm | | `botToken` | string | No | Bot token for Custom Bot | | `channel` | string | No | Target Slack channel \(e.g., #general\) | -| `userId` | string | No | Target Slack user ID for direct messages \(e.g., U1234567890\) | +| `dmUserId` | string | No | Target Slack user for direct messages | | `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) | | `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) | | `files` | file[] | No | Files to attach to the message | @@ -132,9 +133,10 @@ Read the latest messages from Slack channels. Retrieve conversation history with | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `authMethod` | string | No | Authentication method: oauth or bot_token | +| `destinationType` | string | No | Destination type: channel or dm | | `botToken` | string | No | Bot token for Custom Bot | | `channel` | string | No | Slack channel to read messages from \(e.g., #general\) | -| `userId` | string | No | User ID for DM conversation \(e.g., U1234567890\) | +| `dmUserId` | string | No | Target Slack user for DM conversation | | `limit` | number | No | Number of messages to retrieve \(default: 10, max: 15\) | | `oldest` | string | No | Start of time range \(timestamp\) | | `latest` | string | No | End of time range \(timestamp\) | diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 9b44dcdf01..4a2ae20a9d 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -84,6 +84,44 @@ export const GitHubBlock: BlockConfig = { { label: 'Create project', id: 'github_create_project' }, { label: 'Update project', id: 'github_update_project' }, { label: 'Delete project', id: 'github_delete_project' }, + // Search Operations + { label: 'Search code', id: 'github_search_code' }, + { label: 'Search commits', id: 'github_search_commits' }, + { label: 'Search issues', id: 'github_search_issues' }, + { label: 'Search repositories', id: 'github_search_repos' }, + { label: 'Search users', id: 'github_search_users' }, + // Commit Operations + { label: 'List commits', id: 'github_list_commits' }, + { label: 'Get commit', id: 'github_get_commit' }, + { label: 'Compare commits', id: 'github_compare_commits' }, + // Gist Operations + { label: 'Create gist', id: 'github_create_gist' }, + { label: 'Get gist', id: 'github_get_gist' }, + { label: 'List gists', id: 'github_list_gists' }, + { label: 'Update gist', id: 'github_update_gist' }, + { label: 'Delete gist', id: 'github_delete_gist' }, + { label: 'Fork gist', id: 'github_fork_gist' }, + { label: 'Star gist', id: 'github_star_gist' }, + { label: 'Unstar gist', id: 'github_unstar_gist' }, + // Fork Operations + { label: 'Fork repository', id: 'github_fork_repo' }, + { label: 'List forks', id: 'github_list_forks' }, + // Milestone Operations + { label: 'Create milestone', id: 'github_create_milestone' }, + { label: 'Get milestone', id: 'github_get_milestone' }, + { label: 'List milestones', id: 'github_list_milestones' }, + { label: 'Update milestone', id: 'github_update_milestone' }, + { label: 'Delete milestone', id: 'github_delete_milestone' }, + // Reaction Operations + { label: 'Add issue reaction', id: 'github_create_issue_reaction' }, + { label: 'Remove issue reaction', id: 'github_delete_issue_reaction' }, + { label: 'Add comment reaction', id: 'github_create_comment_reaction' }, + { label: 'Remove comment reaction', id: 'github_delete_comment_reaction' }, + // Star Operations + { label: 'Star repository', id: 'github_star_repo' }, + { label: 'Unstar repository', id: 'github_unstar_repo' }, + { label: 'Check if starred', id: 'github_check_star' }, + { label: 'List stargazers', id: 'github_list_stargazers' }, ], value: () => 'github_pr', }, @@ -998,6 +1036,440 @@ export const GitHubBlock: BlockConfig = { required: true, condition: { field: 'operation', value: 'github_delete_project' }, }, + // Search operations parameters + { + id: 'q', + title: 'Search Query', + type: 'short-input', + placeholder: 'e.g., react language:typescript', + required: true, + condition: { + field: 'operation', + value: [ + 'github_search_code', + 'github_search_commits', + 'github_search_issues', + 'github_search_repos', + 'github_search_users', + ], + }, + wandConfig: { + enabled: true, + prompt: `Generate a GitHub search query based on the user's description. +GitHub search supports these qualifiers: +- For repos: language:python, stars:>1000, forks:>100, topic:react, user:owner, org:name, created:>2023-01-01 +- For code: repo:owner/name, path:src, extension:ts, language:javascript +- For issues/PRs: is:issue, is:pr, is:open, is:closed, label:bug, author:user, assignee:user +- For commits: repo:owner/name, author:user, committer:user, author-date:>2023-01-01 +- For users: type:user, type:org, followers:>100, repos:>10, location:city + +Examples: +- "Python repos with more than 1000 stars" -> language:python stars:>1000 +- "Open bugs in facebook/react" -> repo:facebook/react is:issue is:open label:bug +- "TypeScript files in src folder" -> language:typescript path:src + +Return ONLY the search query - no explanations.`, + placeholder: 'Describe what you want to search for...', + }, + }, + { + id: 'sort', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Best match', id: '' }, + { label: 'Stars', id: 'stars' }, + { label: 'Forks', id: 'forks' }, + { label: 'Updated', id: 'updated' }, + ], + condition: { field: 'operation', value: 'github_search_repos' }, + }, + { + id: 'order', + title: 'Order', + type: 'dropdown', + options: [ + { label: 'Descending', id: 'desc' }, + { label: 'Ascending', id: 'asc' }, + ], + condition: { + field: 'operation', + value: [ + 'github_search_code', + 'github_search_commits', + 'github_search_issues', + 'github_search_repos', + 'github_search_users', + ], + }, + }, + // Commit operations parameters + { + id: 'sha', + title: 'SHA or Branch', + type: 'short-input', + placeholder: 'e.g., main or abc123', + condition: { field: 'operation', value: 'github_list_commits' }, + }, + { + id: 'author', + title: 'Author Filter', + type: 'short-input', + placeholder: 'GitHub username or email', + condition: { field: 'operation', value: 'github_list_commits' }, + }, + { + id: 'since', + title: 'Since Date', + type: 'short-input', + placeholder: 'ISO 8601: 2024-01-01T00:00:00Z', + condition: { field: 'operation', value: ['github_list_commits', 'github_list_gists'] }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone). +Examples: +- "last week" -> Calculate 7 days ago at 00:00:00Z +- "yesterday" -> Calculate yesterday's date at 00:00:00Z +- "beginning of this month" -> First day of current month at 00:00:00Z +- "30 days ago" -> Calculate 30 days before current time +- "January 1st 2024" -> 2024-01-01T00:00:00Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the start date (e.g., "last week", "beginning of month")...', + generationType: 'timestamp', + }, + }, + { + id: 'until', + title: 'Until Date', + type: 'short-input', + placeholder: 'ISO 8601: 2024-12-31T23:59:59Z', + condition: { field: 'operation', value: 'github_list_commits' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone). +Examples: +- "now" -> Current timestamp +- "end of today" -> Today's date at 23:59:59Z +- "end of last week" -> Calculate end of last week +- "yesterday" -> Yesterday's date at 23:59:59Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the end date (e.g., "now", "end of yesterday")...', + generationType: 'timestamp', + }, + }, + { + id: 'ref', + title: 'Commit Reference', + type: 'short-input', + placeholder: 'SHA, branch, or tag', + required: true, + condition: { field: 'operation', value: 'github_get_commit' }, + }, + { + id: 'base', + title: 'Base Reference', + type: 'short-input', + placeholder: 'Base branch/tag/SHA', + required: true, + condition: { field: 'operation', value: 'github_compare_commits' }, + }, + { + id: 'head', + title: 'Head Reference', + type: 'short-input', + placeholder: 'Head branch/tag/SHA', + required: true, + condition: { field: 'operation', value: 'github_compare_commits' }, + }, + // Gist operations parameters + { + id: 'gist_id', + title: 'Gist ID', + type: 'short-input', + placeholder: 'e.g., aa5a315d61ae9438b18d', + required: true, + condition: { + field: 'operation', + value: [ + 'github_get_gist', + 'github_update_gist', + 'github_delete_gist', + 'github_fork_gist', + 'github_star_gist', + 'github_unstar_gist', + ], + }, + }, + { + id: 'description', + title: 'Description', + type: 'short-input', + placeholder: 'Gist description', + condition: { field: 'operation', value: ['github_create_gist', 'github_update_gist'] }, + }, + { + id: 'files', + title: 'Files (JSON)', + type: 'long-input', + placeholder: '{"file.txt": {"content": "Hello"}}', + required: true, + condition: { field: 'operation', value: 'github_create_gist' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON object for GitHub Gist files based on the user's description. +The format is: {"filename.ext": {"content": "file contents"}} + +Examples: +- "A Python hello world file" -> {"hello.py": {"content": "print('Hello, World!')"}} +- "A README with project title" -> {"README.md": {"content": "# My Project\\n\\nDescription here"}} +- "JavaScript function to add numbers" -> {"add.js": {"content": "function add(a, b) {\\n return a + b;\\n}"}} +- "Two files: index.html and style.css" -> {"index.html": {"content": "..."}, "style.css": {"content": "body { margin: 0; }"}} + +Return ONLY valid JSON - no explanations, no markdown formatting.`, + placeholder: 'Describe the files you want to create...', + generationType: 'json-object', + }, + }, + { + id: 'files', + title: 'Files (JSON)', + type: 'long-input', + placeholder: '{"file.txt": {"content": "Updated"}}', + condition: { field: 'operation', value: 'github_update_gist' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON object for updating GitHub Gist files based on the user's description. +The format is: {"filename.ext": {"content": "new contents"}} +To delete a file, set its value to null: {"old-file.txt": null} +To rename a file, set the new filename: {"old-name.txt": {"filename": "new-name.txt", "content": "..."}} + +Examples: +- "Update hello.py to print goodbye" -> {"hello.py": {"content": "print('Goodbye!')"}} +- "Delete the old readme" -> {"README.md": null} +- "Rename script.js to main.js" -> {"script.js": {"filename": "main.js"}} + +Return ONLY valid JSON - no explanations, no markdown formatting.`, + placeholder: 'Describe the file changes...', + generationType: 'json-object', + }, + }, + { + id: 'gist_public', + title: 'Public', + type: 'dropdown', + options: [ + { label: 'Secret', id: 'false' }, + { label: 'Public', id: 'true' }, + ], + condition: { field: 'operation', value: 'github_create_gist' }, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + placeholder: 'GitHub username (optional)', + condition: { field: 'operation', value: 'github_list_gists' }, + }, + // Fork operations parameters + { + id: 'organization', + title: 'Organization', + type: 'short-input', + placeholder: 'Fork to org (optional)', + condition: { field: 'operation', value: 'github_fork_repo' }, + }, + { + id: 'fork_name', + title: 'Fork Name', + type: 'short-input', + placeholder: 'Custom name (optional)', + condition: { field: 'operation', value: 'github_fork_repo' }, + }, + { + id: 'default_branch_only', + title: 'Default Branch Only', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: 'github_fork_repo' }, + }, + { + id: 'fork_sort', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Newest', id: 'newest' }, + { label: 'Oldest', id: 'oldest' }, + { label: 'Stargazers', id: 'stargazers' }, + { label: 'Watchers', id: 'watchers' }, + ], + condition: { field: 'operation', value: 'github_list_forks' }, + }, + // Milestone operations parameters + { + id: 'milestone_title', + title: 'Milestone Title', + type: 'short-input', + placeholder: 'e.g., v1.0 Release', + required: true, + condition: { field: 'operation', value: 'github_create_milestone' }, + }, + { + id: 'milestone_title', + title: 'New Title', + type: 'short-input', + placeholder: 'Updated title (optional)', + condition: { field: 'operation', value: 'github_update_milestone' }, + }, + { + id: 'milestone_description', + title: 'Description', + type: 'long-input', + placeholder: 'Milestone description', + condition: { + field: 'operation', + value: ['github_create_milestone', 'github_update_milestone'], + }, + }, + { + id: 'due_on', + title: 'Due Date', + type: 'short-input', + placeholder: 'ISO 8601: 2024-12-31T23:59:59Z', + condition: { + field: 'operation', + value: ['github_create_milestone', 'github_update_milestone'], + }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp for a milestone due date based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone). +Examples: +- "end of this month" -> Last day of current month at 23:59:59Z +- "next Friday" -> Calculate next Friday's date at 23:59:59Z +- "in 2 weeks" -> Calculate 14 days from now at 23:59:59Z +- "December 31st" -> 2024-12-31T23:59:59Z (current year) +- "Q1 2025" -> 2025-03-31T23:59:59Z (end of Q1) + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the due date (e.g., "end of month", "next Friday")...', + generationType: 'timestamp', + }, + }, + { + id: 'milestone_number', + title: 'Milestone Number', + type: 'short-input', + placeholder: 'e.g., 1', + required: true, + condition: { + field: 'operation', + value: ['github_get_milestone', 'github_update_milestone', 'github_delete_milestone'], + }, + }, + { + id: 'milestone_state', + title: 'State Filter', + type: 'dropdown', + options: [ + { label: 'Open', id: 'open' }, + { label: 'Closed', id: 'closed' }, + { label: 'All', id: 'all' }, + ], + condition: { field: 'operation', value: 'github_list_milestones' }, + }, + { + id: 'milestone_sort', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Due Date', id: 'due_on' }, + { label: 'Completeness', id: 'completeness' }, + ], + condition: { field: 'operation', value: 'github_list_milestones' }, + }, + // Reaction operations parameters + { + id: 'reaction_content', + title: 'Reaction', + type: 'dropdown', + options: [ + { label: '👍 +1', id: '+1' }, + { label: '👎 -1', id: '-1' }, + { label: '😄 Laugh', id: 'laugh' }, + { label: '😕 Confused', id: 'confused' }, + { label: '❤️ Heart', id: 'heart' }, + { label: '🎉 Hooray', id: 'hooray' }, + { label: '🚀 Rocket', id: 'rocket' }, + { label: '👀 Eyes', id: 'eyes' }, + ], + required: true, + condition: { + field: 'operation', + value: ['github_create_issue_reaction', 'github_create_comment_reaction'], + }, + }, + { + id: 'issue_number', + title: 'Issue Number', + type: 'short-input', + placeholder: 'e.g., 123', + required: true, + condition: { + field: 'operation', + value: ['github_create_issue_reaction', 'github_delete_issue_reaction'], + }, + }, + { + id: 'reaction_id', + title: 'Reaction ID', + type: 'short-input', + placeholder: 'e.g., 12345678', + required: true, + condition: { + field: 'operation', + value: ['github_delete_issue_reaction', 'github_delete_comment_reaction'], + }, + }, + { + id: 'comment_id', + title: 'Comment ID', + type: 'short-input', + placeholder: 'e.g., 987654321', + required: true, + condition: { + field: 'operation', + value: ['github_create_comment_reaction', 'github_delete_comment_reaction'], + }, + }, + // Star operations parameters - owner/repo already covered by existing subBlocks + { + id: 'per_page', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'e.g., 30 (default: 30, max: 100)', + condition: { + field: 'operation', + value: [ + 'github_search_code', + 'github_search_commits', + 'github_search_issues', + 'github_search_repos', + 'github_search_users', + 'github_list_commits', + 'github_list_gists', + 'github_list_forks', + 'github_list_milestones', + 'github_list_stargazers', + ], + }, + }, { id: 'apiKey', title: 'GitHub Token', @@ -1118,6 +1590,44 @@ export const GitHubBlock: BlockConfig = { 'github_create_project', 'github_update_project', 'github_delete_project', + // Search tools + 'github_search_code', + 'github_search_commits', + 'github_search_issues', + 'github_search_repos', + 'github_search_users', + // Commit tools + 'github_list_commits', + 'github_get_commit', + 'github_compare_commits', + // Gist tools + 'github_create_gist', + 'github_get_gist', + 'github_list_gists', + 'github_update_gist', + 'github_delete_gist', + 'github_fork_gist', + 'github_star_gist', + 'github_unstar_gist', + // Fork tools + 'github_fork_repo', + 'github_list_forks', + // Milestone tools + 'github_create_milestone', + 'github_get_milestone', + 'github_list_milestones', + 'github_update_milestone', + 'github_delete_milestone', + // Reaction tools + 'github_create_issue_reaction', + 'github_delete_issue_reaction', + 'github_create_comment_reaction', + 'github_delete_comment_reaction', + // Star tools + 'github_star_repo', + 'github_unstar_repo', + 'github_check_star', + 'github_list_stargazers', ], config: { tool: (params) => { @@ -1234,6 +1744,75 @@ export const GitHubBlock: BlockConfig = { return 'github_update_project' case 'github_delete_project': return 'github_delete_project' + // Search operations + case 'github_search_code': + return 'github_search_code' + case 'github_search_commits': + return 'github_search_commits' + case 'github_search_issues': + return 'github_search_issues' + case 'github_search_repos': + return 'github_search_repos' + case 'github_search_users': + return 'github_search_users' + // Commit operations + case 'github_list_commits': + return 'github_list_commits' + case 'github_get_commit': + return 'github_get_commit' + case 'github_compare_commits': + return 'github_compare_commits' + // Gist operations + case 'github_create_gist': + return 'github_create_gist' + case 'github_get_gist': + return 'github_get_gist' + case 'github_list_gists': + return 'github_list_gists' + case 'github_update_gist': + return 'github_update_gist' + case 'github_delete_gist': + return 'github_delete_gist' + case 'github_fork_gist': + return 'github_fork_gist' + case 'github_star_gist': + return 'github_star_gist' + case 'github_unstar_gist': + return 'github_unstar_gist' + // Fork operations + case 'github_fork_repo': + return 'github_fork_repo' + case 'github_list_forks': + return 'github_list_forks' + // Milestone operations + case 'github_create_milestone': + return 'github_create_milestone' + case 'github_get_milestone': + return 'github_get_milestone' + case 'github_list_milestones': + return 'github_list_milestones' + case 'github_update_milestone': + return 'github_update_milestone' + case 'github_delete_milestone': + return 'github_delete_milestone' + // Reaction operations + case 'github_create_issue_reaction': + return 'github_create_issue_reaction' + case 'github_delete_issue_reaction': + return 'github_delete_issue_reaction' + case 'github_create_comment_reaction': + return 'github_create_comment_reaction' + case 'github_delete_comment_reaction': + return 'github_delete_comment_reaction' + // Star operations + case 'github_star_repo': + return 'github_star_repo' + case 'github_unstar_repo': + return 'github_unstar_repo' + case 'github_check_star': + return 'github_check_star' + case 'github_list_stargazers': + return 'github_list_stargazers' default: return 'github_repo_info' } @@ -1297,6 +1876,38 @@ export const GitHubBlock: BlockConfig = { project_number: { type: 'number', description: 'Project number' }, project_id: { type: 'string', description: 'Project node ID' }, project_public: { type: 'boolean', description: 'Project public status' }, + // Search parameters + q: { type: 'string', description: 'Search query with qualifiers' }, + sort: { type: 'string', description: 'Sort field' }, + order: { type: 'string', description: 'Sort order (asc or desc)' }, + // Commit parameters + author: { type: 'string', description: 'Author filter' }, + committer: { type: 'string', description: 'Committer filter' }, + since: { type: 'string', description: 'Date filter (since)' }, + until: { type: 'string', description: 'Date filter (until)' }, + // Gist parameters + gist_id: { type: 'string', description: 'Gist ID' }, + description: { type: 'string', description: 'Description' }, + files: { type: 'string', description: 'Files JSON object' }, + gist_public: { type: 'boolean', description: 'Public gist status' }, + username: { type: 'string', description: 'GitHub username' }, + // Fork parameters + organization: { type: 'string', description: 'Target organization for fork' }, + fork_name: { type: 'string', description: 'Custom name for fork' }, + default_branch_only: { type: 'boolean', description: 'Fork only default branch' }, + fork_sort: { type: 'string', description: 'Fork list sort field' }, + // Milestone parameters + milestone_title: { type: 'string', description: 'Milestone title' }, + milestone_description: { type: 'string', description: 'Milestone description' }, + due_on: { type: 'string', description: 'Milestone due date' }, + milestone_number: { type: 'number', description: 'Milestone number' }, + milestone_state: { type: 'string', description: 'Milestone state filter' }, + milestone_sort: { type: 'string', description: 'Milestone sort field' }, + // Reaction parameters + reaction_content: { type: 'string', description: 'Reaction type' }, + reaction_id: { type: 'number', description: 'Reaction ID' }, + // Pagination parameters + page: { type: 'number', description: 'Page number for pagination' }, }, outputs: { content: { type: 'string', description: 'Response content' }, diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 4f09c65d26..65ada43166 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -25,6 +25,11 @@ export const GoogleCalendarBlock: BlockConfig = { { label: 'Create Event', id: 'create' }, { label: 'List Events', id: 'list' }, { label: 'Get Event', id: 'get' }, + { label: 'Update Event', id: 'update' }, + { label: 'Delete Event', id: 'delete' }, + { label: 'Move Event', id: 'move' }, + { label: 'Get Recurring Instances', id: 'instances' }, + { label: 'List Calendars', id: 'list_calendars' }, { label: 'Quick Add (Natural Language)', id: 'quick_add' }, { label: 'Invite Attendees', id: 'invite' }, ], @@ -39,7 +44,7 @@ export const GoogleCalendarBlock: BlockConfig = { requiredScopes: ['https://www.googleapis.com/auth/calendar'], placeholder: 'Select Google Calendar account', }, - // Calendar selector (basic mode) + // Calendar selector (basic mode) - not needed for list_calendars { id: 'calendarId', title: 'Calendar', @@ -50,8 +55,9 @@ export const GoogleCalendarBlock: BlockConfig = { placeholder: 'Select calendar', dependsOn: ['credential'], mode: 'basic', + condition: { field: 'operation', value: 'list_calendars', not: true }, }, - // Manual calendar ID input (advanced mode) + // Manual calendar ID input (advanced mode) - not needed for list_calendars { id: 'manualCalendarId', title: 'Calendar ID', @@ -59,6 +65,7 @@ export const GoogleCalendarBlock: BlockConfig = { canonicalParamId: 'calendarId', placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)', mode: 'advanced', + condition: { field: 'operation', value: 'list_calendars', not: true }, }, // Create Event Fields @@ -204,10 +211,179 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Event ID', type: 'short-input', placeholder: 'Event ID', - condition: { field: 'operation', value: ['get', 'invite'] }, + condition: { + field: 'operation', + value: ['get', 'update', 'delete', 'move', 'instances', 'invite'], + }, + required: true, + }, + + // Update Event Fields + { + id: 'summary', + title: 'New Event Title', + type: 'short-input', + placeholder: 'Updated meeting title', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate a clear, descriptive calendar event title based on the user's request. +The title should be concise but informative about the event's purpose. + +Return ONLY the event title - no explanations, no extra text.`, + placeholder: 'Describe the new event title...', + }, + }, + { + id: 'description', + title: 'New Description', + type: 'long-input', + placeholder: 'Updated event description', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate a helpful calendar event description based on the user's request. +Include relevant details like: +- Purpose of the event +- Agenda items +- Preparation notes +- Links or resources + +Return ONLY the description - no explanations, no extra text.`, + placeholder: 'Describe the new event details...', + }, + }, + { + id: 'location', + title: 'New Location', + type: 'short-input', + placeholder: 'Updated location', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'startDateTime', + title: 'New Start Date & Time', + type: 'short-input', + placeholder: '2025-06-03T10:00:00-08:00', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp with timezone offset based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS+HH:MM or YYYY-MM-DDTHH:MM:SS-HH:MM +Examples: +- "tomorrow at 2pm" -> Calculate tomorrow's date at 14:00:00 with local timezone offset +- "next Monday at 9am" -> Calculate next Monday at 09:00:00 with local timezone offset +- "in 2 hours" -> Calculate current time + 2 hours with local timezone offset + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the new start time (e.g., "tomorrow at 2pm")...', + generationType: 'timestamp', + }, + }, + { + id: 'endDateTime', + title: 'New End Date & Time', + type: 'short-input', + placeholder: '2025-06-03T11:00:00-08:00', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp with timezone offset based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS+HH:MM or YYYY-MM-DDTHH:MM:SS-HH:MM +Examples: +- "tomorrow at 3pm" -> Calculate tomorrow's date at 15:00:00 with local timezone offset +- "1 hour after start" -> Calculate start time + 1 hour with local timezone offset +- "next Monday at 5pm" -> Calculate next Monday at 17:00:00 with local timezone offset + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the new end time (e.g., "tomorrow at 3pm")...', + generationType: 'timestamp', + }, + }, + { + id: 'attendees', + title: 'New Attendees (comma-separated emails)', + type: 'short-input', + placeholder: 'john@example.com, jane@example.com', + condition: { field: 'operation', value: 'update' }, + }, + + // Move Event Fields + { + id: 'destinationCalendarId', + title: 'Destination Calendar ID', + type: 'short-input', + placeholder: 'destination@group.calendar.google.com', + condition: { field: 'operation', value: 'move' }, required: true, }, + // Instances Fields + { + id: 'timeMin', + title: 'Start Time Filter', + type: 'short-input', + placeholder: '2025-06-03T00:00:00Z', + condition: { field: 'operation', value: 'instances' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone). +Examples: +- "today" -> Calculate today's date at 00:00:00Z +- "yesterday" -> Calculate yesterday's date at 00:00:00Z +- "last week" -> Calculate 7 days ago at 00:00:00Z +- "beginning of this month" -> Calculate the first day of current month at 00:00:00Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the start of time range (e.g., "today", "last week")...', + generationType: 'timestamp', + }, + }, + { + id: 'timeMax', + title: 'End Time Filter', + type: 'short-input', + placeholder: '2025-06-04T00:00:00Z', + condition: { field: 'operation', value: 'instances' }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description. +The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone). +Examples: +- "tomorrow" -> Calculate tomorrow's date at 00:00:00Z +- "end of today" -> Calculate today's date at 23:59:59Z +- "next week" -> Calculate 7 days from now at 00:00:00Z +- "end of this month" -> Calculate the last day of current month at 23:59:59Z + +Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the end of time range (e.g., "tomorrow", "end of this week")...', + generationType: 'timestamp', + }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + placeholder: '250', + condition: { field: 'operation', value: ['instances', 'list_calendars'] }, + }, + + // List Calendars Fields + { + id: 'minAccessRole', + title: 'Minimum Access Role', + type: 'dropdown', + condition: { field: 'operation', value: 'list_calendars' }, + options: [ + { label: 'Any Role', id: '' }, + { label: 'Free/Busy Reader', id: 'freeBusyReader' }, + { label: 'Reader', id: 'reader' }, + { label: 'Writer', id: 'writer' }, + { label: 'Owner', id: 'owner' }, + ], + }, + // Invite Attendees Fields { id: 'attendees', @@ -262,14 +438,14 @@ Return ONLY the natural language event text - no explanations.`, required: true, }, - // Notification setting (for create, quick_add, invite) + // Notification setting (for create, update, delete, move, quick_add, invite) { id: 'sendUpdates', title: 'Send Email Notifications', type: 'dropdown', condition: { field: 'operation', - value: ['create', 'quick_add', 'invite'], + value: ['create', 'update', 'delete', 'move', 'quick_add', 'invite'], }, options: [ { label: 'All attendees', id: 'all' }, @@ -283,6 +459,11 @@ Return ONLY the natural language event text - no explanations.`, 'google_calendar_create', 'google_calendar_list', 'google_calendar_get', + 'google_calendar_update', + 'google_calendar_delete', + 'google_calendar_move', + 'google_calendar_instances', + 'google_calendar_list_calendars', 'google_calendar_quick_add', 'google_calendar_invite', ], @@ -295,6 +476,16 @@ Return ONLY the natural language event text - no explanations.`, return 'google_calendar_list' case 'get': return 'google_calendar_get' + case 'update': + return 'google_calendar_update' + case 'delete': + return 'google_calendar_delete' + case 'move': + return 'google_calendar_move' + case 'instances': + return 'google_calendar_instances' + case 'list_calendars': + return 'google_calendar_list_calendars' case 'quick_add': return 'google_calendar_quick_add' case 'invite': @@ -341,10 +532,23 @@ Return ONLY the natural language event text - no explanations.`, } // Set default sendUpdates to 'all' if not specified for operations that support it - if (['create', 'quick_add', 'invite'].includes(operation) && !processedParams.sendUpdates) { + if ( + ['create', 'update', 'delete', 'move', 'quick_add', 'invite'].includes(operation) && + !processedParams.sendUpdates + ) { processedParams.sendUpdates = 'all' } + // Convert maxResults to number if provided + if (processedParams.maxResults && typeof processedParams.maxResults === 'string') { + processedParams.maxResults = Number.parseInt(processedParams.maxResults, 10) + } + + // Remove empty minAccessRole + if (processedParams.minAccessRole === '') { + processedParams.minAccessRole = undefined + } + return { credential, ...processedParams, @@ -358,7 +562,7 @@ Return ONLY the natural language event text - no explanations.`, calendarId: { type: 'string', description: 'Calendar identifier' }, manualCalendarId: { type: 'string', description: 'Manual calendar identifier' }, - // Create operation inputs + // Create/Update operation inputs summary: { type: 'string', description: 'Event title' }, description: { type: 'string', description: 'Event description' }, location: { type: 'string', description: 'Event location' }, @@ -366,13 +570,20 @@ Return ONLY the natural language event text - no explanations.`, endDateTime: { type: 'string', description: 'Event end time' }, attendees: { type: 'string', description: 'Attendee email list' }, - // List operation inputs + // List/Instances operation inputs timeMin: { type: 'string', description: 'Start time filter' }, timeMax: { type: 'string', description: 'End time filter' }, + maxResults: { type: 'string', description: 'Maximum number of results' }, - // Get/Invite operation inputs + // Get/Update/Delete/Move/Instances/Invite operation inputs eventId: { type: 'string', description: 'Event identifier' }, + // Move operation inputs + destinationCalendarId: { type: 'string', description: 'Destination calendar ID' }, + + // List Calendars operation inputs + minAccessRole: { type: 'string', description: 'Minimum access role filter' }, + // Quick add inputs text: { type: 'string', description: 'Natural language event' }, @@ -384,7 +595,7 @@ Return ONLY the natural language event text - no explanations.`, }, outputs: { content: { type: 'string', description: 'Operation response content' }, - metadata: { type: 'json', description: 'Event metadata' }, + metadata: { type: 'json', description: 'Event or calendar metadata' }, }, } @@ -399,6 +610,11 @@ export const GoogleCalendarV2Block: BlockConfig = { 'google_calendar_create_v2', 'google_calendar_list_v2', 'google_calendar_get_v2', + 'google_calendar_update_v2', + 'google_calendar_delete_v2', + 'google_calendar_move_v2', + 'google_calendar_instances_v2', + 'google_calendar_list_calendars_v2', 'google_calendar_quick_add_v2', 'google_calendar_invite_v2', ], @@ -413,6 +629,7 @@ export const GoogleCalendarV2Block: BlockConfig = { }, }, outputs: { + // Event outputs (create, get, update, move, quick_add, invite) id: { type: 'string', description: 'Event ID' }, htmlLink: { type: 'string', description: 'Event link' }, status: { type: 'string', description: 'Event status' }, @@ -424,9 +641,17 @@ export const GoogleCalendarV2Block: BlockConfig = { attendees: { type: 'json', description: 'Event attendees' }, creator: { type: 'json', description: 'Event creator' }, organizer: { type: 'json', description: 'Event organizer' }, + // List events outputs events: { type: 'json', description: 'List of events (list operation)' }, + // Delete outputs + eventId: { type: 'string', description: 'Deleted event ID' }, + deleted: { type: 'boolean', description: 'Whether deletion was successful' }, + // Instances outputs + instances: { type: 'json', description: 'List of recurring event instances' }, + // List calendars outputs + calendars: { type: 'json', description: 'List of calendars' }, + // Common outputs nextPageToken: { type: 'string', description: 'Next page token' }, - nextSyncToken: { type: 'string', description: 'Next sync token' }, timeZone: { type: 'string', description: 'Calendar time zone' }, }, } diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 6bed9c18a9..23232db528 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -6,9 +6,10 @@ import type { GoogleDriveResponse } from '@/tools/google_drive/types' export const GoogleDriveBlock: BlockConfig = { type: 'google_drive', name: 'Google Drive', - description: 'Create, upload, and list files', + description: 'Manage files, folders, and permissions', authMode: AuthMode.OAuth, - longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.', + longDescription: + 'Integrate Google Drive into the workflow. Can create, upload, download, copy, move, delete, share files and manage permissions.', docsLink: 'https://docs.sim.ai/tools/google_drive', category: 'tools', bgColor: '#E0E0E0', @@ -20,13 +21,23 @@ export const GoogleDriveBlock: BlockConfig = { title: 'Operation', type: 'dropdown', options: [ + { label: 'List Files', id: 'list' }, + { label: 'Get File Info', id: 'get_file' }, { label: 'Create Folder', id: 'create_folder' }, { label: 'Create File', id: 'create_file' }, { label: 'Upload File', id: 'upload' }, { label: 'Download File', id: 'download' }, - { label: 'List Files', id: 'list' }, + { label: 'Copy File', id: 'copy' }, + { label: 'Update File', id: 'update' }, + { label: 'Move to Trash', id: 'trash' }, + { label: 'Restore from Trash', id: 'untrash' }, + { label: 'Delete Permanently', id: 'delete' }, + { label: 'Share File', id: 'share' }, + { label: 'Remove Sharing', id: 'unshare' }, + { label: 'List Permissions', id: 'list_permissions' }, + { label: 'Get Drive Info', id: 'get_about' }, ], - value: () => 'create_folder', + value: () => 'list', }, // Google Drive Credentials { @@ -326,26 +337,453 @@ Return ONLY the query string - no explanations, no quotes around the whole thing placeholder: 'Optional: Override the filename', condition: { field: 'operation', value: 'download' }, }, + // Get File Info Fields + { + id: 'fileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to get info for', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_file' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'get_file' }, + required: true, + }, + // Copy File Fields + { + id: 'fileSelector', + title: 'Select File to Copy', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to copy', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'copy' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to copy', + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'newName', + title: 'New File Name', + type: 'short-input', + placeholder: 'Name for the copy (optional)', + condition: { field: 'operation', value: 'copy' }, + }, + { + id: 'folderSelector', + title: 'Destination Folder', + type: 'file-selector', + canonicalParamId: 'destinationFolderId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + mimeType: 'application/vnd.google-apps.folder', + placeholder: 'Select destination folder (optional)', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'copy' }, + }, + { + id: 'manualDestinationFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'destinationFolderId', + placeholder: 'Enter destination folder ID (optional)', + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + }, + // Update File Fields + { + id: 'fileSelector', + title: 'Select File to Update', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to update', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to update', + mode: 'advanced', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + { + id: 'name', + title: 'New Name', + type: 'short-input', + placeholder: 'New name for the file (optional)', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'New description for the file (optional)', + condition: { field: 'operation', value: 'update' }, + wandConfig: { + enabled: true, + prompt: `Generate a clear, informative file description based on the user's input. +The description should help users understand the file's purpose and contents. +Keep it concise but comprehensive - typically 1-3 sentences. + +Return ONLY the description text - no explanations, no quotes, no extra text.`, + placeholder: 'Describe what this file is about...', + }, + }, + { + id: 'starred', + title: 'Starred', + type: 'dropdown', + options: [ + { label: 'No Change', id: '' }, + { label: 'Star', id: 'true' }, + { label: 'Unstar', id: 'false' }, + ], + placeholder: 'Star or unstar the file', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'addParents', + title: 'Add to Folders', + type: 'short-input', + placeholder: 'Comma-separated folder IDs to add file to', + mode: 'advanced', + condition: { field: 'operation', value: 'update' }, + }, + { + id: 'removeParents', + title: 'Remove from Folders', + type: 'short-input', + placeholder: 'Comma-separated folder IDs to remove file from', + mode: 'advanced', + condition: { field: 'operation', value: 'update' }, + }, + // Trash File Fields + { + id: 'fileSelector', + title: 'Select File to Trash', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to move to trash', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'trash' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to trash', + mode: 'advanced', + condition: { field: 'operation', value: 'trash' }, + required: true, + }, + // Untrash File Fields + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to restore from trash', + condition: { field: 'operation', value: 'untrash' }, + required: true, + }, + // Delete File Fields + { + id: 'fileSelector', + title: 'Select File to Delete', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to permanently delete', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'delete' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to permanently delete', + mode: 'advanced', + condition: { field: 'operation', value: 'delete' }, + required: true, + }, + // Share File Fields + { + id: 'fileSelector', + title: 'Select File to Share', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to share', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'share' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID to share', + mode: 'advanced', + condition: { field: 'operation', value: 'share' }, + required: true, + }, + { + id: 'shareType', + title: 'Share With', + type: 'dropdown', + options: [ + { label: 'User (email)', id: 'user' }, + { label: 'Group (email)', id: 'group' }, + { label: 'Domain', id: 'domain' }, + { label: 'Anyone with link', id: 'anyone' }, + ], + placeholder: 'Who to share with', + condition: { field: 'operation', value: 'share' }, + required: true, + }, + { + id: 'role', + title: 'Permission Level', + type: 'dropdown', + options: [ + { label: 'Viewer (read only)', id: 'reader' }, + { label: 'Commenter (view & comment)', id: 'commenter' }, + { label: 'Editor (can edit)', id: 'writer' }, + { label: 'Transfer Ownership', id: 'owner' }, + ], + placeholder: 'Permission level', + condition: { field: 'operation', value: 'share' }, + required: true, + }, + { + id: 'email', + title: 'Email Address', + type: 'short-input', + placeholder: 'Email of user or group to share with', + condition: { + field: 'operation', + value: 'share', + and: { field: 'shareType', value: ['user', 'group'] }, + }, + required: true, + }, + { + id: 'domain', + title: 'Domain', + type: 'short-input', + placeholder: 'Domain to share with (e.g., example.com)', + condition: { + field: 'operation', + value: 'share', + and: { field: 'shareType', value: 'domain' }, + }, + required: true, + }, + { + id: 'sendNotification', + title: 'Send Notification', + type: 'dropdown', + options: [ + { label: 'Yes (default)', id: 'true' }, + { label: 'No', id: 'false' }, + ], + placeholder: 'Send email notification', + condition: { + field: 'operation', + value: 'share', + and: { field: 'shareType', value: ['user', 'group'] }, + }, + }, + { + id: 'emailMessage', + title: 'Custom Message', + type: 'long-input', + placeholder: 'Custom message for the notification email (optional)', + condition: { + field: 'operation', + value: 'share', + and: { field: 'shareType', value: ['user', 'group'] }, + }, + wandConfig: { + enabled: true, + prompt: `Generate a professional, friendly sharing notification message based on the user's input. +The message should clearly explain why the file is being shared and any relevant context. +Keep it concise and appropriate for a business email - typically 2-4 sentences. + +Return ONLY the message text - no subject line, no greetings/signatures, no extra formatting.`, + placeholder: 'Describe why you are sharing this file...', + }, + }, + // Unshare (Remove Permission) Fields + { + id: 'fileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to remove sharing from', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'unshare' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'unshare' }, + required: true, + }, + { + id: 'permissionId', + title: 'Permission ID', + type: 'short-input', + placeholder: 'Permission ID to remove (use List Permissions to find)', + condition: { field: 'operation', value: 'unshare' }, + required: true, + }, + // List Permissions Fields + { + id: 'fileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'fileId', + serviceId: 'google-drive', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select a file to list permissions for', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'list_permissions' }, + }, + { + id: 'manualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'fileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'list_permissions' }, + required: true, + }, + // Get Drive Info has no additional fields (just needs credential) ], tools: { access: [ - 'google_drive_upload', + 'google_drive_list', + 'google_drive_get_file', 'google_drive_create_folder', + 'google_drive_upload', 'google_drive_download', - 'google_drive_list', + 'google_drive_copy', + 'google_drive_update', + 'google_drive_trash', + 'google_drive_untrash', + 'google_drive_delete', + 'google_drive_share', + 'google_drive_unshare', + 'google_drive_list_permissions', + 'google_drive_get_about', ], config: { tool: (params) => { switch (params.operation) { + case 'list': + return 'google_drive_list' + case 'get_file': + return 'google_drive_get_file' + case 'create_folder': + return 'google_drive_create_folder' case 'create_file': case 'upload': return 'google_drive_upload' - case 'create_folder': - return 'google_drive_create_folder' case 'download': return 'google_drive_download' - case 'list': - return 'google_drive_list' + case 'copy': + return 'google_drive_copy' + case 'update': + return 'google_drive_update' + case 'trash': + return 'google_drive_trash' + case 'untrash': + return 'google_drive_untrash' + case 'delete': + return 'google_drive_delete' + case 'share': + return 'google_drive_share' + case 'unshare': + return 'google_drive_unshare' + case 'list_permissions': + return 'google_drive_list_permissions' + case 'get_about': + return 'google_drive_get_about' default: throw new Error(`Invalid Google Drive operation: ${params.operation}`) } @@ -355,9 +793,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing credential, folderSelector, manualFolderId, + manualDestinationFolderId, fileSelector, manualFileId, mimeType, + shareType, + starred, + sendNotification, ...rest } = params @@ -367,12 +809,30 @@ Return ONLY the query string - no explanations, no quotes around the whole thing // Use fileSelector if provided, otherwise use manualFileId const effectiveFileId = (fileSelector || manualFileId || '').trim() + // Use folderSelector for destination or manualDestinationFolderId for copy operation + const effectiveDestinationFolderId = + params.operation === 'copy' + ? (folderSelector || manualDestinationFolderId || '').trim() + : undefined + + // Convert starred dropdown to boolean + const starredValue = starred === 'true' ? true : starred === 'false' ? false : undefined + + // Convert sendNotification dropdown to boolean + const sendNotificationValue = + sendNotification === 'true' ? true : sendNotification === 'false' ? false : undefined + return { credential, folderId: effectiveFolderId || undefined, fileId: effectiveFileId || undefined, + destinationFolderId: effectiveDestinationFolderId || undefined, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, mimeType: mimeType, + type: shareType, // Map shareType to type for share tool + starred: starredValue, + sendNotification: sendNotificationValue, + transferOwnership: rest.role === 'owner' ? true : undefined, ...rest, } }, @@ -381,22 +841,47 @@ Return ONLY the query string - no explanations, no quotes around the whole thing inputs: { operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Google Drive access token' }, - // Upload and Create Folder operation inputs + // File selection inputs + fileSelector: { type: 'string', description: 'Selected file' }, + manualFileId: { type: 'string', description: 'Manual file identifier' }, + // Folder selection inputs + folderSelector: { type: 'string', description: 'Selected folder' }, + manualFolderId: { type: 'string', description: 'Manual folder identifier' }, + manualDestinationFolderId: { type: 'string', description: 'Destination folder for copy' }, + // Upload and Create inputs fileName: { type: 'string', description: 'File or folder name' }, file: { type: 'json', description: 'File to upload (UserFile object)' }, content: { type: 'string', description: 'Text content to upload' }, mimeType: { type: 'string', description: 'File MIME type or export format' }, - // Download operation inputs - fileSelector: { type: 'string', description: 'Selected file to download' }, - manualFileId: { type: 'string', description: 'Manual file identifier' }, // List operation inputs - folderSelector: { type: 'string', description: 'Selected folder' }, - manualFolderId: { type: 'string', description: 'Manual folder identifier' }, query: { type: 'string', description: 'Search query' }, pageSize: { type: 'number', description: 'Results per page' }, + // Copy operation inputs + newName: { type: 'string', description: 'New name for copied file' }, + // Update operation inputs + name: { type: 'string', description: 'New name for file' }, + description: { type: 'string', description: 'New description for file' }, + starred: { type: 'string', description: 'Star or unstar the file' }, + addParents: { type: 'string', description: 'Folder IDs to add file to' }, + removeParents: { type: 'string', description: 'Folder IDs to remove file from' }, + // Share operation inputs + shareType: { type: 'string', description: 'Type of sharing (user, group, domain, anyone)' }, + role: { type: 'string', description: 'Permission role' }, + email: { type: 'string', description: 'Email address to share with' }, + domain: { type: 'string', description: 'Domain to share with' }, + sendNotification: { type: 'string', description: 'Send notification email' }, + emailMessage: { type: 'string', description: 'Custom notification message' }, + // Unshare operation inputs + permissionId: { type: 'string', description: 'Permission ID to remove' }, }, outputs: { - file: { type: 'json', description: 'File data' }, - files: { type: 'json', description: 'Files list' }, + file: { type: 'json', description: 'File metadata' }, + files: { type: 'json', description: 'List of files' }, + permission: { type: 'json', description: 'Permission details' }, + permissions: { type: 'json', description: 'List of permissions' }, + user: { type: 'json', description: 'User information' }, + storageQuota: { type: 'json', description: 'Storage quota information' }, + deleted: { type: 'boolean', description: 'Whether file was deleted' }, + removed: { type: 'boolean', description: 'Whether permission was removed' }, }, } diff --git a/apps/sim/blocks/blocks/google_form.ts b/apps/sim/blocks/blocks/google_form.ts index dbc0afa59f..38f8d384c1 100644 --- a/apps/sim/blocks/blocks/google_form.ts +++ b/apps/sim/blocks/blocks/google_form.ts @@ -5,14 +5,31 @@ import { getTrigger } from '@/triggers' export const GoogleFormsBlock: BlockConfig = { type: 'google_forms', name: 'Google Forms', - description: 'Read responses from a Google Form', + description: 'Manage Google Forms and responses', longDescription: - 'Integrate Google Forms into your workflow. Provide a Form ID to list responses, or specify a Response ID to fetch a single response. Requires OAuth.', + 'Integrate Google Forms into your workflow. Read form structure, get responses, create forms, update content, and manage notification watches.', docsLink: 'https://docs.sim.ai/tools/google_forms', category: 'tools', bgColor: '#E0E0E0', icon: GoogleFormsIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Responses', id: 'get_responses' }, + { label: 'Get Form', id: 'get_form' }, + { label: 'Create Form', id: 'create_form' }, + { label: 'Batch Update', id: 'batch_update' }, + { label: 'Set Publish Settings', id: 'set_publish_settings' }, + { label: 'Create Watch', id: 'create_watch' }, + { label: 'List Watches', id: 'list_watches' }, + { label: 'Delete Watch', id: 'delete_watch' }, + { label: 'Renew Watch', id: 'renew_watch' }, + ], + value: () => 'get_responses', + }, { id: 'credential', title: 'Google Account', @@ -22,10 +39,12 @@ export const GoogleFormsBlock: BlockConfig = { requiredScopes: [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/forms.body', 'https://www.googleapis.com/auth/forms.responses.readonly', ], placeholder: 'Select Google account', }, + // Form ID - required for most operations except create_form { id: 'formId', title: 'Form ID', @@ -33,51 +52,275 @@ export const GoogleFormsBlock: BlockConfig = { required: true, placeholder: 'Enter the Google Form ID', dependsOn: ['credential'], + condition: { + field: 'operation', + value: 'create_form', + not: true, + }, }, + // Get Responses specific fields { id: 'responseId', title: 'Response ID', type: 'short-input', - placeholder: 'Enter a specific response ID', + placeholder: 'Enter a specific response ID (optional)', + condition: { field: 'operation', value: 'get_responses' }, }, { id: 'pageSize', title: 'Page Size', type: 'short-input', placeholder: 'Max responses to retrieve (default 5000)', + condition: { field: 'operation', value: 'get_responses' }, + }, + // Create Form specific fields + { + id: 'title', + title: 'Form Title', + type: 'short-input', + required: true, + placeholder: 'Enter the form title', + condition: { field: 'operation', value: 'create_form' }, + }, + { + id: 'documentTitle', + title: 'Document Title', + type: 'short-input', + placeholder: 'Title visible in Drive (optional)', + condition: { field: 'operation', value: 'create_form' }, + }, + { + id: 'unpublished', + title: 'Create Unpublished', + type: 'switch', + condition: { field: 'operation', value: 'create_form' }, + }, + // Batch Update specific fields + { + id: 'requests', + title: 'Update Requests', + type: 'code', + placeholder: 'JSON array of update requests', + required: true, + condition: { field: 'operation', value: 'batch_update' }, + wandConfig: { + enabled: true, + prompt: `Generate a Google Forms batchUpdate requests array based on the user's description. + +The requests array can contain these operation types: +- updateFormInfo: Update form title/description. Structure: {updateFormInfo: {info: {title?, description?}, updateMask: "title,description"}} +- updateSettings: Update form settings. Structure: {updateSettings: {settings: {quizSettings?: {isQuiz: boolean}}, updateMask: "quizSettings.isQuiz"}} +- createItem: Add a question/section. Structure: {createItem: {item: {title, questionItem?: {question: {required?: boolean, choiceQuestion?: {type: "RADIO"|"CHECKBOX"|"DROP_DOWN", options: [{value: string}]}, textQuestion?: {paragraph?: boolean}, scaleQuestion?: {low: number, high: number}}}}, location: {index: number}}} +- updateItem: Modify existing item. Structure: {updateItem: {item: {...}, location: {index: number}, updateMask: "..."}} +- moveItem: Reorder item. Structure: {moveItem: {originalLocation: {index: number}, newLocation: {index: number}}} +- deleteItem: Remove item. Structure: {deleteItem: {location: {index: number}}} + +Return ONLY a valid JSON array of request objects. No explanations. + +Example for "Add a required multiple choice question about favorite color": +[{"createItem":{"item":{"title":"What is your favorite color?","questionItem":{"question":{"required":true,"choiceQuestion":{"type":"RADIO","options":[{"value":"Red"},{"value":"Blue"},{"value":"Green"}]}}}},"location":{"index":0}}}]`, + placeholder: 'Describe what you want to add or change in the form...', + }, + }, + { + id: 'includeFormInResponse', + title: 'Include Form in Response', + type: 'switch', + condition: { field: 'operation', value: 'batch_update' }, + }, + // Set Publish Settings specific fields + { + id: 'isPublished', + title: 'Published', + type: 'switch', + required: true, + condition: { field: 'operation', value: 'set_publish_settings' }, + }, + { + id: 'isAcceptingResponses', + title: 'Accepting Responses', + type: 'switch', + condition: { field: 'operation', value: 'set_publish_settings' }, + }, + // Watch specific fields + { + id: 'eventType', + title: 'Event Type', + type: 'dropdown', + options: [ + { label: 'Form Responses', id: 'RESPONSES' }, + { label: 'Form Schema Changes', id: 'SCHEMA' }, + ], + required: true, + condition: { field: 'operation', value: 'create_watch' }, + }, + { + id: 'topicName', + title: 'Pub/Sub Topic', + type: 'short-input', + required: true, + placeholder: 'projects/{project}/topics/{topic}', + condition: { field: 'operation', value: 'create_watch' }, + }, + { + id: 'watchId', + title: 'Watch ID', + type: 'short-input', + placeholder: 'Custom watch ID (optional)', + condition: { field: 'operation', value: ['create_watch', 'delete_watch', 'renew_watch'] }, + required: { field: 'operation', value: ['delete_watch', 'renew_watch'] }, }, ...getTrigger('google_forms_webhook').subBlocks, ], tools: { - access: ['google_forms_get_responses'], + access: [ + 'google_forms_get_responses', + 'google_forms_get_form', + 'google_forms_create_form', + 'google_forms_batch_update', + 'google_forms_set_publish_settings', + 'google_forms_create_watch', + 'google_forms_list_watches', + 'google_forms_delete_watch', + 'google_forms_renew_watch', + ], config: { - tool: () => 'google_forms_get_responses', + tool: (params) => { + switch (params.operation) { + case 'get_responses': + return 'google_forms_get_responses' + case 'get_form': + return 'google_forms_get_form' + case 'create_form': + return 'google_forms_create_form' + case 'batch_update': + return 'google_forms_batch_update' + case 'set_publish_settings': + return 'google_forms_set_publish_settings' + case 'create_watch': + return 'google_forms_create_watch' + case 'list_watches': + return 'google_forms_list_watches' + case 'delete_watch': + return 'google_forms_delete_watch' + case 'renew_watch': + return 'google_forms_renew_watch' + default: + throw new Error(`Invalid Google Forms operation: ${params.operation}`) + } + }, params: (params) => { - const { credential, formId, responseId, pageSize, ...rest } = params + const { + credential, + operation, + formId, + responseId, + pageSize, + title, + documentTitle, + unpublished, + requests, + includeFormInResponse, + isPublished, + isAcceptingResponses, + eventType, + topicName, + watchId, + ...rest + } = params - const effectiveFormId = String(formId || '').trim() - if (!effectiveFormId) { - throw new Error('Form ID is required.') - } + const baseParams = { ...rest, credential } + const effectiveFormId = formId ? String(formId).trim() : undefined - return { - ...rest, - formId: effectiveFormId, - responseId: responseId ? String(responseId).trim() : undefined, - pageSize: pageSize ? Number(pageSize) : undefined, - credential, + switch (operation) { + case 'get_responses': + if (!effectiveFormId) throw new Error('Form ID is required.') + return { + ...baseParams, + formId: effectiveFormId, + responseId: responseId ? String(responseId).trim() : undefined, + pageSize: pageSize ? Number(pageSize) : undefined, + } + case 'get_form': + case 'list_watches': + if (!effectiveFormId) throw new Error('Form ID is required.') + return { ...baseParams, formId: effectiveFormId } + case 'create_form': + if (!title) throw new Error('Form title is required.') + return { + ...baseParams, + title: String(title).trim(), + documentTitle: documentTitle ? String(documentTitle).trim() : undefined, + unpublished: unpublished ?? false, + } + case 'batch_update': + if (!effectiveFormId) throw new Error('Form ID is required.') + if (!requests) throw new Error('Update requests are required.') + return { + ...baseParams, + formId: effectiveFormId, + requests: typeof requests === 'string' ? JSON.parse(requests) : requests, + includeFormInResponse: includeFormInResponse ?? false, + } + case 'set_publish_settings': + if (!effectiveFormId) throw new Error('Form ID is required.') + return { + ...baseParams, + formId: effectiveFormId, + isPublished: isPublished ?? false, + isAcceptingResponses: isAcceptingResponses, + } + case 'create_watch': + if (!effectiveFormId) throw new Error('Form ID is required.') + if (!eventType) throw new Error('Event type is required.') + if (!topicName) throw new Error('Pub/Sub topic is required.') + return { + ...baseParams, + formId: effectiveFormId, + eventType: String(eventType), + topicName: String(topicName).trim(), + watchId: watchId ? String(watchId).trim() : undefined, + } + case 'delete_watch': + case 'renew_watch': + if (!effectiveFormId) throw new Error('Form ID is required.') + if (!watchId) throw new Error('Watch ID is required.') + return { + ...baseParams, + formId: effectiveFormId, + watchId: String(watchId).trim(), + } + default: + throw new Error(`Invalid Google Forms operation: ${operation}`) } }, }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Google OAuth credential' }, formId: { type: 'string', description: 'Google Form ID' }, responseId: { type: 'string', description: 'Specific response ID' }, - pageSize: { type: 'string', description: 'Max responses to retrieve (default 5000)' }, + pageSize: { type: 'string', description: 'Max responses to retrieve' }, + title: { type: 'string', description: 'Form title for creation' }, + documentTitle: { type: 'string', description: 'Document title in Drive' }, + unpublished: { type: 'boolean', description: 'Create as unpublished' }, + requests: { type: 'json', description: 'Batch update requests' }, + includeFormInResponse: { type: 'boolean', description: 'Include form in response' }, + isPublished: { type: 'boolean', description: 'Form published state' }, + isAcceptingResponses: { type: 'boolean', description: 'Form accepting responses' }, + eventType: { type: 'string', description: 'Watch event type' }, + topicName: { type: 'string', description: 'Pub/Sub topic name' }, + watchId: { type: 'string', description: 'Watch ID' }, }, outputs: { - data: { type: 'json', description: 'Response or list of responses' }, + response: { type: 'json', description: 'Operation response data' }, + formId: { type: 'string', description: 'Form ID' }, + title: { type: 'string', description: 'Form title' }, + responderUri: { type: 'string', description: 'Form responder URL' }, + items: { type: 'json', description: 'Form items' }, + responses: { type: 'json', description: 'Form responses' }, + watches: { type: 'json', description: 'Form watches' }, }, triggers: { enabled: true, diff --git a/apps/sim/blocks/blocks/google_groups.ts b/apps/sim/blocks/blocks/google_groups.ts index de3d28ca5f..2e847d8c71 100644 --- a/apps/sim/blocks/blocks/google_groups.ts +++ b/apps/sim/blocks/blocks/google_groups.ts @@ -30,6 +30,11 @@ export const GoogleGroupsBlock: BlockConfig = { { label: 'Update Member Role', id: 'update_member' }, { label: 'Remove Member', id: 'remove_member' }, { label: 'Check Membership', id: 'has_member' }, + { label: 'List Aliases', id: 'list_aliases' }, + { label: 'Add Alias', id: 'add_alias' }, + { label: 'Remove Alias', id: 'remove_alias' }, + { label: 'Get Settings', id: 'get_settings' }, + { label: 'Update Settings', id: 'update_settings' }, ], value: () => 'list_groups', }, @@ -112,10 +117,37 @@ Return ONLY the query string - no explanations, no quotes, no extra text.`, 'update_member', 'remove_member', 'has_member', + 'list_aliases', + 'add_alias', + 'remove_alias', ], }, }, + { + id: 'groupEmail', + title: 'Group Email', + type: 'short-input', + placeholder: 'group@example.com', + required: true, + condition: { + field: 'operation', + value: ['get_settings', 'update_settings'], + }, + }, + + { + id: 'alias', + title: 'Alias Email', + type: 'short-input', + placeholder: 'alias@example.com', + required: true, + condition: { + field: 'operation', + value: ['add_alias', 'remove_alias'], + }, + }, + { id: 'email', title: 'Group Email', @@ -233,6 +265,11 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, 'google_groups_remove_member', 'google_groups_update_member', 'google_groups_has_member', + 'google_groups_list_aliases', + 'google_groups_add_alias', + 'google_groups_remove_alias', + 'google_groups_get_settings', + 'google_groups_update_settings', ], config: { tool: (params) => { @@ -259,6 +296,16 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, return 'google_groups_remove_member' case 'has_member': return 'google_groups_has_member' + case 'list_aliases': + return 'google_groups_list_aliases' + case 'add_alias': + return 'google_groups_add_alias' + case 'remove_alias': + return 'google_groups_remove_alias' + case 'get_settings': + return 'google_groups_get_settings' + case 'update_settings': + return 'google_groups_update_settings' default: throw new Error(`Invalid Google Groups operation: ${params.operation}`) } @@ -330,6 +377,33 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, groupKey: rest.groupKey, memberKey: rest.memberKey, } + case 'list_aliases': + return { + credential, + groupKey: rest.groupKey, + } + case 'add_alias': + return { + credential, + groupKey: rest.groupKey, + alias: rest.alias, + } + case 'remove_alias': + return { + credential, + groupKey: rest.groupKey, + alias: rest.alias, + } + case 'get_settings': + return { + credential, + groupEmail: rest.groupEmail, + } + case 'update_settings': + return { + credential, + groupEmail: rest.groupEmail, + } default: return { credential, ...rest } } @@ -353,6 +427,8 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, memberEmail: { type: 'string', description: 'Email of member to add' }, role: { type: 'string', description: 'Member role (MEMBER, MANAGER, OWNER)' }, roles: { type: 'string', description: 'Filter by roles for list members' }, + alias: { type: 'string', description: 'Alias email address' }, + groupEmail: { type: 'string', description: 'Group email address for settings operations' }, }, outputs: { groups: { type: 'json', description: 'Array of group objects (for list_groups)' }, @@ -362,5 +438,8 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, isMember: { type: 'boolean', description: 'Membership check result (for has_member)' }, message: { type: 'string', description: 'Success message (for delete/remove operations)' }, nextPageToken: { type: 'string', description: 'Token for fetching next page of results' }, + aliases: { type: 'json', description: 'Array of alias objects (for list_aliases)' }, + settings: { type: 'json', description: 'Group settings object (for get/update_settings)' }, + deleted: { type: 'boolean', description: 'Deletion result (for remove_alias)' }, }, } diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index bb653f7950..2598425848 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -1,7 +1,7 @@ import { GoogleSheetsIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' -import type { GoogleSheetsResponse } from '@/tools/google_sheets/types' +import type { GoogleSheetsResponse, GoogleSheetsV2Response } from '@/tools/google_sheets/types' // Legacy block - hidden from toolbar export const GoogleSheetsBlock: BlockConfig = { @@ -284,3 +284,725 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, tableRange: { type: 'string', description: 'Table range' }, }, } + +export const GoogleSheetsV2Block: BlockConfig = { + type: 'google_sheets_v2', + name: 'Google Sheets', + description: 'Read, write, and update data with sheet selection', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + longDescription: + 'Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, update, clear data, create spreadsheets, get spreadsheet info, and copy sheets.', + docsLink: 'https://docs.sim.ai/tools/google_sheets', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleSheetsIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Read Data', id: 'read' }, + { label: 'Write Data', id: 'write' }, + { label: 'Update Data', id: 'update' }, + { label: 'Append Data', id: 'append' }, + { label: 'Clear Data', id: 'clear' }, + { label: 'Get Spreadsheet Info', id: 'get_info' }, + { label: 'Create Spreadsheet', id: 'create' }, + { label: 'Batch Read', id: 'batch_get' }, + { label: 'Batch Update', id: 'batch_update' }, + { label: 'Batch Clear', id: 'batch_clear' }, + { label: 'Copy Sheet', id: 'copy_sheet' }, + ], + value: () => 'read', + }, + // Google Sheets Credentials + { + id: 'credential', + title: 'Google Account', + type: 'oauth-input', + required: true, + serviceId: 'google-sheets', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + placeholder: 'Select Google account', + }, + // Spreadsheet Selector (basic mode) - not for create operation + { + id: 'spreadsheetId', + title: 'Select Spreadsheet', + type: 'file-selector', + canonicalParamId: 'spreadsheetId', + serviceId: 'google-sheets', + requiredScopes: [ + 'https://www.googleapis.com/auth/drive.file', + 'https://www.googleapis.com/auth/drive', + ], + mimeType: 'application/vnd.google-apps.spreadsheet', + placeholder: 'Select a spreadsheet', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'create', not: true }, + }, + // Manual Spreadsheet ID (advanced mode) - not for create operation + { + id: 'manualSpreadsheetId', + title: 'Spreadsheet ID', + type: 'short-input', + canonicalParamId: 'spreadsheetId', + placeholder: 'ID of the spreadsheet (from URL)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'create', not: true }, + }, + // Sheet Name Selector (basic mode) - for operations that need sheet name + { + id: 'sheetName', + title: 'Sheet (Tab)', + type: 'sheet-selector', + canonicalParamId: 'sheetName', + serviceId: 'google-sheets', + placeholder: 'Select a sheet', + required: true, + dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, + mode: 'basic', + condition: { field: 'operation', value: ['read', 'write', 'update', 'append', 'clear'] }, + }, + // Manual Sheet Name (advanced mode) - for operations that need sheet name + { + id: 'manualSheetName', + title: 'Sheet Name', + type: 'short-input', + canonicalParamId: 'sheetName', + placeholder: 'Name of the sheet/tab (e.g., Sheet1)', + required: true, + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: ['read', 'write', 'update', 'append', 'clear'] }, + }, + // Cell Range (optional for read/write/update/clear) + { + id: 'cellRange', + title: 'Cell Range', + type: 'short-input', + placeholder: 'Cell range (e.g., A1:D10). Defaults to A1 for write.', + condition: { field: 'operation', value: ['read', 'write', 'update', 'clear'] }, + wandConfig: { + enabled: true, + prompt: `Generate a valid cell range based on the user's description. + +### VALID FORMATS +- Single cell: A1 +- Range: A1:D10 +- Entire column: A:A +- Entire row: 1:1 +- Multiple columns: A:D +- Multiple rows: 1:10 + +### RANGE RULES +- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc. +- Row numbers start at 1 (not 0) + +### EXAMPLES +- "first 100 rows" -> A1:Z100 +- "cells A1 through C50" -> A1:C50 +- "column A" -> A:A +- "just the headers row" -> 1:1 +- "first cell" -> A1 + +Return ONLY the range string - no sheet name, no explanations, no quotes.`, + placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...', + }, + }, + // Write-specific Fields + { + id: 'values', + title: 'Values', + type: 'long-input', + placeholder: + 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])', + condition: { field: 'operation', value: 'write' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate Google Sheets data as a JSON array based on the user's description. + +Format options: +1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] +2. Array of objects: [{"column1": "value1", "column2": "value2"}] + +Examples: +- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]] +- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the data you want to write...', + generationType: 'json-object', + }, + }, + { + id: 'valueInputOption', + title: 'Value Input Option', + type: 'dropdown', + options: [ + { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, + { label: "Raw (Don't parse formulas)", id: 'RAW' }, + ], + condition: { field: 'operation', value: ['write', 'batch_update'] }, + }, + // Update-specific Fields + { + id: 'values', + title: 'Values', + type: 'long-input', + placeholder: + 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects', + condition: { field: 'operation', value: 'update' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate Google Sheets data as a JSON array based on the user's description. + +Format options: +1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] +2. Array of objects: [{"column1": "value1", "column2": "value2"}] + +Examples: +- "update with new prices" -> [["Product", "Price"], ["Widget A", 29.99], ["Widget B", 49.99]] +- "quarterly targets" -> [{"Q1": 10000, "Q2": 12000, "Q3": 15000, "Q4": 18000}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the data you want to update...', + generationType: 'json-object', + }, + }, + // Append-specific Fields + { + id: 'values', + title: 'Values', + type: 'long-input', + placeholder: + 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects', + condition: { field: 'operation', value: 'append' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate Google Sheets data as a JSON array based on the user's description. + +Format options: +1. Array of arrays: [["Value1", "Value2"], ["Value3", "Value4"]] +2. Array of objects: [{"column1": "value1", "column2": "value2"}] + +Examples: +- "add new sales record" -> [["2024-01-15", "Widget Pro", 5, 249.99]] +- "append customer info" -> [{"name": "Acme Corp", "contact": "John Smith", "status": "Active"}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the data you want to append...', + generationType: 'json-object', + }, + }, + { + id: 'insertDataOption', + title: 'Insert Data Option', + type: 'dropdown', + options: [ + { label: 'Insert Rows (Add new rows)', id: 'INSERT_ROWS' }, + { label: 'Overwrite (Add to existing data)', id: 'OVERWRITE' }, + ], + condition: { field: 'operation', value: 'append' }, + }, + // Create Spreadsheet Fields + { + id: 'title', + title: 'Spreadsheet Title', + type: 'short-input', + placeholder: 'Title for the new spreadsheet', + condition: { field: 'operation', value: 'create' }, + required: true, + }, + { + id: 'sheetTitles', + title: 'Sheet Names', + type: 'short-input', + placeholder: 'Comma-separated sheet names (e.g., Sheet1, Data, Summary)', + condition: { field: 'operation', value: 'create' }, + }, + // Batch Get Fields + { + id: 'ranges', + title: 'Ranges', + type: 'long-input', + placeholder: + 'JSON array of ranges to read (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Include sheet name in each range.', + condition: { field: 'operation', value: 'batch_get' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of Google Sheets ranges based on the user's description. + +### FORMAT +Return a JSON array of range strings. Each range must include the sheet name. +Format: ["SheetName!CellRange", "SheetName!CellRange", ...] + +### RANGE RULES +- Always include sheet name: Sheet1!A1:D10 (not just A1:D10) +- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10 +- Column letters are uppercase: A, B, C, ... Z, AA, AB +- Row numbers start at 1 +- For entire column: Sheet1!A:A +- For entire row: Sheet1!1:1 + +### EXAMPLES +- "all data from Sales and the summary from Reports" -> ["Sales!A1:Z1000", "Reports!A1:D20"] +- "first 100 rows from Sheet1 and Sheet2" -> ["Sheet1!A1:Z100", "Sheet2!A1:Z100"] +- "headers from all three sheets" -> ["Sheet1!1:1", "Sheet2!1:1", "Sheet3!1:1"] +- "column A from Products and Orders" -> ["Products!A:A", "Orders!A:A"] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: + 'Describe the ranges you want to read (e.g., "all data from Sales and summary from Reports")...', + generationType: 'json-object', + }, + }, + // Batch Update Fields + { + id: 'batchData', + title: 'Data', + type: 'long-input', + placeholder: + 'JSON array of {range, values} objects (e.g., [{"range": "Sheet1!A1:B2", "values": [["A","B"],["C","D"]]}])', + condition: { field: 'operation', value: 'batch_update' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of data updates for Google Sheets based on the user's description. + +### FORMAT +Return a JSON array where each item has: +- "range": The target range including sheet name (e.g., "Sheet1!A1:B2") +- "values": A 2D array of values to write + +Format: [{"range": "SheetName!CellRange", "values": [[row1], [row2], ...]}, ...] + +### RANGE RULES +- Always include sheet name: Sheet1!A1:D10 +- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10 +- The range size should match the values array dimensions + +### EXAMPLES +- "set headers to Name, Email, Phone in Sheet1 and Status, Date in Sheet2" -> + [{"range": "Sheet1!A1:C1", "values": [["Name", "Email", "Phone"]]}, {"range": "Sheet2!A1:B1", "values": [["Status", "Date"]]}] + +- "add totals row in A10 of Sales with formula" -> + [{"range": "Sales!A10:B10", "values": [["Total", "=SUM(B1:B9)"]]}] + +- "update the first three rows of data in Products" -> + [{"range": "Products!A2:C4", "values": [["Widget", 10, 29.99], ["Gadget", 5, 49.99], ["Tool", 20, 9.99]]}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: + 'Describe the updates (e.g., "set headers in Sheet1 and add totals in Sheet2")...', + generationType: 'json-object', + }, + }, + // Batch Clear Fields + { + id: 'ranges', + title: 'Ranges to Clear', + type: 'long-input', + placeholder: + 'JSON array of ranges to clear (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Include sheet name in each range.', + condition: { field: 'operation', value: 'batch_clear' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of Google Sheets ranges to clear based on the user's description. + +### FORMAT +Return a JSON array of range strings. Each range must include the sheet name. +Format: ["SheetName!CellRange", "SheetName!CellRange", ...] + +### RANGE RULES +- Always include sheet name: Sheet1!A1:D10 (not just A1:D10) +- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10 +- Column letters are uppercase: A, B, C, ... Z, AA, AB +- Row numbers start at 1 +- For entire column: Sheet1!A:A +- For entire row: Sheet1!1:1 +- For entire sheet: Sheet1!A:ZZ (or use large range) + +### EXAMPLES +- "clear all data from Sales and Reports" -> ["Sales!A1:ZZ10000", "Reports!A1:ZZ10000"] +- "clear rows 2-100 from Sheet1 and Sheet2, keep headers" -> ["Sheet1!A2:ZZ100", "Sheet2!A2:ZZ100"] +- "clear column A from Products and Orders" -> ["Products!A:A", "Orders!A:A"] +- "clear the summary section in Reports" -> ["Reports!A1:D20"] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: + 'Describe the ranges to clear (e.g., "clear all data from Sales and Reports, keep headers")...', + generationType: 'json-object', + }, + }, + // Copy Sheet Fields + { + id: 'sheetId', + title: 'Sheet ID', + type: 'short-input', + placeholder: 'Numeric ID of the sheet to copy (use Get Spreadsheet Info to find IDs)', + condition: { field: 'operation', value: 'copy_sheet' }, + required: true, + }, + { + id: 'destinationSpreadsheetId', + title: 'Destination Spreadsheet ID', + type: 'short-input', + placeholder: 'ID of the spreadsheet to copy to', + condition: { field: 'operation', value: 'copy_sheet' }, + required: true, + }, + ], + tools: { + access: [ + 'google_sheets_read_v2', + 'google_sheets_write_v2', + 'google_sheets_update_v2', + 'google_sheets_append_v2', + 'google_sheets_clear_v2', + 'google_sheets_get_spreadsheet_v2', + 'google_sheets_create_spreadsheet_v2', + 'google_sheets_batch_get_v2', + 'google_sheets_batch_update_v2', + 'google_sheets_batch_clear_v2', + 'google_sheets_copy_sheet_v2', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'read': + return 'google_sheets_read_v2' + case 'write': + return 'google_sheets_write_v2' + case 'update': + return 'google_sheets_update_v2' + case 'append': + return 'google_sheets_append_v2' + case 'clear': + return 'google_sheets_clear_v2' + case 'get_info': + return 'google_sheets_get_spreadsheet_v2' + case 'create': + return 'google_sheets_create_spreadsheet_v2' + case 'batch_get': + return 'google_sheets_batch_get_v2' + case 'batch_update': + return 'google_sheets_batch_update_v2' + case 'batch_clear': + return 'google_sheets_batch_clear_v2' + case 'copy_sheet': + return 'google_sheets_copy_sheet_v2' + default: + throw new Error(`Invalid Google Sheets V2 operation: ${params.operation}`) + } + }, + params: (params) => { + const { + credential, + values, + spreadsheetId, + manualSpreadsheetId, + sheetName, + manualSheetName, + cellRange, + title, + sheetTitles, + ranges, + batchData, + sheetId, + destinationSpreadsheetId, + ...rest + } = params + + const operation = params.operation as string + + // Handle create operation + if (operation === 'create') { + const sheetTitlesArray = sheetTitles + ? (sheetTitles as string).split(',').map((s: string) => s.trim()) + : undefined + return { + title: (title as string)?.trim(), + sheetTitles: sheetTitlesArray, + credential, + } + } + + const effectiveSpreadsheetId = ( + (spreadsheetId || manualSpreadsheetId || '') as string + ).trim() + + if (!effectiveSpreadsheetId) { + throw new Error('Spreadsheet ID is required.') + } + + // Handle get_info operation + if (operation === 'get_info') { + return { + spreadsheetId: effectiveSpreadsheetId, + credential, + } + } + + // Handle batch_get operation + if (operation === 'batch_get') { + const parsedRanges = ranges ? JSON.parse(ranges as string) : [] + return { + spreadsheetId: effectiveSpreadsheetId, + ranges: parsedRanges, + credential, + } + } + + // Handle batch_update operation + if (operation === 'batch_update') { + const parsedData = batchData ? JSON.parse(batchData as string) : [] + return { + ...rest, + spreadsheetId: effectiveSpreadsheetId, + data: parsedData, + credential, + } + } + + // Handle batch_clear operation + if (operation === 'batch_clear') { + const parsedRanges = ranges ? JSON.parse(ranges as string) : [] + return { + spreadsheetId: effectiveSpreadsheetId, + ranges: parsedRanges, + credential, + } + } + + // Handle copy_sheet operation + if (operation === 'copy_sheet') { + return { + sourceSpreadsheetId: effectiveSpreadsheetId, + sheetId: Number.parseInt(sheetId as string, 10), + destinationSpreadsheetId: (destinationSpreadsheetId as string)?.trim(), + credential, + } + } + + // Handle read/write/update/append/clear operations (require sheet name) + const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim() + + if (!effectiveSheetName) { + throw new Error('Sheet name is required. Please select or enter a sheet name.') + } + + const parsedValues = values ? JSON.parse(values as string) : undefined + + return { + ...rest, + spreadsheetId: effectiveSpreadsheetId, + sheetName: effectiveSheetName, + cellRange: cellRange ? (cellRange as string).trim() : undefined, + values: parsedValues, + credential, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Google Sheets access token' }, + spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' }, + manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' }, + sheetName: { type: 'string', description: 'Name of the sheet/tab' }, + manualSheetName: { type: 'string', description: 'Manual sheet name entry' }, + cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, + values: { type: 'string', description: 'Cell values data' }, + valueInputOption: { type: 'string', description: 'Value input option' }, + insertDataOption: { type: 'string', description: 'Data insertion option' }, + title: { type: 'string', description: 'Title for new spreadsheet' }, + sheetTitles: { type: 'string', description: 'Comma-separated sheet names for new spreadsheet' }, + ranges: { type: 'string', description: 'JSON array of ranges for batch operations' }, + batchData: { type: 'string', description: 'JSON array of data for batch update' }, + sheetId: { type: 'string', description: 'Numeric sheet ID for copy operation' }, + destinationSpreadsheetId: { + type: 'string', + description: 'Destination spreadsheet ID for copy', + }, + }, + outputs: { + // Read outputs + sheetName: { + type: 'string', + description: 'Name of the sheet', + condition: { field: 'operation', value: ['read', 'clear'] }, + }, + range: { + type: 'string', + description: 'Range that was read', + condition: { field: 'operation', value: 'read' }, + }, + values: { + type: 'json', + description: 'Cell values as 2D array', + condition: { field: 'operation', value: 'read' }, + }, + // Write/Update/Append outputs + updatedRange: { + type: 'string', + description: 'Updated range', + condition: { field: 'operation', value: ['write', 'update', 'append'] }, + }, + updatedRows: { + type: 'number', + description: 'Updated rows count', + condition: { field: 'operation', value: ['write', 'update', 'append'] }, + }, + updatedColumns: { + type: 'number', + description: 'Updated columns count', + condition: { field: 'operation', value: ['write', 'update', 'append'] }, + }, + updatedCells: { + type: 'number', + description: 'Updated cells count', + condition: { field: 'operation', value: ['write', 'update', 'append'] }, + }, + tableRange: { + type: 'string', + description: 'Table range', + condition: { field: 'operation', value: 'append' }, + }, + // Clear outputs + clearedRange: { + type: 'string', + description: 'Range that was cleared', + condition: { field: 'operation', value: 'clear' }, + }, + // Get Info / Create / Batch outputs + spreadsheetId: { + type: 'string', + description: 'Spreadsheet ID', + condition: { + field: 'operation', + value: ['get_info', 'create', 'batch_get', 'batch_update', 'batch_clear'], + }, + }, + title: { + type: 'string', + description: 'Spreadsheet title (or copied sheet title for copy_sheet)', + condition: { field: 'operation', value: ['get_info', 'create', 'copy_sheet'] }, + }, + sheets: { + type: 'json', + description: 'List of sheets in the spreadsheet', + condition: { field: 'operation', value: ['get_info', 'create'] }, + }, + locale: { + type: 'string', + description: 'Spreadsheet locale', + condition: { field: 'operation', value: 'get_info' }, + }, + timeZone: { + type: 'string', + description: 'Spreadsheet time zone', + condition: { field: 'operation', value: 'get_info' }, + }, + spreadsheetUrl: { + type: 'string', + description: 'Spreadsheet URL', + condition: { field: 'operation', value: ['get_info', 'create'] }, + }, + // Batch Get outputs + valueRanges: { + type: 'json', + description: 'Array of value ranges read from the spreadsheet', + condition: { field: 'operation', value: 'batch_get' }, + }, + // Batch Update outputs + totalUpdatedRows: { + type: 'number', + description: 'Total rows updated', + condition: { field: 'operation', value: 'batch_update' }, + }, + totalUpdatedColumns: { + type: 'number', + description: 'Total columns updated', + condition: { field: 'operation', value: 'batch_update' }, + }, + totalUpdatedCells: { + type: 'number', + description: 'Total cells updated', + condition: { field: 'operation', value: 'batch_update' }, + }, + totalUpdatedSheets: { + type: 'number', + description: 'Total sheets updated', + condition: { field: 'operation', value: 'batch_update' }, + }, + responses: { + type: 'json', + description: 'Array of update responses for each range', + condition: { field: 'operation', value: 'batch_update' }, + }, + // Batch Clear outputs + clearedRanges: { + type: 'json', + description: 'Array of ranges that were cleared', + condition: { field: 'operation', value: 'batch_clear' }, + }, + // Copy Sheet outputs + sheetId: { + type: 'number', + description: 'ID of the copied sheet in the destination', + condition: { field: 'operation', value: 'copy_sheet' }, + }, + index: { + type: 'number', + description: 'Position/index of the copied sheet', + condition: { field: 'operation', value: 'copy_sheet' }, + }, + sheetType: { + type: 'string', + description: 'Type of the sheet (GRID, CHART, etc.)', + condition: { field: 'operation', value: 'copy_sheet' }, + }, + destinationSpreadsheetId: { + type: 'string', + description: 'ID of the destination spreadsheet', + condition: { field: 'operation', value: 'copy_sheet' }, + }, + destinationSpreadsheetUrl: { + type: 'string', + description: 'URL of the destination spreadsheet', + condition: { field: 'operation', value: 'copy_sheet' }, + }, + // Common metadata + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + condition: { + field: 'operation', + value: [ + 'read', + 'write', + 'update', + 'append', + 'clear', + 'batch_get', + 'batch_update', + 'batch_clear', + ], + }, + }, + }, +} diff --git a/apps/sim/blocks/blocks/google_sheets_v2.ts b/apps/sim/blocks/blocks/google_sheets_v2.ts deleted file mode 100644 index 798b9ce221..0000000000 --- a/apps/sim/blocks/blocks/google_sheets_v2.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { GoogleSheetsIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' -import { AuthMode } from '@/blocks/types' -import type { GoogleSheetsV2Response } from '@/tools/google_sheets/types' - -export const GoogleSheetsV2Block: BlockConfig = { - type: 'google_sheets_v2', - name: 'Google Sheets', - description: 'Read, write, and update data with sheet selection', - authMode: AuthMode.OAuth, - hideFromToolbar: false, - longDescription: - 'Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, and update data in specific sheets.', - docsLink: 'https://docs.sim.ai/tools/google_sheets', - category: 'tools', - bgColor: '#E0E0E0', - icon: GoogleSheetsIcon, - subBlocks: [ - // Operation selector - { - id: 'operation', - title: 'Operation', - type: 'dropdown', - options: [ - { label: 'Read Data', id: 'read' }, - { label: 'Write Data', id: 'write' }, - { label: 'Update Data', id: 'update' }, - { label: 'Append Data', id: 'append' }, - ], - value: () => 'read', - }, - // Google Sheets Credentials - { - id: 'credential', - title: 'Google Account', - type: 'oauth-input', - required: true, - serviceId: 'google-sheets', - requiredScopes: [ - 'https://www.googleapis.com/auth/drive.file', - 'https://www.googleapis.com/auth/drive', - ], - placeholder: 'Select Google account', - }, - // Spreadsheet Selector (basic mode) - { - id: 'spreadsheetId', - title: 'Select Spreadsheet', - type: 'file-selector', - canonicalParamId: 'spreadsheetId', - serviceId: 'google-sheets', - requiredScopes: [ - 'https://www.googleapis.com/auth/drive.file', - 'https://www.googleapis.com/auth/drive', - ], - mimeType: 'application/vnd.google-apps.spreadsheet', - placeholder: 'Select a spreadsheet', - dependsOn: ['credential'], - mode: 'basic', - }, - // Manual Spreadsheet ID (advanced mode) - { - id: 'manualSpreadsheetId', - title: 'Spreadsheet ID', - type: 'short-input', - canonicalParamId: 'spreadsheetId', - placeholder: 'ID of the spreadsheet (from URL)', - dependsOn: ['credential'], - mode: 'advanced', - }, - // Sheet Name Selector (basic mode) - { - id: 'sheetName', - title: 'Sheet (Tab)', - type: 'sheet-selector', - canonicalParamId: 'sheetName', - serviceId: 'google-sheets', - placeholder: 'Select a sheet', - required: true, - dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, - mode: 'basic', - }, - // Manual Sheet Name (advanced mode) - { - id: 'manualSheetName', - title: 'Sheet Name', - type: 'short-input', - canonicalParamId: 'sheetName', - placeholder: 'Name of the sheet/tab (e.g., Sheet1)', - required: true, - dependsOn: ['credential'], - mode: 'advanced', - }, - // Cell Range (optional for read/write/update) - { - id: 'cellRange', - title: 'Cell Range', - type: 'short-input', - placeholder: 'Cell range (e.g., A1:D10). Defaults to A1 for write.', - condition: { field: 'operation', value: ['read', 'write', 'update'] }, - wandConfig: { - enabled: true, - prompt: `Generate a valid cell range based on the user's description. - -### VALID FORMATS -- Single cell: A1 -- Range: A1:D10 -- Entire column: A:A -- Entire row: 1:1 -- Multiple columns: A:D -- Multiple rows: 1:10 - -### RANGE RULES -- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc. -- Row numbers start at 1 (not 0) - -### EXAMPLES -- "first 100 rows" -> A1:Z100 -- "cells A1 through C50" -> A1:C50 -- "column A" -> A:A -- "just the headers row" -> 1:1 -- "first cell" -> A1 - -Return ONLY the range string - no sheet name, no explanations, no quotes.`, - placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...', - }, - }, - // Write-specific Fields - { - id: 'values', - title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])', - condition: { field: 'operation', value: 'write' }, - required: true, - wandConfig: { - enabled: true, - prompt: `Generate Google Sheets data as a JSON array based on the user's description. - -Format options: -1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] -2. Array of objects: [{"column1": "value1", "column2": "value2"}] - -Examples: -- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]] -- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}] - -Return ONLY the JSON array - no explanations, no markdown, no extra text.`, - placeholder: 'Describe the data you want to write...', - generationType: 'json-object', - }, - }, - { - id: 'valueInputOption', - title: 'Value Input Option', - type: 'dropdown', - options: [ - { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, - { label: "Raw (Don't parse formulas)", id: 'RAW' }, - ], - condition: { field: 'operation', value: 'write' }, - }, - // Update-specific Fields - { - id: 'values', - title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects', - condition: { field: 'operation', value: 'update' }, - required: true, - wandConfig: { - enabled: true, - prompt: `Generate Google Sheets data as a JSON array based on the user's description. - -Format options: -1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] -2. Array of objects: [{"column1": "value1", "column2": "value2"}] - -Examples: -- "update with new prices" -> [["Product", "Price"], ["Widget A", 29.99], ["Widget B", 49.99]] -- "quarterly targets" -> [{"Q1": 10000, "Q2": 12000, "Q3": 15000, "Q4": 18000}] - -Return ONLY the JSON array - no explanations, no markdown, no extra text.`, - placeholder: 'Describe the data you want to update...', - generationType: 'json-object', - }, - }, - { - id: 'valueInputOption', - title: 'Value Input Option', - type: 'dropdown', - options: [ - { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, - { label: "Raw (Don't parse formulas)", id: 'RAW' }, - ], - condition: { field: 'operation', value: 'update' }, - }, - // Append-specific Fields - { - id: 'values', - title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects', - condition: { field: 'operation', value: 'append' }, - required: true, - wandConfig: { - enabled: true, - prompt: `Generate Google Sheets data as a JSON array based on the user's description. - -Format options: -1. Array of arrays: [["Value1", "Value2"], ["Value3", "Value4"]] -2. Array of objects: [{"column1": "value1", "column2": "value2"}] - -Examples: -- "add new sales record" -> [["2024-01-15", "Widget Pro", 5, 249.99]] -- "append customer info" -> [{"name": "Acme Corp", "contact": "John Smith", "status": "Active"}] - -Return ONLY the JSON array - no explanations, no markdown, no extra text.`, - placeholder: 'Describe the data you want to append...', - generationType: 'json-object', - }, - }, - { - id: 'valueInputOption', - title: 'Value Input Option', - type: 'dropdown', - options: [ - { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, - { label: "Raw (Don't parse formulas)", id: 'RAW' }, - ], - condition: { field: 'operation', value: 'append' }, - }, - { - id: 'insertDataOption', - title: 'Insert Data Option', - type: 'dropdown', - options: [ - { label: 'Insert Rows (Add new rows)', id: 'INSERT_ROWS' }, - { label: 'Overwrite (Add to existing data)', id: 'OVERWRITE' }, - ], - condition: { field: 'operation', value: 'append' }, - }, - ], - tools: { - access: [ - 'google_sheets_read_v2', - 'google_sheets_write_v2', - 'google_sheets_update_v2', - 'google_sheets_append_v2', - ], - config: { - tool: (params) => { - switch (params.operation) { - case 'read': - return 'google_sheets_read_v2' - case 'write': - return 'google_sheets_write_v2' - case 'update': - return 'google_sheets_update_v2' - case 'append': - return 'google_sheets_append_v2' - default: - throw new Error(`Invalid Google Sheets V2 operation: ${params.operation}`) - } - }, - params: (params) => { - const { - credential, - values, - spreadsheetId, - manualSpreadsheetId, - sheetName, - manualSheetName, - cellRange, - ...rest - } = params - - const parsedValues = values ? JSON.parse(values as string) : undefined - - const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim() - const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim() - - if (!effectiveSpreadsheetId) { - throw new Error('Spreadsheet ID is required.') - } - - if (!effectiveSheetName) { - throw new Error('Sheet name is required. Please select or enter a sheet name.') - } - - return { - ...rest, - spreadsheetId: effectiveSpreadsheetId, - sheetName: effectiveSheetName, - cellRange: cellRange ? (cellRange as string).trim() : undefined, - values: parsedValues, - credential, - } - }, - }, - }, - inputs: { - operation: { type: 'string', description: 'Operation to perform' }, - credential: { type: 'string', description: 'Google Sheets access token' }, - spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' }, - manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' }, - sheetName: { type: 'string', description: 'Name of the sheet/tab' }, - manualSheetName: { type: 'string', description: 'Manual sheet name entry' }, - cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, - values: { type: 'string', description: 'Cell values data' }, - valueInputOption: { type: 'string', description: 'Value input option' }, - insertDataOption: { type: 'string', description: 'Data insertion option' }, - }, - outputs: { - sheetName: { - type: 'string', - description: 'Name of the sheet', - condition: { field: 'operation', value: 'read' }, - }, - range: { - type: 'string', - description: 'Range that was read', - condition: { field: 'operation', value: 'read' }, - }, - values: { - type: 'json', - description: 'Cell values as 2D array', - condition: { field: 'operation', value: 'read' }, - }, - updatedRange: { - type: 'string', - description: 'Updated range', - condition: { field: 'operation', value: ['write', 'update', 'append'] }, - }, - updatedRows: { - type: 'number', - description: 'Updated rows count', - condition: { field: 'operation', value: ['write', 'update', 'append'] }, - }, - updatedColumns: { - type: 'number', - description: 'Updated columns count', - condition: { field: 'operation', value: ['write', 'update', 'append'] }, - }, - updatedCells: { - type: 'number', - description: 'Updated cells count', - condition: { field: 'operation', value: ['write', 'update', 'append'] }, - }, - tableRange: { - type: 'string', - description: 'Table range', - condition: { field: 'operation', value: 'append' }, - }, - metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' }, - }, -} diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index bbab20654d..016d04a74d 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -9,7 +9,7 @@ export const GoogleSlidesBlock: BlockConfig = { description: 'Read, write, and create presentations', authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails.', + 'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text.', docsLink: 'https://docs.sim.ai/tools/google_slides', category: 'tools', bgColor: '#E0E0E0', @@ -28,6 +28,13 @@ export const GoogleSlidesBlock: BlockConfig = { { label: 'Add Slide', id: 'add_slide' }, { label: 'Add Image', id: 'add_image' }, { label: 'Get Thumbnail', id: 'get_thumbnail' }, + { label: 'Get Page', id: 'get_page' }, + { label: 'Delete Object', id: 'delete_object' }, + { label: 'Duplicate Object', id: 'duplicate_object' }, + { label: 'Reorder Slides', id: 'reorder_slides' }, + { label: 'Create Table', id: 'create_table' }, + { label: 'Create Shape', id: 'create_shape' }, + { label: 'Insert Text', id: 'insert_text' }, ], value: () => 'read', }, @@ -58,7 +65,21 @@ export const GoogleSlidesBlock: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'], + value: [ + 'read', + 'write', + 'replace_all_text', + 'add_slide', + 'add_image', + 'get_thumbnail', + 'get_page', + 'delete_object', + 'duplicate_object', + 'reorder_slides', + 'create_table', + 'create_shape', + 'insert_text', + ], }, }, // Manual presentation ID input (advanced mode) @@ -72,7 +93,21 @@ export const GoogleSlidesBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'], + value: [ + 'read', + 'write', + 'replace_all_text', + 'add_slide', + 'add_image', + 'get_thumbnail', + 'get_page', + 'delete_object', + 'duplicate_object', + 'reorder_slides', + 'create_table', + 'create_shape', + 'insert_text', + ], }, }, @@ -348,6 +383,213 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, condition: { field: 'operation', value: 'get_thumbnail' }, value: () => 'PNG', }, + + // ========== Get Page Operation Fields ========== + { + id: 'getPageObjectId', + title: 'Page/Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide/page to retrieve', + condition: { field: 'operation', value: 'get_page' }, + required: true, + }, + + // ========== Delete Object Operation Fields ========== + { + id: 'deleteObjectId', + title: 'Object ID', + type: 'short-input', + placeholder: 'Object ID of the element or slide to delete', + condition: { field: 'operation', value: 'delete_object' }, + required: true, + }, + + // ========== Duplicate Object Operation Fields ========== + { + id: 'duplicateObjectId', + title: 'Object ID', + type: 'short-input', + placeholder: 'Object ID of the element or slide to duplicate', + condition: { field: 'operation', value: 'duplicate_object' }, + required: true, + }, + { + id: 'duplicateObjectIds', + title: 'Object ID Mappings', + type: 'long-input', + placeholder: 'JSON object: {"sourceId1":"newId1","sourceId2":"newId2"}', + condition: { field: 'operation', value: 'duplicate_object' }, + mode: 'advanced', + }, + + // ========== Reorder Slides Operation Fields ========== + { + id: 'reorderSlideIds', + title: 'Slide IDs', + type: 'short-input', + placeholder: 'Comma-separated slide object IDs to move', + condition: { field: 'operation', value: 'reorder_slides' }, + required: true, + }, + { + id: 'reorderInsertionIndex', + title: 'New Position', + type: 'short-input', + placeholder: 'Zero-based index where slides should be moved', + condition: { field: 'operation', value: 'reorder_slides' }, + required: true, + }, + + // ========== Create Table Operation Fields ========== + { + id: 'tablePageObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide to add the table to', + condition: { field: 'operation', value: 'create_table' }, + required: true, + }, + { + id: 'tableRows', + title: 'Rows', + type: 'short-input', + placeholder: 'Number of rows (minimum 1)', + condition: { field: 'operation', value: 'create_table' }, + required: true, + }, + { + id: 'tableColumns', + title: 'Columns', + type: 'short-input', + placeholder: 'Number of columns (minimum 1)', + condition: { field: 'operation', value: 'create_table' }, + required: true, + }, + { + id: 'tableWidth', + title: 'Width (points)', + type: 'short-input', + placeholder: 'Table width in points (default: 400)', + condition: { field: 'operation', value: 'create_table' }, + }, + { + id: 'tableHeight', + title: 'Height (points)', + type: 'short-input', + placeholder: 'Table height in points (default: 200)', + condition: { field: 'operation', value: 'create_table' }, + }, + { + id: 'tablePositionX', + title: 'X Position (points)', + type: 'short-input', + placeholder: 'X position from left (default: 100)', + condition: { field: 'operation', value: 'create_table' }, + }, + { + id: 'tablePositionY', + title: 'Y Position (points)', + type: 'short-input', + placeholder: 'Y position from top (default: 100)', + condition: { field: 'operation', value: 'create_table' }, + }, + + // ========== Create Shape Operation Fields ========== + { + id: 'shapePageObjectId', + title: 'Slide ID', + type: 'short-input', + placeholder: 'Object ID of the slide to add the shape to', + condition: { field: 'operation', value: 'create_shape' }, + required: true, + }, + { + id: 'shapeType', + title: 'Shape Type', + type: 'dropdown', + options: [ + { label: 'Text Box', id: 'TEXT_BOX' }, + { label: 'Rectangle', id: 'RECTANGLE' }, + { label: 'Rounded Rectangle', id: 'ROUND_RECTANGLE' }, + { label: 'Ellipse', id: 'ELLIPSE' }, + { label: 'Triangle', id: 'TRIANGLE' }, + { label: 'Diamond', id: 'DIAMOND' }, + { label: 'Star (5 points)', id: 'STAR_5' }, + { label: 'Arrow (Right)', id: 'RIGHT_ARROW' }, + { label: 'Arrow (Left)', id: 'LEFT_ARROW' }, + { label: 'Arrow (Up)', id: 'UP_ARROW' }, + { label: 'Arrow (Down)', id: 'DOWN_ARROW' }, + { label: 'Heart', id: 'HEART' }, + { label: 'Cloud', id: 'CLOUD' }, + { label: 'Lightning Bolt', id: 'LIGHTNING_BOLT' }, + ], + condition: { field: 'operation', value: 'create_shape' }, + value: () => 'RECTANGLE', + }, + { + id: 'shapeWidth', + title: 'Width (points)', + type: 'short-input', + placeholder: 'Shape width in points (default: 200)', + condition: { field: 'operation', value: 'create_shape' }, + }, + { + id: 'shapeHeight', + title: 'Height (points)', + type: 'short-input', + placeholder: 'Shape height in points (default: 100)', + condition: { field: 'operation', value: 'create_shape' }, + }, + { + id: 'shapePositionX', + title: 'X Position (points)', + type: 'short-input', + placeholder: 'X position from left (default: 100)', + condition: { field: 'operation', value: 'create_shape' }, + }, + { + id: 'shapePositionY', + title: 'Y Position (points)', + type: 'short-input', + placeholder: 'Y position from top (default: 100)', + condition: { field: 'operation', value: 'create_shape' }, + }, + + // ========== Insert Text Operation Fields ========== + { + id: 'insertTextObjectId', + title: 'Object ID', + type: 'short-input', + placeholder: 'Object ID of the shape or table cell', + condition: { field: 'operation', value: 'insert_text' }, + required: true, + }, + { + id: 'insertTextContent', + title: 'Text', + type: 'long-input', + placeholder: 'Text to insert', + condition: { field: 'operation', value: 'insert_text' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate text content for a presentation slide based on the user's description. +The text should be: +- Clear and concise +- Professional and appropriate for presentations +- Well-structured with bullet points if listing items + +Return ONLY the text content - no explanations, no markdown formatting markers, no extra text.`, + placeholder: 'Describe the text you want to insert...', + }, + }, + { + id: 'insertTextIndex', + title: 'Insertion Index', + type: 'short-input', + placeholder: 'Zero-based index (default: 0)', + condition: { field: 'operation', value: 'insert_text' }, + }, ], tools: { access: [ @@ -358,6 +600,13 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, 'google_slides_add_slide', 'google_slides_add_image', 'google_slides_get_thumbnail', + 'google_slides_get_page', + 'google_slides_delete_object', + 'google_slides_duplicate_object', + 'google_slides_update_slides_position', + 'google_slides_create_table', + 'google_slides_create_shape', + 'google_slides_insert_text', ], config: { tool: (params) => { @@ -376,6 +625,20 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, return 'google_slides_add_image' case 'get_thumbnail': return 'google_slides_get_thumbnail' + case 'get_page': + return 'google_slides_get_page' + case 'delete_object': + return 'google_slides_delete_object' + case 'duplicate_object': + return 'google_slides_duplicate_object' + case 'reorder_slides': + return 'google_slides_update_slides_position' + case 'create_table': + return 'google_slides_create_table' + case 'create_shape': + return 'google_slides_create_shape' + case 'insert_text': + return 'google_slides_insert_text' default: throw new Error(`Invalid Google Slides operation: ${params.operation}`) } @@ -439,6 +702,82 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, result.pageObjectId = thumbnailPageId } + // Get Page operation + if (params.operation === 'get_page') { + result.pageObjectId = params.getPageObjectId + } + + // Delete Object operation + if (params.operation === 'delete_object') { + result.objectId = params.deleteObjectId + } + + // Duplicate Object operation + if (params.operation === 'duplicate_object') { + result.objectId = params.duplicateObjectId + if (params.duplicateObjectIds) { + result.objectIds = params.duplicateObjectIds + } + } + + // Reorder Slides operation + if (params.operation === 'reorder_slides') { + result.slideObjectIds = params.reorderSlideIds + if (params.reorderInsertionIndex) { + result.insertionIndex = Number.parseInt(params.reorderInsertionIndex as string, 10) + } + } + + // Create Table operation + if (params.operation === 'create_table') { + result.pageObjectId = params.tablePageObjectId + if (params.tableRows) { + result.rows = Number.parseInt(params.tableRows as string, 10) + } + if (params.tableColumns) { + result.columns = Number.parseInt(params.tableColumns as string, 10) + } + if (params.tableWidth) { + result.width = Number.parseInt(params.tableWidth as string, 10) + } + if (params.tableHeight) { + result.height = Number.parseInt(params.tableHeight as string, 10) + } + if (params.tablePositionX) { + result.positionX = Number.parseInt(params.tablePositionX as string, 10) + } + if (params.tablePositionY) { + result.positionY = Number.parseInt(params.tablePositionY as string, 10) + } + } + + // Create Shape operation + if (params.operation === 'create_shape') { + result.pageObjectId = params.shapePageObjectId + result.shapeType = params.shapeType + if (params.shapeWidth) { + result.width = Number.parseInt(params.shapeWidth as string, 10) + } + if (params.shapeHeight) { + result.height = Number.parseInt(params.shapeHeight as string, 10) + } + if (params.shapePositionX) { + result.positionX = Number.parseInt(params.shapePositionX as string, 10) + } + if (params.shapePositionY) { + result.positionY = Number.parseInt(params.shapePositionY as string, 10) + } + } + + // Insert Text operation + if (params.operation === 'insert_text') { + result.objectId = params.insertTextObjectId + result.text = params.insertTextContent + if (params.insertTextIndex) { + result.insertionIndex = Number.parseInt(params.insertTextIndex as string, 10) + } + } + return result }, }, @@ -479,6 +818,35 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, thumbnailPageId: { type: 'string', description: 'Slide object ID for thumbnail' }, thumbnailSize: { type: 'string', description: 'Thumbnail size' }, mimeType: { type: 'string', description: 'Image format (PNG or GIF)' }, + // Get page operation + getPageObjectId: { type: 'string', description: 'Page/slide object ID to retrieve' }, + // Delete object operation + deleteObjectId: { type: 'string', description: 'Object ID to delete' }, + // Duplicate object operation + duplicateObjectId: { type: 'string', description: 'Object ID to duplicate' }, + duplicateObjectIds: { type: 'string', description: 'JSON object ID mappings' }, + // Reorder slides operation + reorderSlideIds: { type: 'string', description: 'Comma-separated slide IDs to move' }, + reorderInsertionIndex: { type: 'number', description: 'New position for slides' }, + // Create table operation + tablePageObjectId: { type: 'string', description: 'Slide ID for table' }, + tableRows: { type: 'number', description: 'Number of rows' }, + tableColumns: { type: 'number', description: 'Number of columns' }, + tableWidth: { type: 'number', description: 'Table width in points' }, + tableHeight: { type: 'number', description: 'Table height in points' }, + tablePositionX: { type: 'number', description: 'Table X position in points' }, + tablePositionY: { type: 'number', description: 'Table Y position in points' }, + // Create shape operation + shapePageObjectId: { type: 'string', description: 'Slide ID for shape' }, + shapeType: { type: 'string', description: 'Shape type' }, + shapeWidth: { type: 'number', description: 'Shape width in points' }, + shapeHeight: { type: 'number', description: 'Shape height in points' }, + shapePositionX: { type: 'number', description: 'Shape X position in points' }, + shapePositionY: { type: 'number', description: 'Shape Y position in points' }, + // Insert text operation + insertTextObjectId: { type: 'string', description: 'Object ID for text insertion' }, + insertTextContent: { type: 'string', description: 'Text to insert' }, + insertTextIndex: { type: 'number', description: 'Insertion index' }, }, outputs: { // Read operation @@ -496,5 +864,26 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, contentUrl: { type: 'string', description: 'URL to the thumbnail image' }, width: { type: 'number', description: 'Thumbnail width in pixels' }, height: { type: 'number', description: 'Thumbnail height in pixels' }, + // Get page operation + objectId: { type: 'string', description: 'Page object ID' }, + pageType: { type: 'string', description: 'Page type (SLIDE, MASTER, etc.)' }, + pageElements: { type: 'json', description: 'Page elements array' }, + slideProperties: { type: 'json', description: 'Slide-specific properties' }, + // Delete object operation + deleted: { type: 'boolean', description: 'Whether object was deleted' }, + // Duplicate object operation + duplicatedObjectId: { type: 'string', description: 'Object ID of the duplicate' }, + // Reorder slides operation + moved: { type: 'boolean', description: 'Whether slides were moved' }, + slideObjectIds: { type: 'json', description: 'Slide IDs that were moved' }, + // Create table operation + tableId: { type: 'string', description: 'Object ID of newly created table' }, + rows: { type: 'number', description: 'Number of rows created' }, + columns: { type: 'number', description: 'Number of columns created' }, + // Create shape operation + shapeId: { type: 'string', description: 'Object ID of newly created shape' }, + // Insert text operation + inserted: { type: 'boolean', description: 'Whether text was inserted' }, + text: { type: 'string', description: 'Text that was inserted' }, }, } diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 872e740ab8..adb4c5fa17 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -1,7 +1,10 @@ import { MicrosoftExcelIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' -import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types' +import type { + MicrosoftExcelResponse, + MicrosoftExcelV2Response, +} from '@/tools/microsoft_excel/types' export const MicrosoftExcelBlock: BlockConfig = { type: 'microsoft_excel', @@ -325,3 +328,260 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, }, }, } + +export const MicrosoftExcelV2Block: BlockConfig = { + type: 'microsoft_excel_v2', + name: 'Microsoft Excel', + description: 'Read and write data with sheet selection', + authMode: AuthMode.OAuth, + hideFromToolbar: false, + longDescription: + 'Integrate Microsoft Excel into the workflow with explicit sheet selection. Can read and write data in specific sheets.', + docsLink: 'https://docs.sim.ai/tools/microsoft_excel', + category: 'tools', + bgColor: '#E0E0E0', + icon: MicrosoftExcelIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Read Data', id: 'read' }, + { label: 'Write Data', id: 'write' }, + ], + value: () => 'read', + }, + // Microsoft Excel Credentials + { + id: 'credential', + title: 'Microsoft Account', + type: 'oauth-input', + serviceId: 'microsoft-excel', + requiredScopes: [ + 'openid', + 'profile', + 'email', + 'Files.Read', + 'Files.ReadWrite', + 'offline_access', + ], + placeholder: 'Select Microsoft account', + required: true, + }, + // Spreadsheet Selector (basic mode) + { + id: 'spreadsheetId', + title: 'Select Spreadsheet', + type: 'file-selector', + canonicalParamId: 'spreadsheetId', + serviceId: 'microsoft-excel', + requiredScopes: [], + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + placeholder: 'Select a spreadsheet', + dependsOn: ['credential'], + mode: 'basic', + }, + // Manual Spreadsheet ID (advanced mode) + { + id: 'manualSpreadsheetId', + title: 'Spreadsheet ID', + type: 'short-input', + canonicalParamId: 'spreadsheetId', + placeholder: 'Enter spreadsheet ID', + dependsOn: ['credential'], + mode: 'advanced', + }, + // Sheet Name Selector (basic mode) + { + id: 'sheetName', + title: 'Sheet (Tab)', + type: 'sheet-selector', + canonicalParamId: 'sheetName', + serviceId: 'microsoft-excel', + placeholder: 'Select a sheet', + required: true, + dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, + mode: 'basic', + }, + // Manual Sheet Name (advanced mode) + { + id: 'manualSheetName', + title: 'Sheet Name', + type: 'short-input', + canonicalParamId: 'sheetName', + placeholder: 'Name of the sheet/tab (e.g., Sheet1)', + required: true, + dependsOn: ['credential'], + mode: 'advanced', + }, + // Cell Range (optional for read/write) + { + id: 'cellRange', + title: 'Cell Range', + type: 'short-input', + placeholder: 'Cell range (e.g., A1:D10). Defaults to used range for read, A1 for write.', + wandConfig: { + enabled: true, + prompt: `Generate a valid cell range based on the user's description. + +### VALID FORMATS +- Single cell: A1 +- Range: A1:D10 +- Entire column: A:A +- Entire row: 1:1 +- Multiple columns: A:D +- Multiple rows: 1:10 + +### RANGE RULES +- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc. +- Row numbers start at 1 (not 0) + +### EXAMPLES +- "first 100 rows" -> A1:Z100 +- "cells A1 through C50" -> A1:C50 +- "column A" -> A:A +- "just the headers row" -> 1:1 +- "first cell" -> A1 + +Return ONLY the range string - no sheet name, no explanations, no quotes.`, + placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...', + }, + }, + // Write-specific Fields + { + id: 'values', + title: 'Values', + type: 'long-input', + placeholder: + 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])', + condition: { field: 'operation', value: 'write' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate Microsoft Excel data as a JSON array based on the user's description. + +Format options: +1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] +2. Array of objects: [{"column1": "value1", "column2": "value2"}] + +Examples: +- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]] +- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the data you want to write...', + generationType: 'json-object', + }, + }, + { + id: 'valueInputOption', + title: 'Value Input Option', + type: 'dropdown', + options: [ + { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, + { label: "Raw (Don't parse formulas)", id: 'RAW' }, + ], + condition: { field: 'operation', value: 'write' }, + }, + ], + tools: { + access: ['microsoft_excel_read_v2', 'microsoft_excel_write_v2'], + config: { + tool: (params) => { + switch (params.operation) { + case 'read': + return 'microsoft_excel_read_v2' + case 'write': + return 'microsoft_excel_write_v2' + default: + throw new Error(`Invalid Microsoft Excel V2 operation: ${params.operation}`) + } + }, + params: (params) => { + const { + credential, + values, + spreadsheetId, + manualSpreadsheetId, + sheetName, + manualSheetName, + cellRange, + ...rest + } = params + + const parsedValues = values ? JSON.parse(values as string) : undefined + + const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim() + const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim() + + if (!effectiveSpreadsheetId) { + throw new Error('Spreadsheet ID is required.') + } + + if (!effectiveSheetName) { + throw new Error('Sheet name is required. Please select or enter a sheet name.') + } + + return { + ...rest, + spreadsheetId: effectiveSpreadsheetId, + sheetName: effectiveSheetName, + cellRange: cellRange ? (cellRange as string).trim() : undefined, + values: parsedValues, + credential, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Microsoft Excel access token' }, + spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' }, + manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' }, + sheetName: { type: 'string', description: 'Name of the sheet/tab' }, + manualSheetName: { type: 'string', description: 'Manual sheet name entry' }, + cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, + values: { type: 'string', description: 'Cell values data' }, + valueInputOption: { type: 'string', description: 'Value input option' }, + }, + outputs: { + sheetName: { + type: 'string', + description: 'Name of the sheet', + condition: { field: 'operation', value: 'read' }, + }, + range: { + type: 'string', + description: 'Range that was read', + condition: { field: 'operation', value: 'read' }, + }, + values: { + type: 'json', + description: 'Cell values as 2D array', + condition: { field: 'operation', value: 'read' }, + }, + updatedRange: { + type: 'string', + description: 'Updated range', + condition: { field: 'operation', value: 'write' }, + }, + updatedRows: { + type: 'number', + description: 'Updated rows count', + condition: { field: 'operation', value: 'write' }, + }, + updatedColumns: { + type: 'number', + description: 'Updated columns count', + condition: { field: 'operation', value: 'write' }, + }, + updatedCells: { + type: 'number', + description: 'Updated cells count', + condition: { field: 'operation', value: 'write' }, + }, + metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' }, + }, +} diff --git a/apps/sim/blocks/blocks/microsoft_excel_v2.ts b/apps/sim/blocks/blocks/microsoft_excel_v2.ts deleted file mode 100644 index e637637d08..0000000000 --- a/apps/sim/blocks/blocks/microsoft_excel_v2.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { MicrosoftExcelIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' -import { AuthMode } from '@/blocks/types' -import type { MicrosoftExcelV2Response } from '@/tools/microsoft_excel/types' - -export const MicrosoftExcelV2Block: BlockConfig = { - type: 'microsoft_excel_v2', - name: 'Microsoft Excel', - description: 'Read and write data with sheet selection', - authMode: AuthMode.OAuth, - hideFromToolbar: false, - longDescription: - 'Integrate Microsoft Excel into the workflow with explicit sheet selection. Can read and write data in specific sheets.', - docsLink: 'https://docs.sim.ai/tools/microsoft_excel', - category: 'tools', - bgColor: '#E0E0E0', - icon: MicrosoftExcelIcon, - subBlocks: [ - // Operation selector - { - id: 'operation', - title: 'Operation', - type: 'dropdown', - options: [ - { label: 'Read Data', id: 'read' }, - { label: 'Write Data', id: 'write' }, - ], - value: () => 'read', - }, - // Microsoft Excel Credentials - { - id: 'credential', - title: 'Microsoft Account', - type: 'oauth-input', - serviceId: 'microsoft-excel', - requiredScopes: [ - 'openid', - 'profile', - 'email', - 'Files.Read', - 'Files.ReadWrite', - 'offline_access', - ], - placeholder: 'Select Microsoft account', - required: true, - }, - // Spreadsheet Selector (basic mode) - { - id: 'spreadsheetId', - title: 'Select Spreadsheet', - type: 'file-selector', - canonicalParamId: 'spreadsheetId', - serviceId: 'microsoft-excel', - requiredScopes: [], - mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - placeholder: 'Select a spreadsheet', - dependsOn: ['credential'], - mode: 'basic', - }, - // Manual Spreadsheet ID (advanced mode) - { - id: 'manualSpreadsheetId', - title: 'Spreadsheet ID', - type: 'short-input', - canonicalParamId: 'spreadsheetId', - placeholder: 'Enter spreadsheet ID', - dependsOn: ['credential'], - mode: 'advanced', - }, - // Sheet Name Selector (basic mode) - { - id: 'sheetName', - title: 'Sheet (Tab)', - type: 'sheet-selector', - canonicalParamId: 'sheetName', - serviceId: 'microsoft-excel', - placeholder: 'Select a sheet', - required: true, - dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, - mode: 'basic', - }, - // Manual Sheet Name (advanced mode) - { - id: 'manualSheetName', - title: 'Sheet Name', - type: 'short-input', - canonicalParamId: 'sheetName', - placeholder: 'Name of the sheet/tab (e.g., Sheet1)', - required: true, - dependsOn: ['credential'], - mode: 'advanced', - }, - // Cell Range (optional for read/write) - { - id: 'cellRange', - title: 'Cell Range', - type: 'short-input', - placeholder: 'Cell range (e.g., A1:D10). Defaults to used range for read, A1 for write.', - wandConfig: { - enabled: true, - prompt: `Generate a valid cell range based on the user's description. - -### VALID FORMATS -- Single cell: A1 -- Range: A1:D10 -- Entire column: A:A -- Entire row: 1:1 -- Multiple columns: A:D -- Multiple rows: 1:10 - -### RANGE RULES -- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc. -- Row numbers start at 1 (not 0) - -### EXAMPLES -- "first 100 rows" -> A1:Z100 -- "cells A1 through C50" -> A1:C50 -- "column A" -> A:A -- "just the headers row" -> 1:1 -- "first cell" -> A1 - -Return ONLY the range string - no sheet name, no explanations, no quotes.`, - placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...', - }, - }, - // Write-specific Fields - { - id: 'values', - title: 'Values', - type: 'long-input', - placeholder: - 'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])', - condition: { field: 'operation', value: 'write' }, - required: true, - wandConfig: { - enabled: true, - prompt: `Generate Microsoft Excel data as a JSON array based on the user's description. - -Format options: -1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]] -2. Array of objects: [{"column1": "value1", "column2": "value2"}] - -Examples: -- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]] -- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}] - -Return ONLY the JSON array - no explanations, no markdown, no extra text.`, - placeholder: 'Describe the data you want to write...', - generationType: 'json-object', - }, - }, - { - id: 'valueInputOption', - title: 'Value Input Option', - type: 'dropdown', - options: [ - { label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' }, - { label: "Raw (Don't parse formulas)", id: 'RAW' }, - ], - condition: { field: 'operation', value: 'write' }, - }, - ], - tools: { - access: ['microsoft_excel_read_v2', 'microsoft_excel_write_v2'], - config: { - tool: (params) => { - switch (params.operation) { - case 'read': - return 'microsoft_excel_read_v2' - case 'write': - return 'microsoft_excel_write_v2' - default: - throw new Error(`Invalid Microsoft Excel V2 operation: ${params.operation}`) - } - }, - params: (params) => { - const { - credential, - values, - spreadsheetId, - manualSpreadsheetId, - sheetName, - manualSheetName, - cellRange, - ...rest - } = params - - const parsedValues = values ? JSON.parse(values as string) : undefined - - const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim() - const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim() - - if (!effectiveSpreadsheetId) { - throw new Error('Spreadsheet ID is required.') - } - - if (!effectiveSheetName) { - throw new Error('Sheet name is required. Please select or enter a sheet name.') - } - - return { - ...rest, - spreadsheetId: effectiveSpreadsheetId, - sheetName: effectiveSheetName, - cellRange: cellRange ? (cellRange as string).trim() : undefined, - values: parsedValues, - credential, - } - }, - }, - }, - inputs: { - operation: { type: 'string', description: 'Operation to perform' }, - credential: { type: 'string', description: 'Microsoft Excel access token' }, - spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' }, - manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' }, - sheetName: { type: 'string', description: 'Name of the sheet/tab' }, - manualSheetName: { type: 'string', description: 'Manual sheet name entry' }, - cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, - values: { type: 'string', description: 'Cell values data' }, - valueInputOption: { type: 'string', description: 'Value input option' }, - }, - outputs: { - sheetName: { - type: 'string', - description: 'Name of the sheet', - condition: { field: 'operation', value: 'read' }, - }, - range: { - type: 'string', - description: 'Range that was read', - condition: { field: 'operation', value: 'read' }, - }, - values: { - type: 'json', - description: 'Cell values as 2D array', - condition: { field: 'operation', value: 'read' }, - }, - updatedRange: { - type: 'string', - description: 'Updated range', - condition: { field: 'operation', value: 'write' }, - }, - updatedRows: { - type: 'number', - description: 'Updated rows count', - condition: { field: 'operation', value: 'write' }, - }, - updatedColumns: { - type: 'number', - description: 'Updated columns count', - condition: { field: 'operation', value: 'write' }, - }, - updatedCells: { - type: 'number', - description: 'Updated cells count', - condition: { field: 'operation', value: 'write' }, - }, - metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' }, - }, -} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 916453eda2..33ff415f72 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -40,8 +40,7 @@ import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_form' import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups' -import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets' -import { GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets_v2' +import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets' import { GoogleSlidesBlock } from '@/blocks/blocks/google_slides' import { GoogleVaultBlock } from '@/blocks/blocks/google_vault' import { GrafanaBlock } from '@/blocks/blocks/grafana' @@ -73,8 +72,7 @@ import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger' import { McpBlock } from '@/blocks/blocks/mcp' import { Mem0Block } from '@/blocks/blocks/mem0' import { MemoryBlock } from '@/blocks/blocks/memory' -import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel' -import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel_v2' +import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel' import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index 26797e7b29..49f4646402 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -587,9 +587,14 @@ const Combobox = memo( value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Escape') { - setOpen(false) - setSearchQuery('') + // Forward navigation keys to main handler + if ( + e.key === 'ArrowDown' || + e.key === 'ArrowUp' || + e.key === 'Enter' || + e.key === 'Escape' + ) { + handleKeyDown(e as unknown as KeyboardEvent) } }} /> diff --git a/apps/sim/tools/a2a/cancel_task.ts b/apps/sim/tools/a2a/cancel_task.ts index 6ac1b78937..dd0c5cdc23 100644 --- a/apps/sim/tools/a2a/cancel_task.ts +++ b/apps/sim/tools/a2a/cancel_task.ts @@ -11,15 +11,18 @@ export const a2aCancelTaskTool: ToolConfig = agentUrl: { type: 'string', required: true, + visibility: 'user-only', description: 'The A2A agent endpoint URL', }, taskId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Task ID to query', }, apiKey: { type: 'string', + visibility: 'user-only', description: 'API key for authentication', }, historyLength: { type: 'number', + visibility: 'user-or-llm', description: 'Number of history messages to include', }, }, diff --git a/apps/sim/tools/a2a/resubscribe.ts b/apps/sim/tools/a2a/resubscribe.ts index 99456b8b53..2aa1600824 100644 --- a/apps/sim/tools/a2a/resubscribe.ts +++ b/apps/sim/tools/a2a/resubscribe.ts @@ -11,15 +11,18 @@ export const a2aResubscribeTool: ToolConfig = { + id: 'github_check_star', + name: 'GitHub Check Star', + description: 'Check if you have starred a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/user/starred/${params.owner}/${params.repo}`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const starred = response.status === 204 + + return { + success: true, + output: { + content: starred + ? `You have starred ${params?.owner}/${params?.repo}` + : `You have not starred ${params?.owner}/${params?.repo}`, + metadata: { + starred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Check star metadata', + properties: { + starred: { type: 'boolean', description: 'Whether you have starred the repo' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, + }, + }, +} + +export const checkStarV2Tool: ToolConfig = { + id: 'github_check_star_v2', + name: checkStarTool.name, + description: checkStarTool.description, + version: '2.0.0', + params: checkStarTool.params, + request: checkStarTool.request, + + transformResponse: async (response: Response, params) => { + const starred = response.status === 204 + return { + success: true, + output: { + starred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + } + }, + + outputs: { + starred: { type: 'boolean', description: 'Whether you have starred the repo' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, +} diff --git a/apps/sim/tools/github/compare_commits.ts b/apps/sim/tools/github/compare_commits.ts new file mode 100644 index 0000000000..02ee8a5689 --- /dev/null +++ b/apps/sim/tools/github/compare_commits.ts @@ -0,0 +1,256 @@ +import type { ToolConfig } from '@/tools/types' + +interface CompareCommitsParams { + owner: string + repo: string + base: string + head: string + per_page?: number + page?: number + apiKey: string +} + +interface CompareCommitsResponse { + success: boolean + output: { + content: string + metadata: { + status: string + ahead_by: number + behind_by: number + total_commits: number + html_url: string + diff_url: string + patch_url: string + base_commit: { sha: string; html_url: string } + merge_base_commit: { sha: string; html_url: string } + commits: Array<{ + sha: string + html_url: string + message: string + author: { login?: string; name: string } + }> + files: Array<{ + filename: string + status: string + additions: number + deletions: number + changes: number + }> + } + } +} + +export const compareCommitsTool: ToolConfig = { + id: 'github_compare_commits', + name: 'GitHub Compare Commits', + description: + 'Compare two commits or branches to see the diff, commits between them, and changed files', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + base: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Base branch/tag/SHA for comparison', + }, + head: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Head branch/tag/SHA for comparison', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page for files (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number for files (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://api.github.com/repos/${params.owner}/${params.repo}/compare/${params.base}...${params.head}` + ) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const commits = (data.commits ?? []).map((c: any) => ({ + sha: c.sha, + html_url: c.html_url, + message: c.commit.message, + author: { + login: c.author?.login, + name: c.commit.author.name, + }, + })) + + const files = (data.files ?? []).map((f: any) => ({ + filename: f.filename, + status: f.status, + additions: f.additions, + deletions: f.deletions, + changes: f.changes, + })) + + const metadata = { + status: data.status, + ahead_by: data.ahead_by, + behind_by: data.behind_by, + total_commits: data.total_commits, + html_url: data.html_url, + diff_url: data.diff_url, + patch_url: data.patch_url, + base_commit: { + sha: data.base_commit.sha, + html_url: data.base_commit.html_url, + }, + merge_base_commit: { + sha: data.merge_base_commit.sha, + html_url: data.merge_base_commit.html_url, + }, + commits, + files, + } + + const content = `Comparing ${data.base_commit.sha.substring(0, 7)}...${data.commits?.length > 0 ? data.commits[data.commits.length - 1].sha.substring(0, 7) : 'HEAD'} +Status: ${data.status} | Ahead: ${data.ahead_by} | Behind: ${data.behind_by} +Total commits: ${data.total_commits} | Files changed: ${files.length} +${data.html_url} + +Commits: +${commits.map((c: any) => ` ${c.sha.substring(0, 7)} - ${c.message.split('\n')[0]}`).join('\n')} + +Files changed: +${files.map((f: any) => ` ${f.status}: ${f.filename} (+${f.additions} -${f.deletions})`).join('\n')}` + + return { + success: true, + output: { + content, + metadata, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable comparison' }, + metadata: { + type: 'object', + description: 'Comparison metadata', + properties: { + status: { type: 'string', description: 'ahead, behind, identical, or diverged' }, + ahead_by: { type: 'number', description: 'Commits ahead' }, + behind_by: { type: 'number', description: 'Commits behind' }, + total_commits: { type: 'number', description: 'Total commits between' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + diff_url: { type: 'string', description: 'Diff URL' }, + patch_url: { type: 'string', description: 'Patch URL' }, + base_commit: { type: 'object', description: 'Base commit info' }, + merge_base_commit: { type: 'object', description: 'Merge base commit info' }, + commits: { + type: 'array', + description: 'Commits between base and head', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Author info' }, + }, + }, + }, + files: { + type: 'array', + description: 'Changed files', + items: { + type: 'object', + properties: { + filename: { type: 'string', description: 'File path' }, + status: { type: 'string', description: 'Change type' }, + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + changes: { type: 'number', description: 'Total changes' }, + }, + }, + }, + }, + }, + }, +} + +export const compareCommitsV2Tool: ToolConfig = { + id: 'github_compare_commits_v2', + name: compareCommitsTool.name, + description: compareCommitsTool.description, + version: '2.0.0', + params: compareCommitsTool.params, + request: compareCommitsTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + commits: data.commits ?? [], + files: data.files ?? [], + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Comparison status' }, + ahead_by: { type: 'number', description: 'Commits ahead' }, + behind_by: { type: 'number', description: 'Commits behind' }, + total_commits: { type: 'number', description: 'Total commits' }, + html_url: { type: 'string', description: 'Web URL' }, + diff_url: { type: 'string', description: 'Diff URL' }, + patch_url: { type: 'string', description: 'Patch URL' }, + base_commit: { type: 'object', description: 'Base commit' }, + merge_base_commit: { type: 'object', description: 'Merge base' }, + commits: { type: 'array', description: 'Commits between' }, + files: { type: 'array', description: 'Changed files' }, + }, +} diff --git a/apps/sim/tools/github/create_comment_reaction.ts b/apps/sim/tools/github/create_comment_reaction.ts new file mode 100644 index 0000000000..e27d897a7f --- /dev/null +++ b/apps/sim/tools/github/create_comment_reaction.ts @@ -0,0 +1,138 @@ +import type { ToolConfig } from '@/tools/types' + +interface CreateCommentReactionParams { + owner: string + repo: string + comment_id: number + content: '+1' | '-1' | 'laugh' | 'confused' | 'heart' | 'hooray' | 'rocket' | 'eyes' + apiKey: string +} + +interface CreateCommentReactionResponse { + success: boolean + output: { + content: string + metadata: { + id: number + user: { login: string } + content: string + created_at: string + } + } +} + +export const createCommentReactionTool: ToolConfig< + CreateCommentReactionParams, + CreateCommentReactionResponse +> = { + id: 'github_create_comment_reaction', + name: 'GitHub Create Comment Reaction', + description: 'Add a reaction to an issue comment', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + comment_id: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Comment ID', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Reaction type: +1 (thumbs up), -1 (thumbs down), laugh, confused, heart, hooray, rocket, eyes', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/issues/comments/${params.comment_id}/reactions`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.squirrel-girl-preview+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => ({ + content: params.content, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const content = `Added ${data.content} reaction to comment by ${data.user?.login ?? 'unknown'}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + user: { login: data.user?.login ?? 'unknown' }, + content: data.content, + created_at: data.created_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Reaction metadata', + properties: { + id: { type: 'number', description: 'Reaction ID' }, + user: { type: 'object', description: 'User who reacted' }, + content: { type: 'string', description: 'Reaction type' }, + created_at: { type: 'string', description: 'Creation date' }, + }, + }, + }, +} + +export const createCommentReactionV2Tool: ToolConfig = { + id: 'github_create_comment_reaction_v2', + name: createCommentReactionTool.name, + description: createCommentReactionTool.description, + version: '2.0.0', + params: createCommentReactionTool.params, + request: createCommentReactionTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: data, + } + }, + + outputs: { + id: { type: 'number', description: 'Reaction ID' }, + user: { type: 'object', description: 'User who reacted' }, + content: { type: 'string', description: 'Reaction type' }, + created_at: { type: 'string', description: 'Creation date' }, + }, +} diff --git a/apps/sim/tools/github/create_gist.ts b/apps/sim/tools/github/create_gist.ts new file mode 100644 index 0000000000..96b349e35a --- /dev/null +++ b/apps/sim/tools/github/create_gist.ts @@ -0,0 +1,183 @@ +import type { ToolConfig } from '@/tools/types' + +interface CreateGistParams { + description?: string + files: string + public?: boolean + apiKey: string +} + +interface CreateGistResponse { + success: boolean + output: { + content: string + metadata: { + id: string + html_url: string + git_pull_url: string + git_push_url: string + description: string | null + public: boolean + created_at: string + updated_at: string + files: Record< + string, + { filename: string; type: string; language: string | null; size: number } + > + owner: { login: string } + } + } +} + +export const createGistTool: ToolConfig = { + id: 'github_create_gist', + name: 'GitHub Create Gist', + description: 'Create a new gist with one or more files', + version: '1.0.0', + + params: { + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the gist', + }, + files: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'JSON object with filenames as keys and content as values. Example: {"file.txt": {"content": "Hello"}}', + }, + public: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the gist is public (default: false)', + default: false, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: () => 'https://api.github.com/gists', + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const filesObj = typeof params.files === 'string' ? JSON.parse(params.files) : params.files + return { + description: params.description, + public: params.public ?? false, + files: filesObj, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + const files: Record< + string, + { filename: string; type: string; language: string | null; size: number } + > = {} + for (const [key, value] of Object.entries(data.files ?? {})) { + const file = value as any + files[key] = { + filename: file.filename, + type: file.type, + language: file.language ?? null, + size: file.size, + } + } + + const metadata = { + id: data.id, + html_url: data.html_url, + git_pull_url: data.git_pull_url, + git_push_url: data.git_push_url, + description: data.description ?? null, + public: data.public, + created_at: data.created_at, + updated_at: data.updated_at, + files, + owner: { login: data.owner?.login ?? 'unknown' }, + } + + const content = `Created gist: ${data.html_url} +Description: ${data.description ?? 'No description'} +Public: ${data.public} +Files: ${Object.keys(files).join(', ')}` + + return { + success: true, + output: { + content, + metadata, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Gist metadata', + properties: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Files in gist' }, + owner: { type: 'object', description: 'Owner info' }, + }, + }, + }, +} + +export const createGistV2Tool: ToolConfig = { + id: 'github_create_gist_v2', + name: createGistTool.name, + description: createGistTool.description, + version: '2.0.0', + params: createGistTool.params, + request: createGistTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + files: data.files ?? {}, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Files in gist' }, + owner: { type: 'object', description: 'Owner info' }, + }, +} diff --git a/apps/sim/tools/github/create_issue_reaction.ts b/apps/sim/tools/github/create_issue_reaction.ts new file mode 100644 index 0000000000..dfb30fd5b8 --- /dev/null +++ b/apps/sim/tools/github/create_issue_reaction.ts @@ -0,0 +1,138 @@ +import type { ToolConfig } from '@/tools/types' + +interface CreateIssueReactionParams { + owner: string + repo: string + issue_number: number + content: '+1' | '-1' | 'laugh' | 'confused' | 'heart' | 'hooray' | 'rocket' | 'eyes' + apiKey: string +} + +interface CreateIssueReactionResponse { + success: boolean + output: { + content: string + metadata: { + id: number + user: { login: string } + content: string + created_at: string + } + } +} + +export const createIssueReactionTool: ToolConfig< + CreateIssueReactionParams, + CreateIssueReactionResponse +> = { + id: 'github_create_issue_reaction', + name: 'GitHub Create Issue Reaction', + description: 'Add a reaction to an issue', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + issue_number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Issue number', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Reaction type: +1 (thumbs up), -1 (thumbs down), laugh, confused, heart, hooray, rocket, eyes', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/issues/${params.issue_number}/reactions`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.squirrel-girl-preview+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => ({ + content: params.content, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const content = `Added ${data.content} reaction to issue by ${data.user?.login ?? 'unknown'}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + user: { login: data.user?.login ?? 'unknown' }, + content: data.content, + created_at: data.created_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Reaction metadata', + properties: { + id: { type: 'number', description: 'Reaction ID' }, + user: { type: 'object', description: 'User who reacted' }, + content: { type: 'string', description: 'Reaction type' }, + created_at: { type: 'string', description: 'Creation date' }, + }, + }, + }, +} + +export const createIssueReactionV2Tool: ToolConfig = { + id: 'github_create_issue_reaction_v2', + name: createIssueReactionTool.name, + description: createIssueReactionTool.description, + version: '2.0.0', + params: createIssueReactionTool.params, + request: createIssueReactionTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: data, + } + }, + + outputs: { + id: { type: 'number', description: 'Reaction ID' }, + user: { type: 'object', description: 'User who reacted' }, + content: { type: 'string', description: 'Reaction type' }, + created_at: { type: 'string', description: 'Creation date' }, + }, +} diff --git a/apps/sim/tools/github/create_milestone.ts b/apps/sim/tools/github/create_milestone.ts new file mode 100644 index 0000000000..fe5b91c0bc --- /dev/null +++ b/apps/sim/tools/github/create_milestone.ts @@ -0,0 +1,182 @@ +import type { ToolConfig } from '@/tools/types' + +interface CreateMilestoneParams { + owner: string + repo: string + title: string + state?: 'open' | 'closed' + description?: string + due_on?: string + apiKey: string +} + +interface CreateMilestoneResponse { + success: boolean + output: { + content: string + metadata: { + number: number + title: string + description: string | null + state: string + html_url: string + due_on: string | null + open_issues: number + closed_issues: number + created_at: string + creator: { login: string } + } + } +} + +export const createMilestoneTool: ToolConfig = { + id: 'github_create_milestone', + name: 'GitHub Create Milestone', + description: 'Create a milestone in a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Milestone title', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State: open or closed (default: open)', + default: 'open', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Milestone description', + }, + due_on: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Due date (ISO 8601 format, e.g., 2024-12-31T23:59:59Z)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/repos/${params.owner}/${params.repo}/milestones`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => ({ + title: params.title, + state: params.state ?? 'open', + description: params.description, + due_on: params.due_on, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const content = `Created milestone: ${data.title} +Number: ${data.number} +State: ${data.state} +Due: ${data.due_on ?? 'No due date'} +${data.html_url}` + + return { + success: true, + output: { + content, + metadata: { + number: data.number, + title: data.title, + description: data.description ?? null, + state: data.state, + html_url: data.html_url, + due_on: data.due_on ?? null, + open_issues: data.open_issues, + closed_issues: data.closed_issues, + created_at: data.created_at, + creator: { login: data.creator?.login ?? 'unknown' }, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Milestone metadata', + properties: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues count' }, + closed_issues: { type: 'number', description: 'Closed issues count' }, + created_at: { type: 'string', description: 'Creation date' }, + creator: { type: 'object', description: 'Creator info' }, + }, + }, + }, +} + +export const createMilestoneV2Tool: ToolConfig = { + id: 'github_create_milestone_v2', + name: createMilestoneTool.name, + description: createMilestoneTool.description, + version: '2.0.0', + params: createMilestoneTool.params, + request: createMilestoneTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + due_on: data.due_on ?? null, + }, + } + }, + + outputs: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + creator: { type: 'object', description: 'Creator' }, + }, +} diff --git a/apps/sim/tools/github/delete_comment_reaction.ts b/apps/sim/tools/github/delete_comment_reaction.ts new file mode 100644 index 0000000000..7708bfe3a1 --- /dev/null +++ b/apps/sim/tools/github/delete_comment_reaction.ts @@ -0,0 +1,128 @@ +import type { ToolConfig } from '@/tools/types' + +interface DeleteCommentReactionParams { + owner: string + repo: string + comment_id: number + reaction_id: number + apiKey: string +} + +interface DeleteCommentReactionResponse { + success: boolean + output: { + content: string + metadata: { + deleted: boolean + reaction_id: number + } + } +} + +export const deleteCommentReactionTool: ToolConfig< + DeleteCommentReactionParams, + DeleteCommentReactionResponse +> = { + id: 'github_delete_comment_reaction', + name: 'GitHub Delete Comment Reaction', + description: 'Remove a reaction from an issue comment', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + comment_id: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Comment ID', + }, + reaction_id: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Reaction ID to delete', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/issues/comments/${params.comment_id}/reactions/${params.reaction_id}`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.squirrel-girl-preview+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const deleted = response.status === 204 + + return { + success: deleted, + output: { + content: deleted + ? `Successfully deleted reaction ${params?.reaction_id}` + : `Failed to delete reaction ${params?.reaction_id}`, + metadata: { + deleted, + reaction_id: params?.reaction_id ?? 0, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Delete operation metadata', + properties: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + reaction_id: { type: 'number', description: 'The deleted reaction ID' }, + }, + }, + }, +} + +export const deleteCommentReactionV2Tool: ToolConfig = { + id: 'github_delete_comment_reaction_v2', + name: deleteCommentReactionTool.name, + description: deleteCommentReactionTool.description, + version: '2.0.0', + params: deleteCommentReactionTool.params, + request: deleteCommentReactionTool.request, + + transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 + return { + success: deleted, + output: { + deleted, + reaction_id: params?.reaction_id ?? 0, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + reaction_id: { type: 'number', description: 'The deleted reaction ID' }, + }, +} diff --git a/apps/sim/tools/github/delete_gist.ts b/apps/sim/tools/github/delete_gist.ts new file mode 100644 index 0000000000..da9e884047 --- /dev/null +++ b/apps/sim/tools/github/delete_gist.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' + +interface DeleteGistParams { + gist_id: string + apiKey: string +} + +interface DeleteGistResponse { + success: boolean + output: { + content: string + metadata: { + deleted: boolean + gist_id: string + } + } +} + +export const deleteGistTool: ToolConfig = { + id: 'github_delete_gist', + name: 'GitHub Delete Gist', + description: 'Delete a gist by ID', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID to delete', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const deleted = response.status === 204 + + return { + success: deleted, + output: { + content: deleted + ? `Successfully deleted gist ${params?.gist_id}` + : `Failed to delete gist ${params?.gist_id}`, + metadata: { + deleted, + gist_id: params?.gist_id ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Delete operation metadata', + properties: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + gist_id: { type: 'string', description: 'The deleted gist ID' }, + }, + }, + }, +} + +export const deleteGistV2Tool: ToolConfig = { + id: 'github_delete_gist_v2', + name: deleteGistTool.name, + description: deleteGistTool.description, + version: '2.0.0', + params: deleteGistTool.params, + request: deleteGistTool.request, + + transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 + return { + success: deleted, + output: { + deleted, + gist_id: params?.gist_id ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + gist_id: { type: 'string', description: 'The deleted gist ID' }, + }, +} diff --git a/apps/sim/tools/github/delete_issue_reaction.ts b/apps/sim/tools/github/delete_issue_reaction.ts new file mode 100644 index 0000000000..410d398026 --- /dev/null +++ b/apps/sim/tools/github/delete_issue_reaction.ts @@ -0,0 +1,128 @@ +import type { ToolConfig } from '@/tools/types' + +interface DeleteIssueReactionParams { + owner: string + repo: string + issue_number: number + reaction_id: number + apiKey: string +} + +interface DeleteIssueReactionResponse { + success: boolean + output: { + content: string + metadata: { + deleted: boolean + reaction_id: number + } + } +} + +export const deleteIssueReactionTool: ToolConfig< + DeleteIssueReactionParams, + DeleteIssueReactionResponse +> = { + id: 'github_delete_issue_reaction', + name: 'GitHub Delete Issue Reaction', + description: 'Remove a reaction from an issue', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + issue_number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Issue number', + }, + reaction_id: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Reaction ID to delete', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/issues/${params.issue_number}/reactions/${params.reaction_id}`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.squirrel-girl-preview+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const deleted = response.status === 204 + + return { + success: deleted, + output: { + content: deleted + ? `Successfully deleted reaction ${params?.reaction_id}` + : `Failed to delete reaction ${params?.reaction_id}`, + metadata: { + deleted, + reaction_id: params?.reaction_id ?? 0, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Delete operation metadata', + properties: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + reaction_id: { type: 'number', description: 'The deleted reaction ID' }, + }, + }, + }, +} + +export const deleteIssueReactionV2Tool: ToolConfig = { + id: 'github_delete_issue_reaction_v2', + name: deleteIssueReactionTool.name, + description: deleteIssueReactionTool.description, + version: '2.0.0', + params: deleteIssueReactionTool.params, + request: deleteIssueReactionTool.request, + + transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 + return { + success: deleted, + output: { + deleted, + reaction_id: params?.reaction_id ?? 0, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + reaction_id: { type: 'number', description: 'The deleted reaction ID' }, + }, +} diff --git a/apps/sim/tools/github/delete_milestone.ts b/apps/sim/tools/github/delete_milestone.ts new file mode 100644 index 0000000000..cbe44f634d --- /dev/null +++ b/apps/sim/tools/github/delete_milestone.ts @@ -0,0 +1,118 @@ +import type { ToolConfig } from '@/tools/types' + +interface DeleteMilestoneParams { + owner: string + repo: string + milestone_number: number + apiKey: string +} + +interface DeleteMilestoneResponse { + success: boolean + output: { + content: string + metadata: { + deleted: boolean + milestone_number: number + } + } +} + +export const deleteMilestoneTool: ToolConfig = { + id: 'github_delete_milestone', + name: 'GitHub Delete Milestone', + description: 'Delete a milestone from a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + milestone_number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Milestone number to delete', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/milestones/${params.milestone_number}`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const deleted = response.status === 204 + + return { + success: deleted, + output: { + content: deleted + ? `Successfully deleted milestone #${params?.milestone_number}` + : `Failed to delete milestone #${params?.milestone_number}`, + metadata: { + deleted, + milestone_number: params?.milestone_number ?? 0, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Delete operation metadata', + properties: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + milestone_number: { type: 'number', description: 'The deleted milestone number' }, + }, + }, + }, +} + +export const deleteMilestoneV2Tool: ToolConfig = { + id: 'github_delete_milestone_v2', + name: deleteMilestoneTool.name, + description: deleteMilestoneTool.description, + version: '2.0.0', + params: deleteMilestoneTool.params, + request: deleteMilestoneTool.request, + + transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 + return { + success: deleted, + output: { + deleted, + milestone_number: params?.milestone_number ?? 0, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether deletion succeeded' }, + milestone_number: { type: 'number', description: 'The deleted milestone number' }, + }, +} diff --git a/apps/sim/tools/github/fork_gist.ts b/apps/sim/tools/github/fork_gist.ts new file mode 100644 index 0000000000..8ecafafb28 --- /dev/null +++ b/apps/sim/tools/github/fork_gist.ts @@ -0,0 +1,131 @@ +import type { ToolConfig } from '@/tools/types' + +interface ForkGistParams { + gist_id: string + apiKey: string +} + +interface ForkGistResponse { + success: boolean + output: { + content: string + metadata: { + id: string + html_url: string + git_pull_url: string + description: string | null + public: boolean + created_at: string + owner: { login: string } + files: string[] + } + } +} + +export const forkGistTool: ToolConfig = { + id: 'github_fork_gist', + name: 'GitHub Fork Gist', + description: 'Fork a gist to create your own copy', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID to fork', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}/forks`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const files = Object.keys(data.files ?? {}) + + const content = `Forked gist: ${data.html_url} +Description: ${data.description ?? 'No description'} +Files: ${files.join(', ')}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + html_url: data.html_url, + git_pull_url: data.git_pull_url, + description: data.description ?? null, + public: data.public, + created_at: data.created_at, + owner: { login: data.owner?.login ?? 'unknown' }, + files, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Forked gist metadata', + properties: { + id: { type: 'string', description: 'New gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + owner: { type: 'object', description: 'Owner info' }, + files: { type: 'array', description: 'File names' }, + }, + }, + }, +} + +export const forkGistV2Tool: ToolConfig = { + id: 'github_fork_gist_v2', + name: forkGistTool.name, + description: forkGistTool.description, + version: '2.0.0', + params: forkGistTool.params, + request: forkGistTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + files: data.files ?? {}, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'New gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + owner: { type: 'object', description: 'Owner info' }, + files: { type: 'object', description: 'Files' }, + }, +} diff --git a/apps/sim/tools/github/fork_repo.ts b/apps/sim/tools/github/fork_repo.ts new file mode 100644 index 0000000000..a16b1f5875 --- /dev/null +++ b/apps/sim/tools/github/fork_repo.ts @@ -0,0 +1,179 @@ +import type { ToolConfig } from '@/tools/types' + +interface ForkRepoParams { + owner: string + repo: string + organization?: string + name?: string + default_branch_only?: boolean + apiKey: string +} + +interface ForkRepoResponse { + success: boolean + output: { + content: string + metadata: { + id: number + full_name: string + html_url: string + clone_url: string + ssh_url: string + default_branch: string + fork: boolean + parent: { full_name: string; html_url: string } + owner: { login: string } + created_at: string + } + } +} + +export const forkRepoTool: ToolConfig = { + id: 'github_fork_repo', + name: 'GitHub Fork Repository', + description: 'Fork a repository to your account or an organization', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner to fork from', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name to fork', + }, + organization: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization to fork into (omit to fork to your account)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom name for the forked repository', + }, + default_branch_only: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Only fork the default branch (default: false)', + default: false, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/repos/${params.owner}/${params.repo}/forks`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const body: Record = {} + if (params.organization) body.organization = params.organization + if (params.name) body.name = params.name + if (params.default_branch_only !== undefined) + body.default_branch_only = params.default_branch_only + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + const content = `Forked repository: ${data.html_url} +Forked from: ${data.parent?.full_name ?? 'unknown'} +Clone URL: ${data.clone_url} +Default branch: ${data.default_branch}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + full_name: data.full_name, + html_url: data.html_url, + clone_url: data.clone_url, + ssh_url: data.ssh_url, + default_branch: data.default_branch, + fork: data.fork, + parent: { + full_name: data.parent?.full_name ?? '', + html_url: data.parent?.html_url ?? '', + }, + owner: { login: data.owner?.login ?? 'unknown' }, + created_at: data.created_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Forked repository metadata', + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + html_url: { type: 'string', description: 'Web URL' }, + clone_url: { type: 'string', description: 'HTTPS clone URL' }, + ssh_url: { type: 'string', description: 'SSH clone URL' }, + default_branch: { type: 'string', description: 'Default branch' }, + fork: { type: 'boolean', description: 'Is a fork' }, + parent: { type: 'object', description: 'Parent repository' }, + owner: { type: 'object', description: 'Owner info' }, + created_at: { type: 'string', description: 'Creation date' }, + }, + }, + }, +} + +export const forkRepoV2Tool: ToolConfig = { + id: 'github_fork_repo_v2', + name: forkRepoTool.name, + description: forkRepoTool.description, + version: '2.0.0', + params: forkRepoTool.params, + request: forkRepoTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + parent: data.parent ?? null, + source: data.source ?? null, + }, + } + }, + + outputs: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + html_url: { type: 'string', description: 'Web URL' }, + clone_url: { type: 'string', description: 'Clone URL' }, + ssh_url: { type: 'string', description: 'SSH URL' }, + default_branch: { type: 'string', description: 'Default branch' }, + fork: { type: 'boolean', description: 'Is a fork' }, + parent: { type: 'object', description: 'Parent repository', optional: true }, + owner: { type: 'object', description: 'Owner' }, + }, +} diff --git a/apps/sim/tools/github/get_commit.ts b/apps/sim/tools/github/get_commit.ts new file mode 100644 index 0000000000..b71ee941e1 --- /dev/null +++ b/apps/sim/tools/github/get_commit.ts @@ -0,0 +1,202 @@ +import type { ToolConfig } from '@/tools/types' + +interface GetCommitParams { + owner: string + repo: string + ref: string + apiKey: string +} + +interface GetCommitResponse { + success: boolean + output: { + content: string + metadata: { + sha: string + html_url: string + message: string + author: { name: string; email: string; date: string; login?: string } + committer: { name: string; email: string; date: string; login?: string } + stats: { additions: number; deletions: number; total: number } + files: Array<{ + filename: string + status: string + additions: number + deletions: number + changes: number + patch?: string + }> + parents: Array<{ sha: string; html_url: string }> + } + } +} + +export const getCommitTool: ToolConfig = { + id: 'github_get_commit', + name: 'GitHub Get Commit', + description: 'Get detailed information about a specific commit including files changed and stats', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + ref: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Commit SHA, branch name, or tag name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/commits/${params.ref}`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const files = (data.files ?? []).map((f: any) => ({ + filename: f.filename, + status: f.status, + additions: f.additions, + deletions: f.deletions, + changes: f.changes, + patch: f.patch, + })) + + const metadata = { + sha: data.sha, + html_url: data.html_url, + message: data.commit.message, + author: { + name: data.commit.author.name, + email: data.commit.author.email, + date: data.commit.author.date, + login: data.author?.login, + }, + committer: { + name: data.commit.committer.name, + email: data.commit.committer.email, + date: data.commit.committer.date, + login: data.committer?.login, + }, + stats: data.stats ?? { additions: 0, deletions: 0, total: 0 }, + files, + parents: data.parents.map((p: any) => ({ sha: p.sha, html_url: p.html_url })), + } + + const content = `Commit ${data.sha.substring(0, 7)} +Message: ${data.commit.message.split('\n')[0]} +Author: ${metadata.author.login ?? metadata.author.name} (${metadata.author.date}) +Stats: +${metadata.stats.additions} -${metadata.stats.deletions} (${files.length} files) +${data.html_url} + +Files changed: +${files.map((f: any) => ` ${f.status}: ${f.filename} (+${f.additions} -${f.deletions})`).join('\n')}` + + return { + success: true, + output: { + content, + metadata, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable commit details' }, + metadata: { + type: 'object', + description: 'Commit metadata', + properties: { + sha: { type: 'string', description: 'Full commit SHA' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Author info' }, + committer: { type: 'object', description: 'Committer info' }, + stats: { + type: 'object', + description: 'Change stats', + properties: { + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + total: { type: 'number', description: 'Total changes' }, + }, + }, + files: { + type: 'array', + description: 'Changed files', + items: { + type: 'object', + properties: { + filename: { type: 'string', description: 'File path' }, + status: { type: 'string', description: 'Change type' }, + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + changes: { type: 'number', description: 'Total changes' }, + patch: { type: 'string', description: 'Diff patch', optional: true }, + }, + }, + }, + parents: { type: 'array', description: 'Parent commits' }, + }, + }, + }, +} + +export const getCommitV2Tool: ToolConfig = { + id: 'github_get_commit_v2', + name: getCommitTool.name, + description: getCommitTool.description, + version: '2.0.0', + params: getCommitTool.params, + request: getCommitTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + author: data.author ?? null, + committer: data.committer ?? null, + stats: data.stats ?? null, + files: data.files ?? [], + }, + } + }, + + outputs: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + commit: { type: 'object', description: 'Commit data' }, + author: { type: 'object', description: 'GitHub user', optional: true }, + committer: { type: 'object', description: 'GitHub user', optional: true }, + stats: { type: 'object', description: 'Change stats', optional: true }, + files: { type: 'array', description: 'Changed files' }, + parents: { type: 'array', description: 'Parent commits' }, + }, +} diff --git a/apps/sim/tools/github/get_gist.ts b/apps/sim/tools/github/get_gist.ts new file mode 100644 index 0000000000..67dee27ba3 --- /dev/null +++ b/apps/sim/tools/github/get_gist.ts @@ -0,0 +1,177 @@ +import type { ToolConfig } from '@/tools/types' + +interface GetGistParams { + gist_id: string + apiKey: string +} + +interface GetGistResponse { + success: boolean + output: { + content: string + metadata: { + id: string + html_url: string + git_pull_url: string + git_push_url: string + description: string | null + public: boolean + created_at: string + updated_at: string + files: Record< + string, + { filename: string; type: string; language: string | null; size: number; content: string } + > + owner: { login: string } + comments: number + forks_url: string + commits_url: string + } + } +} + +export const getGistTool: ToolConfig = { + id: 'github_get_gist', + name: 'GitHub Get Gist', + description: 'Get a gist by ID including its file contents', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const files: Record< + string, + { filename: string; type: string; language: string | null; size: number; content: string } + > = {} + for (const [key, value] of Object.entries(data.files ?? {})) { + const file = value as any + files[key] = { + filename: file.filename, + type: file.type, + language: file.language ?? null, + size: file.size, + content: file.content ?? '', + } + } + + const metadata = { + id: data.id, + html_url: data.html_url, + git_pull_url: data.git_pull_url, + git_push_url: data.git_push_url, + description: data.description ?? null, + public: data.public, + created_at: data.created_at, + updated_at: data.updated_at, + files, + owner: { login: data.owner?.login ?? 'unknown' }, + comments: data.comments ?? 0, + forks_url: data.forks_url, + commits_url: data.commits_url, + } + + const fileList = Object.entries(files) + .map(([name, f]) => `${name} (${f.language ?? 'unknown'}, ${f.size} bytes)`) + .join(', ') + + const content = `Gist: ${data.html_url} +Description: ${data.description ?? 'No description'} +Public: ${data.public} | Comments: ${data.comments ?? 0} +Owner: ${data.owner?.login ?? 'unknown'} +Files: ${fileList} + +${Object.entries(files) + .map(([name, f]) => `--- ${name} ---\n${f.content}`) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable gist with file contents' }, + metadata: { + type: 'object', + description: 'Gist metadata', + properties: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Files with content' }, + owner: { type: 'object', description: 'Owner info' }, + comments: { type: 'number', description: 'Comment count' }, + forks_url: { type: 'string', description: 'Forks URL' }, + commits_url: { type: 'string', description: 'Commits URL' }, + }, + }, + }, +} + +export const getGistV2Tool: ToolConfig = { + id: 'github_get_gist_v2', + name: getGistTool.name, + description: getGistTool.description, + version: '2.0.0', + params: getGistTool.params, + request: getGistTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + files: data.files ?? {}, + comments: data.comments ?? 0, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Files with content' }, + owner: { type: 'object', description: 'Owner info' }, + comments: { type: 'number', description: 'Comment count' }, + }, +} diff --git a/apps/sim/tools/github/get_milestone.ts b/apps/sim/tools/github/get_milestone.ts new file mode 100644 index 0000000000..e8b24ce4ea --- /dev/null +++ b/apps/sim/tools/github/get_milestone.ts @@ -0,0 +1,167 @@ +import type { ToolConfig } from '@/tools/types' + +interface GetMilestoneParams { + owner: string + repo: string + milestone_number: number + apiKey: string +} + +interface GetMilestoneResponse { + success: boolean + output: { + content: string + metadata: { + number: number + title: string + description: string | null + state: string + html_url: string + due_on: string | null + open_issues: number + closed_issues: number + created_at: string + updated_at: string + closed_at: string | null + creator: { login: string } + } + } +} + +export const getMilestoneTool: ToolConfig = { + id: 'github_get_milestone', + name: 'GitHub Get Milestone', + description: 'Get a specific milestone by number', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + milestone_number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Milestone number', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/milestones/${params.milestone_number}`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const progress = + data.open_issues + data.closed_issues > 0 + ? Math.round((data.closed_issues / (data.open_issues + data.closed_issues)) * 100) + : 0 + + const content = `Milestone: ${data.title} (#${data.number}) +State: ${data.state} | Progress: ${progress}% (${data.closed_issues}/${data.open_issues + data.closed_issues} issues) +Due: ${data.due_on ?? 'No due date'} +Description: ${data.description ?? 'No description'} +${data.html_url}` + + return { + success: true, + output: { + content, + metadata: { + number: data.number, + title: data.title, + description: data.description ?? null, + state: data.state, + html_url: data.html_url, + due_on: data.due_on ?? null, + open_issues: data.open_issues, + closed_issues: data.closed_issues, + created_at: data.created_at, + updated_at: data.updated_at, + closed_at: data.closed_at ?? null, + creator: { login: data.creator?.login ?? 'unknown' }, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable milestone details' }, + metadata: { + type: 'object', + description: 'Milestone metadata', + properties: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues count' }, + closed_issues: { type: 'number', description: 'Closed issues count' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + closed_at: { type: 'string', description: 'Close date', optional: true }, + creator: { type: 'object', description: 'Creator info' }, + }, + }, + }, +} + +export const getMilestoneV2Tool: ToolConfig = { + id: 'github_get_milestone_v2', + name: getMilestoneTool.name, + description: getMilestoneTool.description, + version: '2.0.0', + params: getMilestoneTool.params, + request: getMilestoneTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + due_on: data.due_on ?? null, + closed_at: data.closed_at ?? null, + }, + } + }, + + outputs: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + closed_at: { type: 'string', description: 'Close date', optional: true }, + creator: { type: 'object', description: 'Creator' }, + }, +} diff --git a/apps/sim/tools/github/index.ts b/apps/sim/tools/github/index.ts index bd3ff3df89..1623fa79af 100644 --- a/apps/sim/tools/github/index.ts +++ b/apps/sim/tools/github/index.ts @@ -1,27 +1,54 @@ import { addAssigneesTool, addAssigneesV2Tool } from '@/tools/github/add_assignees' import { addLabelsTool, addLabelsV2Tool } from '@/tools/github/add_labels' import { cancelWorkflowRunTool, cancelWorkflowRunV2Tool } from '@/tools/github/cancel_workflow_run' +import { checkStarTool, checkStarV2Tool } from '@/tools/github/check_star' import { closeIssueTool, closeIssueV2Tool } from '@/tools/github/close_issue' import { closePRTool, closePRV2Tool } from '@/tools/github/close_pr' import { commentTool, commentV2Tool } from '@/tools/github/comment' +import { compareCommitsTool, compareCommitsV2Tool } from '@/tools/github/compare_commits' import { createBranchTool, createBranchV2Tool } from '@/tools/github/create_branch' +import { + createCommentReactionTool, + createCommentReactionV2Tool, +} from '@/tools/github/create_comment_reaction' import { createFileTool, createFileV2Tool } from '@/tools/github/create_file' +import { createGistTool, createGistV2Tool } from '@/tools/github/create_gist' import { createIssueTool, createIssueV2Tool } from '@/tools/github/create_issue' +import { + createIssueReactionTool, + createIssueReactionV2Tool, +} from '@/tools/github/create_issue_reaction' +import { createMilestoneTool, createMilestoneV2Tool } from '@/tools/github/create_milestone' import { createPRTool, createPRV2Tool } from '@/tools/github/create_pr' import { createProjectTool, createProjectV2Tool } from '@/tools/github/create_project' import { createReleaseTool, createReleaseV2Tool } from '@/tools/github/create_release' import { deleteBranchTool, deleteBranchV2Tool } from '@/tools/github/delete_branch' import { deleteCommentTool, deleteCommentV2Tool } from '@/tools/github/delete_comment' +import { + deleteCommentReactionTool, + deleteCommentReactionV2Tool, +} from '@/tools/github/delete_comment_reaction' import { deleteFileTool, deleteFileV2Tool } from '@/tools/github/delete_file' +import { deleteGistTool, deleteGistV2Tool } from '@/tools/github/delete_gist' +import { + deleteIssueReactionTool, + deleteIssueReactionV2Tool, +} from '@/tools/github/delete_issue_reaction' +import { deleteMilestoneTool, deleteMilestoneV2Tool } from '@/tools/github/delete_milestone' import { deleteProjectTool, deleteProjectV2Tool } from '@/tools/github/delete_project' import { deleteReleaseTool, deleteReleaseV2Tool } from '@/tools/github/delete_release' +import { forkGistTool, forkGistV2Tool } from '@/tools/github/fork_gist' +import { forkRepoTool, forkRepoV2Tool } from '@/tools/github/fork_repo' import { getBranchTool, getBranchV2Tool } from '@/tools/github/get_branch' import { getBranchProtectionTool, getBranchProtectionV2Tool, } from '@/tools/github/get_branch_protection' +import { getCommitTool, getCommitV2Tool } from '@/tools/github/get_commit' import { getFileContentTool, getFileContentV2Tool } from '@/tools/github/get_file_content' +import { getGistTool, getGistV2Tool } from '@/tools/github/get_gist' import { getIssueTool, getIssueV2Tool } from '@/tools/github/get_issue' +import { getMilestoneTool, getMilestoneV2Tool } from '@/tools/github/get_milestone' import { getPRFilesTool, getPRFilesV2Tool } from '@/tools/github/get_pr_files' import { getProjectTool, getProjectV2Tool } from '@/tools/github/get_project' import { getReleaseTool, getReleaseV2Tool } from '@/tools/github/get_release' @@ -31,12 +58,17 @@ import { getWorkflowRunTool, getWorkflowRunV2Tool } from '@/tools/github/get_wor import { issueCommentTool, issueCommentV2Tool } from '@/tools/github/issue_comment' import { latestCommitTool, latestCommitV2Tool } from '@/tools/github/latest_commit' import { listBranchesTool, listBranchesV2Tool } from '@/tools/github/list_branches' +import { listCommitsTool, listCommitsV2Tool } from '@/tools/github/list_commits' +import { listForksTool, listForksV2Tool } from '@/tools/github/list_forks' +import { listGistsTool, listGistsV2Tool } from '@/tools/github/list_gists' import { listIssueCommentsTool, listIssueCommentsV2Tool } from '@/tools/github/list_issue_comments' import { listIssuesTool, listIssuesV2Tool } from '@/tools/github/list_issues' +import { listMilestonesTool, listMilestonesV2Tool } from '@/tools/github/list_milestones' import { listPRCommentsTool, listPRCommentsV2Tool } from '@/tools/github/list_pr_comments' import { listProjectsTool, listProjectsV2Tool } from '@/tools/github/list_projects' import { listPRsTool, listPRsV2Tool } from '@/tools/github/list_prs' import { listReleasesTool, listReleasesV2Tool } from '@/tools/github/list_releases' +import { listStargazersTool, listStargazersV2Tool } from '@/tools/github/list_stargazers' import { listWorkflowRunsTool, listWorkflowRunsV2Tool } from '@/tools/github/list_workflow_runs' import { listWorkflowsTool, listWorkflowsV2Tool } from '@/tools/github/list_workflows' import { mergePRTool, mergePRV2Tool } from '@/tools/github/merge_pr' @@ -45,20 +77,38 @@ import { removeLabelTool, removeLabelV2Tool } from '@/tools/github/remove_label' import { repoInfoTool, repoInfoV2Tool } from '@/tools/github/repo_info' import { requestReviewersTool, requestReviewersV2Tool } from '@/tools/github/request_reviewers' import { rerunWorkflowTool, rerunWorkflowV2Tool } from '@/tools/github/rerun_workflow' +import { searchCodeTool, searchCodeV2Tool } from '@/tools/github/search_code' +import { searchCommitsTool, searchCommitsV2Tool } from '@/tools/github/search_commits' +import { searchIssuesTool, searchIssuesV2Tool } from '@/tools/github/search_issues' +import { searchReposTool, searchReposV2Tool } from '@/tools/github/search_repos' +import { searchUsersTool, searchUsersV2Tool } from '@/tools/github/search_users' +import { starGistTool, starGistV2Tool } from '@/tools/github/star_gist' +import { starRepoTool, starRepoV2Tool } from '@/tools/github/star_repo' import { triggerWorkflowTool, triggerWorkflowV2Tool } from '@/tools/github/trigger_workflow' +import { unstarGistTool, unstarGistV2Tool } from '@/tools/github/unstar_gist' +import { unstarRepoTool, unstarRepoV2Tool } from '@/tools/github/unstar_repo' import { updateBranchProtectionTool, updateBranchProtectionV2Tool, } from '@/tools/github/update_branch_protection' import { updateCommentTool, updateCommentV2Tool } from '@/tools/github/update_comment' import { updateFileTool, updateFileV2Tool } from '@/tools/github/update_file' +import { updateGistTool, updateGistV2Tool } from '@/tools/github/update_gist' import { updateIssueTool, updateIssueV2Tool } from '@/tools/github/update_issue' +import { updateMilestoneTool, updateMilestoneV2Tool } from '@/tools/github/update_milestone' import { updatePRTool, updatePRV2Tool } from '@/tools/github/update_pr' import { updateProjectTool, updateProjectV2Tool } from '@/tools/github/update_project' import { updateReleaseTool, updateReleaseV2Tool } from '@/tools/github/update_release' +// Existing exports +export const githubAddAssigneesTool = addAssigneesTool +export const githubAddAssigneesV2Tool = addAssigneesV2Tool +export const githubAddLabelsTool = addLabelsTool +export const githubAddLabelsV2Tool = addLabelsV2Tool export const githubCancelWorkflowRunTool = cancelWorkflowRunTool export const githubCancelWorkflowRunV2Tool = cancelWorkflowRunV2Tool +export const githubCloseIssueTool = closeIssueTool +export const githubCloseIssueV2Tool = closeIssueV2Tool export const githubClosePRTool = closePRTool export const githubClosePRV2Tool = closePRV2Tool export const githubCommentTool = commentTool @@ -67,6 +117,8 @@ export const githubCreateBranchTool = createBranchTool export const githubCreateBranchV2Tool = createBranchV2Tool export const githubCreateFileTool = createFileTool export const githubCreateFileV2Tool = createFileV2Tool +export const githubCreateIssueTool = createIssueTool +export const githubCreateIssueV2Tool = createIssueV2Tool export const githubCreatePRTool = createPRTool export const githubCreatePRV2Tool = createPRV2Tool export const githubCreateProjectTool = createProjectTool @@ -89,6 +141,8 @@ export const githubGetBranchProtectionTool = getBranchProtectionTool export const githubGetBranchProtectionV2Tool = getBranchProtectionV2Tool export const githubGetFileContentTool = getFileContentTool export const githubGetFileContentV2Tool = getFileContentV2Tool +export const githubGetIssueTool = getIssueTool +export const githubGetIssueV2Tool = getIssueV2Tool export const githubGetPRFilesTool = getPRFilesTool export const githubGetPRFilesV2Tool = getPRFilesV2Tool export const githubGetProjectTool = getProjectTool @@ -109,6 +163,8 @@ export const githubListBranchesTool = listBranchesTool export const githubListBranchesV2Tool = listBranchesV2Tool export const githubListIssueCommentsTool = listIssueCommentsTool export const githubListIssueCommentsV2Tool = listIssueCommentsV2Tool +export const githubListIssuesTool = listIssuesTool +export const githubListIssuesV2Tool = listIssuesV2Tool export const githubListPRCommentsTool = listPRCommentsTool export const githubListPRCommentsV2Tool = listPRCommentsV2Tool export const githubListPRsTool = listPRsTool @@ -125,6 +181,8 @@ export const githubMergePRTool = mergePRTool export const githubMergePRV2Tool = mergePRV2Tool export const githubPrTool = prTool export const githubPrV2Tool = prV2Tool +export const githubRemoveLabelTool = removeLabelTool +export const githubRemoveLabelV2Tool = removeLabelV2Tool export const githubRepoInfoTool = repoInfoTool export const githubRepoInfoV2Tool = repoInfoV2Tool export const githubRequestReviewersTool = requestReviewersTool @@ -139,25 +197,87 @@ export const githubUpdateCommentTool = updateCommentTool export const githubUpdateCommentV2Tool = updateCommentV2Tool export const githubUpdateFileTool = updateFileTool export const githubUpdateFileV2Tool = updateFileV2Tool +export const githubUpdateIssueTool = updateIssueTool +export const githubUpdateIssueV2Tool = updateIssueV2Tool export const githubUpdatePRTool = updatePRTool export const githubUpdatePRV2Tool = updatePRV2Tool export const githubUpdateProjectTool = updateProjectTool export const githubUpdateProjectV2Tool = updateProjectV2Tool export const githubUpdateReleaseTool = updateReleaseTool export const githubUpdateReleaseV2Tool = updateReleaseV2Tool -export const githubAddAssigneesTool = addAssigneesTool -export const githubAddAssigneesV2Tool = addAssigneesV2Tool -export const githubAddLabelsTool = addLabelsTool -export const githubAddLabelsV2Tool = addLabelsV2Tool -export const githubCloseIssueTool = closeIssueTool -export const githubCloseIssueV2Tool = closeIssueV2Tool -export const githubCreateIssueTool = createIssueTool -export const githubCreateIssueV2Tool = createIssueV2Tool -export const githubGetIssueTool = getIssueTool -export const githubGetIssueV2Tool = getIssueV2Tool -export const githubListIssuesTool = listIssuesTool -export const githubListIssuesV2Tool = listIssuesV2Tool -export const githubRemoveLabelTool = removeLabelTool -export const githubRemoveLabelV2Tool = removeLabelV2Tool -export const githubUpdateIssueTool = updateIssueTool -export const githubUpdateIssueV2Tool = updateIssueV2Tool + +// New exports - Search tools +export const githubSearchCodeTool = searchCodeTool +export const githubSearchCodeV2Tool = searchCodeV2Tool +export const githubSearchCommitsTool = searchCommitsTool +export const githubSearchCommitsV2Tool = searchCommitsV2Tool +export const githubSearchIssuesTool = searchIssuesTool +export const githubSearchIssuesV2Tool = searchIssuesV2Tool +export const githubSearchReposTool = searchReposTool +export const githubSearchReposV2Tool = searchReposV2Tool +export const githubSearchUsersTool = searchUsersTool +export const githubSearchUsersV2Tool = searchUsersV2Tool + +// New exports - Commit tools +export const githubListCommitsTool = listCommitsTool +export const githubListCommitsV2Tool = listCommitsV2Tool +export const githubGetCommitTool = getCommitTool +export const githubGetCommitV2Tool = getCommitV2Tool +export const githubCompareCommitsTool = compareCommitsTool +export const githubCompareCommitsV2Tool = compareCommitsV2Tool + +// New exports - Gist tools +export const githubCreateGistTool = createGistTool +export const githubCreateGistV2Tool = createGistV2Tool +export const githubGetGistTool = getGistTool +export const githubGetGistV2Tool = getGistV2Tool +export const githubListGistsTool = listGistsTool +export const githubListGistsV2Tool = listGistsV2Tool +export const githubUpdateGistTool = updateGistTool +export const githubUpdateGistV2Tool = updateGistV2Tool +export const githubDeleteGistTool = deleteGistTool +export const githubDeleteGistV2Tool = deleteGistV2Tool +export const githubForkGistTool = forkGistTool +export const githubForkGistV2Tool = forkGistV2Tool +export const githubStarGistTool = starGistTool +export const githubStarGistV2Tool = starGistV2Tool +export const githubUnstarGistTool = unstarGistTool +export const githubUnstarGistV2Tool = unstarGistV2Tool + +// New exports - Fork tools +export const githubForkRepoTool = forkRepoTool +export const githubForkRepoV2Tool = forkRepoV2Tool +export const githubListForksTool = listForksTool +export const githubListForksV2Tool = listForksV2Tool + +// New exports - Milestone tools +export const githubCreateMilestoneTool = createMilestoneTool +export const githubCreateMilestoneV2Tool = createMilestoneV2Tool +export const githubGetMilestoneTool = getMilestoneTool +export const githubGetMilestoneV2Tool = getMilestoneV2Tool +export const githubListMilestonesTool = listMilestonesTool +export const githubListMilestonesV2Tool = listMilestonesV2Tool +export const githubUpdateMilestoneTool = updateMilestoneTool +export const githubUpdateMilestoneV2Tool = updateMilestoneV2Tool +export const githubDeleteMilestoneTool = deleteMilestoneTool +export const githubDeleteMilestoneV2Tool = deleteMilestoneV2Tool + +// New exports - Reaction tools +export const githubCreateIssueReactionTool = createIssueReactionTool +export const githubCreateIssueReactionV2Tool = createIssueReactionV2Tool +export const githubDeleteIssueReactionTool = deleteIssueReactionTool +export const githubDeleteIssueReactionV2Tool = deleteIssueReactionV2Tool +export const githubCreateCommentReactionTool = createCommentReactionTool +export const githubCreateCommentReactionV2Tool = createCommentReactionV2Tool +export const githubDeleteCommentReactionTool = deleteCommentReactionTool +export const githubDeleteCommentReactionV2Tool = deleteCommentReactionV2Tool + +// New exports - Star tools +export const githubStarRepoTool = starRepoTool +export const githubStarRepoV2Tool = starRepoV2Tool +export const githubUnstarRepoTool = unstarRepoTool +export const githubUnstarRepoV2Tool = unstarRepoV2Tool +export const githubCheckStarTool = checkStarTool +export const githubCheckStarV2Tool = checkStarV2Tool +export const githubListStargazersTool = listStargazersTool +export const githubListStargazersV2Tool = listStargazersV2Tool diff --git a/apps/sim/tools/github/list_commits.ts b/apps/sim/tools/github/list_commits.ts new file mode 100644 index 0000000000..4781bfacb7 --- /dev/null +++ b/apps/sim/tools/github/list_commits.ts @@ -0,0 +1,243 @@ +import type { ToolConfig } from '@/tools/types' + +interface ListCommitsParams { + owner: string + repo: string + sha?: string + path?: string + author?: string + committer?: string + since?: string + until?: string + per_page?: number + page?: number + apiKey: string +} + +interface ListCommitsResponse { + success: boolean + output: { + content: string + metadata: { + commits: Array<{ + sha: string + html_url: string + message: string + author: { name: string; email: string; date: string; login?: string } + committer: { name: string; email: string; date: string; login?: string } + }> + count: number + } + } +} + +export const listCommitsTool: ToolConfig = { + id: 'github_list_commits', + name: 'GitHub List Commits', + description: + 'List commits in a repository with optional filtering by SHA, path, author, committer, or date range', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + sha: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'SHA or branch to start listing commits from', + }, + path: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only commits containing this file path', + }, + author: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'GitHub login or email address to filter by author', + }, + committer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'GitHub login or email address to filter by committer', + }, + since: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only commits after this date (ISO 8601 format)', + }, + until: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only commits before this date (ISO 8601 format)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api.github.com/repos/${params.owner}/${params.repo}/commits`) + if (params.sha) url.searchParams.append('sha', params.sha) + if (params.path) url.searchParams.append('path', params.path) + if (params.author) url.searchParams.append('author', params.author) + if (params.committer) url.searchParams.append('committer', params.committer) + if (params.since) url.searchParams.append('since', params.since) + if (params.until) url.searchParams.append('until', params.until) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const commits = data.map((item: any) => ({ + sha: item.sha, + html_url: item.html_url, + message: item.commit.message, + author: { + name: item.commit.author.name, + email: item.commit.author.email, + date: item.commit.author.date, + login: item.author?.login, + }, + committer: { + name: item.commit.committer.name, + email: item.commit.committer.email, + date: item.commit.committer.date, + login: item.committer?.login, + }, + })) + + const content = `Found ${commits.length} commit(s): +${commits + .map( + (c: any) => + `${c.sha.substring(0, 7)} - ${c.message.split('\n')[0]} + Author: ${c.author.login ?? c.author.name} (${c.author.date}) + ${c.html_url}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + commits, + count: commits.length, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable commit list' }, + metadata: { + type: 'object', + description: 'Commits metadata', + properties: { + commits: { + type: 'array', + description: 'Array of commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Author info' }, + committer: { type: 'object', description: 'Committer info' }, + }, + }, + }, + count: { type: 'number', description: 'Number of commits returned' }, + }, + }, + }, +} + +export const listCommitsV2Tool: ToolConfig = { + id: 'github_list_commits_v2', + name: listCommitsTool.name, + description: listCommitsTool.description, + version: '2.0.0', + params: listCommitsTool.params, + request: listCommitsTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + items: data.map((item: any) => ({ + ...item, + author: item.author ?? null, + committer: item.committer ?? null, + })), + count: data.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of commit objects from GitHub API', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + commit: { type: 'object', description: 'Commit data' }, + author: { type: 'object', description: 'GitHub user', optional: true }, + committer: { type: 'object', description: 'GitHub user', optional: true }, + parents: { type: 'array', description: 'Parent commits' }, + }, + }, + }, + count: { type: 'number', description: 'Number of commits returned' }, + }, +} diff --git a/apps/sim/tools/github/list_forks.ts b/apps/sim/tools/github/list_forks.ts new file mode 100644 index 0000000000..c967194d9c --- /dev/null +++ b/apps/sim/tools/github/list_forks.ts @@ -0,0 +1,201 @@ +import type { ToolConfig } from '@/tools/types' + +interface ListForksParams { + owner: string + repo: string + sort?: 'newest' | 'oldest' | 'stargazers' | 'watchers' + per_page?: number + page?: number + apiKey: string +} + +interface ListForksResponse { + success: boolean + output: { + content: string + metadata: { + forks: Array<{ + id: number + full_name: string + html_url: string + owner: { login: string } + stargazers_count: number + forks_count: number + created_at: string + updated_at: string + default_branch: string + }> + count: number + } + } +} + +export const listForksTool: ToolConfig = { + id: 'github_list_forks', + name: 'GitHub List Forks', + description: 'List forks of a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by: newest, oldest, stargazers, watchers (default: newest)', + default: 'newest', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api.github.com/repos/${params.owner}/${params.repo}/forks`) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const forks = data.map((f: any) => ({ + id: f.id, + full_name: f.full_name, + html_url: f.html_url, + owner: { login: f.owner?.login ?? 'unknown' }, + stargazers_count: f.stargazers_count, + forks_count: f.forks_count, + created_at: f.created_at, + updated_at: f.updated_at, + default_branch: f.default_branch, + })) + + const content = `Found ${forks.length} fork(s): +${forks + .map( + (f: any) => + `${f.full_name} ⭐ ${f.stargazers_count} + Owner: ${f.owner.login} + ${f.html_url}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + forks, + count: forks.length, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable fork list' }, + metadata: { + type: 'object', + description: 'Forks metadata', + properties: { + forks: { + type: 'array', + description: 'Array of forks', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + html_url: { type: 'string', description: 'Web URL' }, + owner: { type: 'object', description: 'Owner info' }, + stargazers_count: { type: 'number', description: 'Star count' }, + forks_count: { type: 'number', description: 'Fork count' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + default_branch: { type: 'string', description: 'Default branch' }, + }, + }, + }, + count: { type: 'number', description: 'Number of forks returned' }, + }, + }, + }, +} + +export const listForksV2Tool: ToolConfig = { + id: 'github_list_forks_v2', + name: listForksTool.name, + description: listForksTool.description, + version: '2.0.0', + params: listForksTool.params, + request: listForksTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + items: data, + count: data.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of fork repository objects from GitHub API', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + html_url: { type: 'string', description: 'Web URL' }, + owner: { type: 'object', description: 'Owner' }, + stargazers_count: { type: 'number', description: 'Stars' }, + forks_count: { type: 'number', description: 'Forks' }, + }, + }, + }, + count: { type: 'number', description: 'Number of forks returned' }, + }, +} diff --git a/apps/sim/tools/github/list_gists.ts b/apps/sim/tools/github/list_gists.ts new file mode 100644 index 0000000000..40d54ebac5 --- /dev/null +++ b/apps/sim/tools/github/list_gists.ts @@ -0,0 +1,201 @@ +import type { ToolConfig } from '@/tools/types' + +interface ListGistsParams { + username?: string + since?: string + per_page?: number + page?: number + apiKey: string +} + +interface ListGistsResponse { + success: boolean + output: { + content: string + metadata: { + gists: Array<{ + id: string + html_url: string + description: string | null + public: boolean + created_at: string + updated_at: string + files: string[] + owner: { login: string } + comments: number + }> + count: number + } + } +} + +export const listGistsTool: ToolConfig = { + id: 'github_list_gists', + name: 'GitHub List Gists', + description: 'List gists for a user or the authenticated user', + version: '1.0.0', + + params: { + username: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "GitHub username (omit for authenticated user's gists)", + }, + since: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only gists updated after this time (ISO 8601)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.username + ? `https://api.github.com/users/${params.username}/gists` + : 'https://api.github.com/gists' + const url = new URL(baseUrl) + if (params.since) url.searchParams.append('since', params.since) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const gists = data.map((g: any) => ({ + id: g.id, + html_url: g.html_url, + description: g.description ?? null, + public: g.public, + created_at: g.created_at, + updated_at: g.updated_at, + files: Object.keys(g.files ?? {}), + owner: { login: g.owner?.login ?? 'unknown' }, + comments: g.comments ?? 0, + })) + + const content = `Found ${gists.length} gist(s): +${gists + .map( + (g: any) => + `${g.id} - ${g.description ?? 'No description'} (${g.public ? 'public' : 'secret'}) + Files: ${g.files.join(', ')} + ${g.html_url}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + gists, + count: gists.length, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable gist list' }, + metadata: { + type: 'object', + description: 'Gists metadata', + properties: { + gists: { + type: 'array', + description: 'Array of gists', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'array', description: 'File names' }, + owner: { type: 'object', description: 'Owner info' }, + comments: { type: 'number', description: 'Comment count' }, + }, + }, + }, + count: { type: 'number', description: 'Number of gists returned' }, + }, + }, + }, +} + +export const listGistsV2Tool: ToolConfig = { + id: 'github_list_gists_v2', + name: listGistsTool.name, + description: listGistsTool.description, + version: '2.0.0', + params: listGistsTool.params, + request: listGistsTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + items: data.map((g: any) => ({ + ...g, + description: g.description ?? null, + files: g.files ?? {}, + comments: g.comments ?? 0, + })), + count: data.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of gist objects from GitHub API', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + files: { type: 'object', description: 'Files' }, + owner: { type: 'object', description: 'Owner' }, + }, + }, + }, + count: { type: 'number', description: 'Number of gists returned' }, + }, +} diff --git a/apps/sim/tools/github/list_milestones.ts b/apps/sim/tools/github/list_milestones.ts new file mode 100644 index 0000000000..052deabfba --- /dev/null +++ b/apps/sim/tools/github/list_milestones.ts @@ -0,0 +1,226 @@ +import type { ToolConfig } from '@/tools/types' + +interface ListMilestonesParams { + owner: string + repo: string + state?: 'open' | 'closed' | 'all' + sort?: 'due_on' | 'completeness' + direction?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface ListMilestonesResponse { + success: boolean + output: { + content: string + metadata: { + milestones: Array<{ + number: number + title: string + description: string | null + state: string + html_url: string + due_on: string | null + open_issues: number + closed_issues: number + }> + count: number + } + } +} + +export const listMilestonesTool: ToolConfig = { + id: 'github_list_milestones', + name: 'GitHub List Milestones', + description: 'List milestones in a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by state: open, closed, all (default: open)', + default: 'open', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by: due_on or completeness (default: due_on)', + default: 'due_on', + }, + direction: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction: asc or desc (default: asc)', + default: 'asc', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api.github.com/repos/${params.owner}/${params.repo}/milestones`) + if (params.state) url.searchParams.append('state', params.state) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.direction) url.searchParams.append('direction', params.direction) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const milestones = data.map((m: any) => { + const total = m.open_issues + m.closed_issues + const progress = total > 0 ? Math.round((m.closed_issues / total) * 100) : 0 + return { + number: m.number, + title: m.title, + description: m.description ?? null, + state: m.state, + html_url: m.html_url, + due_on: m.due_on ?? null, + open_issues: m.open_issues, + closed_issues: m.closed_issues, + progress, + } + }) + + const content = `Found ${milestones.length} milestone(s): +${milestones + .map( + (m: any) => + `#${m.number}: ${m.title} (${m.state}) + Progress: ${m.progress}% (${m.closed_issues}/${m.open_issues + m.closed_issues} issues) + Due: ${m.due_on ?? 'No due date'} + ${m.html_url}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + milestones, + count: milestones.length, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable milestone list' }, + metadata: { + type: 'object', + description: 'Milestones metadata', + properties: { + milestones: { + type: 'array', + description: 'Array of milestones', + items: { + type: 'object', + properties: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + }, + }, + }, + count: { type: 'number', description: 'Number of milestones returned' }, + }, + }, + }, +} + +export const listMilestonesV2Tool: ToolConfig = { + id: 'github_list_milestones_v2', + name: listMilestonesTool.name, + description: listMilestonesTool.description, + version: '2.0.0', + params: listMilestonesTool.params, + request: listMilestonesTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + items: data.map((m: any) => ({ + ...m, + description: m.description ?? null, + due_on: m.due_on ?? null, + })), + count: data.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of milestone objects from GitHub API', + items: { + type: 'object', + properties: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + }, + }, + }, + count: { type: 'number', description: 'Number of milestones returned' }, + }, +} diff --git a/apps/sim/tools/github/list_stargazers.ts b/apps/sim/tools/github/list_stargazers.ts new file mode 100644 index 0000000000..bd0e7306ca --- /dev/null +++ b/apps/sim/tools/github/list_stargazers.ts @@ -0,0 +1,172 @@ +import type { ToolConfig } from '@/tools/types' + +interface ListStargazersParams { + owner: string + repo: string + per_page?: number + page?: number + apiKey: string +} + +interface ListStargazersResponse { + success: boolean + output: { + content: string + metadata: { + stargazers: Array<{ + login: string + id: number + avatar_url: string + html_url: string + type: string + }> + count: number + } + } +} + +export const listStargazersTool: ToolConfig = { + id: 'github_list_stargazers', + name: 'GitHub List Stargazers', + description: 'List users who have starred a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api.github.com/repos/${params.owner}/${params.repo}/stargazers`) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const stargazers = data.map((u: any) => ({ + login: u.login, + id: u.id, + avatar_url: u.avatar_url, + html_url: u.html_url, + type: u.type, + })) + + const content = `Found ${stargazers.length} stargazer(s): +${stargazers.map((u: any) => `@${u.login} (${u.type}) - ${u.html_url}`).join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + stargazers, + count: stargazers.length, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable stargazer list' }, + metadata: { + type: 'object', + description: 'Stargazers metadata', + properties: { + stargazers: { + type: 'array', + description: 'Array of stargazers', + items: { + type: 'object', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + }, + }, + }, + count: { type: 'number', description: 'Number of stargazers returned' }, + }, + }, + }, +} + +export const listStargazersV2Tool: ToolConfig = { + id: 'github_list_stargazers_v2', + name: listStargazersTool.name, + description: listStargazersTool.description, + version: '2.0.0', + params: listStargazersTool.params, + request: listStargazersTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + items: data, + count: data.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of user objects from GitHub API', + items: { + type: 'object', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + }, + }, + }, + count: { type: 'number', description: 'Number of stargazers returned' }, + }, +} diff --git a/apps/sim/tools/github/search_code.ts b/apps/sim/tools/github/search_code.ts new file mode 100644 index 0000000000..8ad41ccbc2 --- /dev/null +++ b/apps/sim/tools/github/search_code.ts @@ -0,0 +1,211 @@ +import type { ToolConfig } from '@/tools/types' + +interface SearchCodeParams { + q: string + sort?: 'indexed' + order?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface SearchCodeResponse { + success: boolean + output: { + content: string + metadata: { + total_count: number + incomplete_results: boolean + items: Array<{ + name: string + path: string + sha: string + html_url: string + repository: { + full_name: string + html_url: string + } + }> + } + } +} + +export const searchCodeTool: ToolConfig = { + id: 'github_search_code', + name: 'GitHub Search Code', + description: + 'Search for code across GitHub repositories. Use qualifiers like repo:owner/name, language:js, path:src, extension:py', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query with optional qualifiers (repo:, language:, path:, extension:, user:, org:)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by indexed date (default: best match)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: desc)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.github.com/search/code') + url.searchParams.append('q', params.q) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.order) url.searchParams.append('order', params.order) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const items = data.items.map((item: any) => ({ + name: item.name, + path: item.path, + sha: item.sha, + html_url: item.html_url, + repository: { + full_name: item.repository.full_name, + html_url: item.repository.html_url, + }, + })) + + const content = `Found ${data.total_count} code result(s)${data.incomplete_results ? ' (incomplete)' : ''}: +${items + .map( + (item: any) => + `- ${item.repository.full_name}/${item.path} + ${item.html_url}` + ) + .join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable search results' }, + metadata: { + type: 'object', + description: 'Search results metadata', + properties: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of code matches', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'File name' }, + path: { type: 'string', description: 'File path' }, + sha: { type: 'string', description: 'Blob SHA' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + repository: { + type: 'object', + description: 'Repository info', + properties: { + full_name: { type: 'string', description: 'Repository full name' }, + html_url: { type: 'string', description: 'Repository URL' }, + }, + }, + }, + }, + }, + }, + }, + }, +} + +export const searchCodeV2Tool: ToolConfig = { + id: 'github_search_code_v2', + name: searchCodeTool.name, + description: searchCodeTool.description, + version: '2.0.0', + params: searchCodeTool.params, + request: searchCodeTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items: data.items.map((item: any) => ({ + ...item, + text_matches: item.text_matches ?? [], + })), + }, + } + }, + + outputs: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of code matches from GitHub API', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'File name' }, + path: { type: 'string', description: 'File path' }, + sha: { type: 'string', description: 'Blob SHA' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + repository: { type: 'object', description: 'Repository object' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/search_commits.ts b/apps/sim/tools/github/search_commits.ts new file mode 100644 index 0000000000..aafbe47f8d --- /dev/null +++ b/apps/sim/tools/github/search_commits.ts @@ -0,0 +1,224 @@ +import type { ToolConfig } from '@/tools/types' + +interface SearchCommitsParams { + q: string + sort?: 'author-date' | 'committer-date' + order?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface SearchCommitsResponse { + success: boolean + output: { + content: string + metadata: { + total_count: number + incomplete_results: boolean + items: Array<{ + sha: string + html_url: string + commit: { + message: string + author: { name: string; email: string; date: string } + committer: { name: string; email: string; date: string } + } + author: { login: string } | null + committer: { login: string } | null + repository: { full_name: string; html_url: string } + }> + } + } +} + +export const searchCommitsTool: ToolConfig = { + id: 'github_search_commits', + name: 'GitHub Search Commits', + description: + 'Search for commits across GitHub. Use qualifiers like repo:owner/name, author:user, committer:user, author-date:>2023-01-01', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query with optional qualifiers (repo:, author:, committer:, author-date:, committer-date:, merge:true/false)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by: author-date or committer-date (default: best match)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: desc)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.github.com/search/commits') + url.searchParams.append('q', params.q) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.order) url.searchParams.append('order', params.order) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.cloak-preview+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const items = data.items.map((item: any) => ({ + sha: item.sha, + html_url: item.html_url, + commit: { + message: item.commit.message, + author: item.commit.author, + committer: item.commit.committer, + }, + author: item.author ? { login: item.author.login } : null, + committer: item.committer ? { login: item.committer.login } : null, + repository: { + full_name: item.repository.full_name, + html_url: item.repository.html_url, + }, + })) + + const content = `Found ${data.total_count} commit(s)${data.incomplete_results ? ' (incomplete)' : ''}: +${items + .map( + (item: any) => + `${item.sha.substring(0, 7)} - ${item.commit.message.split('\n')[0]} + Repository: ${item.repository.full_name} + Author: ${item.author?.login ?? item.commit.author.name} (${item.commit.author.date}) + ${item.html_url}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable search results' }, + metadata: { + type: 'object', + description: 'Search results metadata', + properties: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + commit: { + type: 'object', + description: 'Commit details', + properties: { + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Author info' }, + committer: { type: 'object', description: 'Committer info' }, + }, + }, + author: { type: 'object', description: 'GitHub user (author)', optional: true }, + committer: { type: 'object', description: 'GitHub user (committer)', optional: true }, + repository: { type: 'object', description: 'Repository info' }, + }, + }, + }, + }, + }, + }, +} + +export const searchCommitsV2Tool: ToolConfig = { + id: 'github_search_commits_v2', + name: searchCommitsTool.name, + description: searchCommitsTool.description, + version: '2.0.0', + params: searchCommitsTool.params, + request: searchCommitsTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items: data.items.map((item: any) => ({ + ...item, + author: item.author ?? null, + committer: item.committer ?? null, + })), + }, + } + }, + + outputs: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of commit objects from GitHub API', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + commit: { type: 'object', description: 'Commit data' }, + author: { type: 'object', description: 'GitHub user', optional: true }, + committer: { type: 'object', description: 'GitHub user', optional: true }, + repository: { type: 'object', description: 'Repository' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/search_issues.ts b/apps/sim/tools/github/search_issues.ts new file mode 100644 index 0000000000..9cab3e58bb --- /dev/null +++ b/apps/sim/tools/github/search_issues.ts @@ -0,0 +1,240 @@ +import type { ToolConfig } from '@/tools/types' + +interface SearchIssuesParams { + q: string + sort?: + | 'comments' + | 'reactions' + | 'reactions-+1' + | 'reactions--1' + | 'reactions-smile' + | 'reactions-thinking_face' + | 'reactions-heart' + | 'reactions-tada' + | 'interactions' + | 'created' + | 'updated' + order?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface SearchIssuesResponse { + success: boolean + output: { + content: string + metadata: { + total_count: number + incomplete_results: boolean + items: Array<{ + number: number + title: string + state: string + html_url: string + user: { login: string } + labels: string[] + created_at: string + updated_at: string + comments: number + is_pull_request: boolean + repository_url: string + }> + } + } +} + +export const searchIssuesTool: ToolConfig = { + id: 'github_search_issues', + name: 'GitHub Search Issues', + description: + 'Search for issues and pull requests across GitHub. Use qualifiers like repo:owner/name, is:issue, is:pr, state:open, label:bug, author:user', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query with optional qualifiers (repo:, is:issue, is:pr, state:, label:, author:, assignee:)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Sort by: comments, reactions, created, updated, interactions (default: best match)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: desc)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.github.com/search/issues') + url.searchParams.append('q', params.q) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.order) url.searchParams.append('order', params.order) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const items = data.items.map((item: any) => ({ + number: item.number, + title: item.title, + state: item.state, + html_url: item.html_url, + user: { login: item.user?.login ?? 'unknown' }, + labels: item.labels?.map((l: any) => l.name) ?? [], + created_at: item.created_at, + updated_at: item.updated_at, + comments: item.comments ?? 0, + is_pull_request: !!item.pull_request, + repository_url: item.repository_url, + })) + + const content = `Found ${data.total_count} result(s)${data.incomplete_results ? ' (incomplete)' : ''}: +${items + .map( + (item: any) => + `#${item.number}: "${item.title}" (${item.state}) [${item.is_pull_request ? 'PR' : 'Issue'}] + ${item.html_url} + Labels: ${item.labels.length > 0 ? item.labels.join(', ') : 'none'} | Comments: ${item.comments}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable search results' }, + metadata: { + type: 'object', + description: 'Search results metadata', + properties: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of issues/PRs', + items: { + type: 'object', + properties: { + number: { type: 'number', description: 'Issue/PR number' }, + title: { type: 'string', description: 'Title' }, + state: { type: 'string', description: 'State (open/closed)' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + user: { type: 'object', description: 'Author info' }, + labels: { type: 'array', description: 'Label names' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Last update date' }, + comments: { type: 'number', description: 'Comment count' }, + is_pull_request: { type: 'boolean', description: 'Whether this is a PR' }, + repository_url: { type: 'string', description: 'Repository API URL' }, + }, + }, + }, + }, + }, + }, +} + +export const searchIssuesV2Tool: ToolConfig = { + id: 'github_search_issues_v2', + name: searchIssuesTool.name, + description: searchIssuesTool.description, + version: '2.0.0', + params: searchIssuesTool.params, + request: searchIssuesTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items: data.items.map((item: any) => ({ + ...item, + body: item.body ?? null, + closed_at: item.closed_at ?? null, + milestone: item.milestone ?? null, + labels: item.labels ?? [], + assignees: item.assignees ?? [], + })), + }, + } + }, + + outputs: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of issue/PR objects from GitHub API', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Issue ID' }, + number: { type: 'number', description: 'Issue number' }, + title: { type: 'string', description: 'Title' }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + body: { type: 'string', description: 'Body text', optional: true }, + user: { type: 'object', description: 'Author' }, + labels: { type: 'array', description: 'Labels' }, + assignees: { type: 'array', description: 'Assignees' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Update date' }, + closed_at: { type: 'string', description: 'Close date', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/search_repos.ts b/apps/sim/tools/github/search_repos.ts new file mode 100644 index 0000000000..17655c4856 --- /dev/null +++ b/apps/sim/tools/github/search_repos.ts @@ -0,0 +1,226 @@ +import type { ToolConfig } from '@/tools/types' + +interface SearchReposParams { + q: string + sort?: 'stars' | 'forks' | 'help-wanted-issues' | 'updated' + order?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface SearchReposResponse { + success: boolean + output: { + content: string + metadata: { + total_count: number + incomplete_results: boolean + items: Array<{ + id: number + full_name: string + description: string | null + html_url: string + stargazers_count: number + forks_count: number + language: string | null + topics: string[] + created_at: string + updated_at: string + owner: { login: string } + }> + } + } +} + +export const searchReposTool: ToolConfig = { + id: 'github_search_repos', + name: 'GitHub Search Repositories', + description: + 'Search for repositories across GitHub. Use qualifiers like language:python, stars:>1000, topic:react, user:owner, org:name', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query with optional qualifiers (language:, stars:, forks:, topic:, user:, org:, in:name,description,readme)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by: stars, forks, help-wanted-issues, updated (default: best match)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: desc)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.github.com/search/repositories') + url.searchParams.append('q', params.q) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.order) url.searchParams.append('order', params.order) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const items = data.items.map((item: any) => ({ + id: item.id, + full_name: item.full_name, + description: item.description ?? null, + html_url: item.html_url, + stargazers_count: item.stargazers_count, + forks_count: item.forks_count, + language: item.language ?? null, + topics: item.topics ?? [], + created_at: item.created_at, + updated_at: item.updated_at, + owner: { login: item.owner?.login ?? 'unknown' }, + })) + + const content = `Found ${data.total_count} repository(s)${data.incomplete_results ? ' (incomplete)' : ''}: +${items + .map( + (item: any) => + `${item.full_name} ⭐ ${item.stargazers_count} | 🍴 ${item.forks_count} + ${item.description ?? 'No description'} + ${item.html_url} + Language: ${item.language ?? 'N/A'} | Topics: ${item.topics.length > 0 ? item.topics.join(', ') : 'none'}` + ) + .join('\n\n')}` + + return { + success: true, + output: { + content, + metadata: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable search results' }, + metadata: { + type: 'object', + description: 'Search results metadata', + properties: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of repositories', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + description: { type: 'string', description: 'Description', optional: true }, + html_url: { type: 'string', description: 'GitHub web URL' }, + stargazers_count: { type: 'number', description: 'Star count' }, + forks_count: { type: 'number', description: 'Fork count' }, + language: { type: 'string', description: 'Primary language', optional: true }, + topics: { type: 'array', description: 'Repository topics' }, + created_at: { type: 'string', description: 'Creation date' }, + updated_at: { type: 'string', description: 'Last update date' }, + owner: { type: 'object', description: 'Owner info' }, + }, + }, + }, + }, + }, + }, +} + +export const searchReposV2Tool: ToolConfig = { + id: 'github_search_repos_v2', + name: searchReposTool.name, + description: searchReposTool.description, + version: '2.0.0', + params: searchReposTool.params, + request: searchReposTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items: data.items.map((item: any) => ({ + ...item, + description: item.description ?? null, + language: item.language ?? null, + topics: item.topics ?? [], + license: item.license ?? null, + })), + }, + } + }, + + outputs: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of repository objects from GitHub API', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + description: { type: 'string', description: 'Description', optional: true }, + html_url: { type: 'string', description: 'Web URL' }, + stargazers_count: { type: 'number', description: 'Stars' }, + forks_count: { type: 'number', description: 'Forks' }, + open_issues_count: { type: 'number', description: 'Open issues' }, + language: { type: 'string', description: 'Language', optional: true }, + topics: { type: 'array', description: 'Topics' }, + owner: { type: 'object', description: 'Owner' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/search_users.ts b/apps/sim/tools/github/search_users.ts new file mode 100644 index 0000000000..7e9e1283d4 --- /dev/null +++ b/apps/sim/tools/github/search_users.ts @@ -0,0 +1,193 @@ +import type { ToolConfig } from '@/tools/types' + +interface SearchUsersParams { + q: string + sort?: 'followers' | 'repositories' | 'joined' + order?: 'asc' | 'desc' + per_page?: number + page?: number + apiKey: string +} + +interface SearchUsersResponse { + success: boolean + output: { + content: string + metadata: { + total_count: number + incomplete_results: boolean + items: Array<{ + id: number + login: string + html_url: string + avatar_url: string + type: string + score: number + }> + } + } +} + +export const searchUsersTool: ToolConfig = { + id: 'github_search_users', + name: 'GitHub Search Users', + description: + 'Search for users and organizations on GitHub. Use qualifiers like type:user, type:org, followers:>1000, repos:>10, location:city', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Search query with optional qualifiers (type:user/org, followers:, repos:, location:, language:, created:)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort by: followers, repositories, joined (default: best match)', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: desc)', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (max 100, default: 30)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.github.com/search/users') + url.searchParams.append('q', params.q) + if (params.sort) url.searchParams.append('sort', params.sort) + if (params.order) url.searchParams.append('order', params.order) + if (params.per_page) url.searchParams.append('per_page', String(params.per_page)) + if (params.page) url.searchParams.append('page', String(params.page)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + const items = data.items.map((item: any) => ({ + id: item.id, + login: item.login, + html_url: item.html_url, + avatar_url: item.avatar_url, + type: item.type, + score: item.score, + })) + + const content = `Found ${data.total_count} user(s)/organization(s)${data.incomplete_results ? ' (incomplete)' : ''}: +${items.map((item: any) => `@${item.login} (${item.type}) - ${item.html_url}`).join('\n')}` + + return { + success: true, + output: { + content, + metadata: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable search results' }, + metadata: { + type: 'object', + description: 'Search results metadata', + properties: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of users/orgs', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'User ID' }, + login: { type: 'string', description: 'Username' }, + html_url: { type: 'string', description: 'Profile URL' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + type: { type: 'string', description: 'User or Organization' }, + score: { type: 'number', description: 'Search relevance score' }, + }, + }, + }, + }, + }, + }, +} + +export const searchUsersV2Tool: ToolConfig = { + id: 'github_search_users_v2', + name: searchUsersTool.name, + description: searchUsersTool.description, + version: '2.0.0', + params: searchUsersTool.params, + request: searchUsersTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + total_count: data.total_count, + incomplete_results: data.incomplete_results, + items: data.items, + }, + } + }, + + outputs: { + total_count: { type: 'number', description: 'Total matching results' }, + incomplete_results: { type: 'boolean', description: 'Whether results are incomplete' }, + items: { + type: 'array', + description: 'Array of user objects from GitHub API', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'User ID' }, + login: { type: 'string', description: 'Username' }, + html_url: { type: 'string', description: 'Profile URL' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'Is site admin' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/star_gist.ts b/apps/sim/tools/github/star_gist.ts new file mode 100644 index 0000000000..0d654cd0ef --- /dev/null +++ b/apps/sim/tools/github/star_gist.ts @@ -0,0 +1,104 @@ +import type { ToolConfig } from '@/tools/types' + +interface StarGistParams { + gist_id: string + apiKey: string +} + +interface StarGistResponse { + success: boolean + output: { + content: string + metadata: { + starred: boolean + gist_id: string + } + } +} + +export const starGistTool: ToolConfig = { + id: 'github_star_gist', + name: 'GitHub Star Gist', + description: 'Star a gist', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID to star', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}/star`, + method: 'PUT', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Length': '0', + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const starred = response.status === 204 + + return { + success: starred, + output: { + content: starred + ? `Successfully starred gist ${params?.gist_id}` + : `Failed to star gist ${params?.gist_id}`, + metadata: { + starred, + gist_id: params?.gist_id ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Star operation metadata', + properties: { + starred: { type: 'boolean', description: 'Whether starring succeeded' }, + gist_id: { type: 'string', description: 'The gist ID' }, + }, + }, + }, +} + +export const starGistV2Tool: ToolConfig = { + id: 'github_star_gist_v2', + name: starGistTool.name, + description: starGistTool.description, + version: '2.0.0', + params: starGistTool.params, + request: starGistTool.request, + + transformResponse: async (response: Response, params) => { + const starred = response.status === 204 + return { + success: starred, + output: { + starred, + gist_id: params?.gist_id ?? '', + }, + } + }, + + outputs: { + starred: { type: 'boolean', description: 'Whether starring succeeded' }, + gist_id: { type: 'string', description: 'The gist ID' }, + }, +} diff --git a/apps/sim/tools/github/star_repo.ts b/apps/sim/tools/github/star_repo.ts new file mode 100644 index 0000000000..1bd65ac103 --- /dev/null +++ b/apps/sim/tools/github/star_repo.ts @@ -0,0 +1,116 @@ +import type { ToolConfig } from '@/tools/types' + +interface StarRepoParams { + owner: string + repo: string + apiKey: string +} + +interface StarRepoResponse { + success: boolean + output: { + content: string + metadata: { + starred: boolean + owner: string + repo: string + } + } +} + +export const starRepoTool: ToolConfig = { + id: 'github_star_repo', + name: 'GitHub Star Repository', + description: 'Star a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/user/starred/${params.owner}/${params.repo}`, + method: 'PUT', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Length': '0', + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const starred = response.status === 204 + + return { + success: starred, + output: { + content: starred + ? `Successfully starred ${params?.owner}/${params?.repo}` + : `Failed to star ${params?.owner}/${params?.repo}`, + metadata: { + starred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Star operation metadata', + properties: { + starred: { type: 'boolean', description: 'Whether starring succeeded' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, + }, + }, +} + +export const starRepoV2Tool: ToolConfig = { + id: 'github_star_repo_v2', + name: starRepoTool.name, + description: starRepoTool.description, + version: '2.0.0', + params: starRepoTool.params, + request: starRepoTool.request, + + transformResponse: async (response: Response, params) => { + const starred = response.status === 204 + return { + success: starred, + output: { + starred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + } + }, + + outputs: { + starred: { type: 'boolean', description: 'Whether starring succeeded' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, +} diff --git a/apps/sim/tools/github/unstar_gist.ts b/apps/sim/tools/github/unstar_gist.ts new file mode 100644 index 0000000000..b57fa4e968 --- /dev/null +++ b/apps/sim/tools/github/unstar_gist.ts @@ -0,0 +1,103 @@ +import type { ToolConfig } from '@/tools/types' + +interface UnstarGistParams { + gist_id: string + apiKey: string +} + +interface UnstarGistResponse { + success: boolean + output: { + content: string + metadata: { + unstarred: boolean + gist_id: string + } + } +} + +export const unstarGistTool: ToolConfig = { + id: 'github_unstar_gist', + name: 'GitHub Unstar Gist', + description: 'Unstar a gist', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID to unstar', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}/star`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const unstarred = response.status === 204 + + return { + success: unstarred, + output: { + content: unstarred + ? `Successfully unstarred gist ${params?.gist_id}` + : `Failed to unstar gist ${params?.gist_id}`, + metadata: { + unstarred, + gist_id: params?.gist_id ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Unstar operation metadata', + properties: { + unstarred: { type: 'boolean', description: 'Whether unstarring succeeded' }, + gist_id: { type: 'string', description: 'The gist ID' }, + }, + }, + }, +} + +export const unstarGistV2Tool: ToolConfig = { + id: 'github_unstar_gist_v2', + name: unstarGistTool.name, + description: unstarGistTool.description, + version: '2.0.0', + params: unstarGistTool.params, + request: unstarGistTool.request, + + transformResponse: async (response: Response, params) => { + const unstarred = response.status === 204 + return { + success: unstarred, + output: { + unstarred, + gist_id: params?.gist_id ?? '', + }, + } + }, + + outputs: { + unstarred: { type: 'boolean', description: 'Whether unstarring succeeded' }, + gist_id: { type: 'string', description: 'The gist ID' }, + }, +} diff --git a/apps/sim/tools/github/unstar_repo.ts b/apps/sim/tools/github/unstar_repo.ts new file mode 100644 index 0000000000..5ae47d7b60 --- /dev/null +++ b/apps/sim/tools/github/unstar_repo.ts @@ -0,0 +1,115 @@ +import type { ToolConfig } from '@/tools/types' + +interface UnstarRepoParams { + owner: string + repo: string + apiKey: string +} + +interface UnstarRepoResponse { + success: boolean + output: { + content: string + metadata: { + unstarred: boolean + owner: string + repo: string + } + } +} + +export const unstarRepoTool: ToolConfig = { + id: 'github_unstar_repo', + name: 'GitHub Unstar Repository', + description: 'Remove star from a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/user/starred/${params.owner}/${params.repo}`, + method: 'DELETE', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response, params) => { + const unstarred = response.status === 204 + + return { + success: unstarred, + output: { + content: unstarred + ? `Successfully unstarred ${params?.owner}/${params?.repo}` + : `Failed to unstar ${params?.owner}/${params?.repo}`, + metadata: { + unstarred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Unstar operation metadata', + properties: { + unstarred: { type: 'boolean', description: 'Whether unstarring succeeded' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, + }, + }, +} + +export const unstarRepoV2Tool: ToolConfig = { + id: 'github_unstar_repo_v2', + name: unstarRepoTool.name, + description: unstarRepoTool.description, + version: '2.0.0', + params: unstarRepoTool.params, + request: unstarRepoTool.request, + + transformResponse: async (response: Response, params) => { + const unstarred = response.status === 204 + return { + success: unstarred, + output: { + unstarred, + owner: params?.owner ?? '', + repo: params?.repo ?? '', + }, + } + }, + + outputs: { + unstarred: { type: 'boolean', description: 'Whether unstarring succeeded' }, + owner: { type: 'string', description: 'Repository owner' }, + repo: { type: 'string', description: 'Repository name' }, + }, +} diff --git a/apps/sim/tools/github/update_gist.ts b/apps/sim/tools/github/update_gist.ts new file mode 100644 index 0000000000..4f35fcf860 --- /dev/null +++ b/apps/sim/tools/github/update_gist.ts @@ -0,0 +1,164 @@ +import type { ToolConfig } from '@/tools/types' + +interface UpdateGistParams { + gist_id: string + description?: string + files?: string + apiKey: string +} + +interface UpdateGistResponse { + success: boolean + output: { + content: string + metadata: { + id: string + html_url: string + description: string | null + public: boolean + updated_at: string + files: Record< + string, + { filename: string; type: string; language: string | null; size: number } + > + } + } +} + +export const updateGistTool: ToolConfig = { + id: 'github_update_gist', + name: 'GitHub Update Gist', + description: + 'Update a gist description or files. To delete a file, set its value to null in files object', + version: '1.0.0', + + params: { + gist_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The gist ID to update', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the gist', + }, + files: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object with filenames as keys. Set to null to delete, or provide content to update/add', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => `https://api.github.com/gists/${params.gist_id?.trim()}`, + method: 'PATCH', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const body: Record = {} + if (params.description !== undefined) body.description = params.description + if (params.files) { + body.files = typeof params.files === 'string' ? JSON.parse(params.files) : params.files + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + const files: Record< + string, + { filename: string; type: string; language: string | null; size: number } + > = {} + for (const [key, value] of Object.entries(data.files ?? {})) { + const file = value as any + files[key] = { + filename: file.filename, + type: file.type, + language: file.language ?? null, + size: file.size, + } + } + + const content = `Updated gist: ${data.html_url} +Description: ${data.description ?? 'No description'} +Files: ${Object.keys(files).join(', ')}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + html_url: data.html_url, + description: data.description ?? null, + public: data.public, + updated_at: data.updated_at, + files, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Updated gist metadata', + properties: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Current files' }, + }, + }, + }, +} + +export const updateGistV2Tool: ToolConfig = { + id: 'github_update_gist_v2', + name: updateGistTool.name, + description: updateGistTool.description, + version: '2.0.0', + params: updateGistTool.params, + request: updateGistTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + files: data.files ?? {}, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Gist ID' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + public: { type: 'boolean', description: 'Is public' }, + updated_at: { type: 'string', description: 'Update date' }, + files: { type: 'object', description: 'Current files' }, + }, +} diff --git a/apps/sim/tools/github/update_milestone.ts b/apps/sim/tools/github/update_milestone.ts new file mode 100644 index 0000000000..c04b6452af --- /dev/null +++ b/apps/sim/tools/github/update_milestone.ts @@ -0,0 +1,186 @@ +import type { ToolConfig } from '@/tools/types' + +interface UpdateMilestoneParams { + owner: string + repo: string + milestone_number: number + title?: string + state?: 'open' | 'closed' + description?: string + due_on?: string + apiKey: string +} + +interface UpdateMilestoneResponse { + success: boolean + output: { + content: string + metadata: { + number: number + title: string + description: string | null + state: string + html_url: string + due_on: string | null + open_issues: number + closed_issues: number + updated_at: string + } + } +} + +export const updateMilestoneTool: ToolConfig = { + id: 'github_update_milestone', + name: 'GitHub Update Milestone', + description: 'Update a milestone in a repository', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + milestone_number: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Milestone number to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New milestone title', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New state: open or closed', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description', + }, + due_on: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New due date (ISO 8601 format)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api.github.com/repos/${params.owner}/${params.repo}/milestones/${params.milestone_number}`, + method: 'PATCH', + headers: (params) => ({ + Accept: 'application/vnd.github.v3+json', + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const body: Record = {} + if (params.title !== undefined) body.title = params.title + if (params.state !== undefined) body.state = params.state + if (params.description !== undefined) body.description = params.description + if (params.due_on !== undefined) body.due_on = params.due_on + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + const content = `Updated milestone: ${data.title} (#${data.number}) +State: ${data.state} +Due: ${data.due_on ?? 'No due date'} +${data.html_url}` + + return { + success: true, + output: { + content, + metadata: { + number: data.number, + title: data.title, + description: data.description ?? null, + state: data.state, + html_url: data.html_url, + due_on: data.due_on ?? null, + open_issues: data.open_issues, + closed_issues: data.closed_issues, + updated_at: data.updated_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable result' }, + metadata: { + type: 'object', + description: 'Updated milestone metadata', + properties: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + updated_at: { type: 'string', description: 'Update date' }, + }, + }, + }, +} + +export const updateMilestoneV2Tool: ToolConfig = { + id: 'github_update_milestone_v2', + name: updateMilestoneTool.name, + description: updateMilestoneTool.description, + version: '2.0.0', + params: updateMilestoneTool.params, + request: updateMilestoneTool.request, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ...data, + description: data.description ?? null, + due_on: data.due_on ?? null, + }, + } + }, + + outputs: { + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description', optional: true }, + state: { type: 'string', description: 'State' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + open_issues: { type: 'number', description: 'Open issues' }, + closed_issues: { type: 'number', description: 'Closed issues' }, + }, +} diff --git a/apps/sim/tools/gitlab/cancel_pipeline.ts b/apps/sim/tools/gitlab/cancel_pipeline.ts index ef48a5bb7d..62b9e096b9 100644 --- a/apps/sim/tools/gitlab/cancel_pipeline.ts +++ b/apps/sim/tools/gitlab/cancel_pipeline.ts @@ -14,16 +14,19 @@ export const gitlabCancelPipelineTool: ToolConfig< accessToken: { type: 'string', required: true, + visibility: 'user-only', description: 'GitLab Personal Access Token', }, projectId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Project ID or URL-encoded path', }, pipelineId: { type: 'number', required: true, + visibility: 'user-or-llm', description: 'Pipeline ID', }, }, diff --git a/apps/sim/tools/gitlab/create_issue.ts b/apps/sim/tools/gitlab/create_issue.ts index 417f614bfd..6a03e9ff97 100644 --- a/apps/sim/tools/gitlab/create_issue.ts +++ b/apps/sim/tools/gitlab/create_issue.ts @@ -12,46 +12,55 @@ export const gitlabCreateIssueTool: ToolConfig = { + id: 'google_calendar_delete', + name: 'Google Calendar Delete Event', + description: 'Delete an event from Google Calendar', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Calendar ID (defaults to primary)', + }, + eventId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Event ID to delete', + }, + sendUpdates: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'How to send updates to attendees: all, externalOnly, or none', + }, + }, + + request: { + url: (params: GoogleCalendarDeleteParams) => { + const calendarId = params.calendarId || 'primary' + const queryParams = new URLSearchParams() + + if (params.sendUpdates !== undefined) { + queryParams.append('sendUpdates', params.sendUpdates) + } + + const queryString = queryParams.toString() + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}` + }, + method: 'DELETE', + headers: (params: GoogleCalendarDeleteParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + // DELETE returns 204 No Content on success + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + content: `Event successfully deleted`, + metadata: { + eventId: params?.eventId || '', + deleted: true, + }, + }, + } + } + + const errorData = await response.json() + throw new Error(errorData.error?.message || 'Failed to delete event') + }, + + outputs: { + content: { type: 'string', description: 'Event deletion confirmation message' }, + metadata: { + type: 'json', + description: 'Deletion details including event ID', + }, + }, +} + +interface GoogleCalendarDeleteV2Response { + success: boolean + output: { + eventId: string + deleted: boolean + } +} + +export const deleteV2Tool: ToolConfig = + { + id: 'google_calendar_delete_v2', + name: 'Google Calendar Delete Event', + description: 'Delete an event from Google Calendar. Returns API-aligned fields only.', + version: '2.0.0', + oauth: deleteTool.oauth, + params: deleteTool.params, + request: deleteTool.request, + transformResponse: async (response: Response, params) => { + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + eventId: params?.eventId || '', + deleted: true, + }, + } + } + + const errorData = await response.json() + throw new Error(errorData.error?.message || 'Failed to delete event') + }, + outputs: { + eventId: { type: 'string', description: 'Deleted event ID' }, + deleted: { type: 'boolean', description: 'Whether deletion was successful' }, + }, + } diff --git a/apps/sim/tools/google_calendar/index.ts b/apps/sim/tools/google_calendar/index.ts index 0f3105ae92..2ddfcf55e5 100644 --- a/apps/sim/tools/google_calendar/index.ts +++ b/apps/sim/tools/google_calendar/index.ts @@ -1,17 +1,32 @@ import { createTool, createV2Tool } from '@/tools/google_calendar/create' +import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete' import { getTool, getV2Tool } from '@/tools/google_calendar/get' +import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances' import { inviteTool, inviteV2Tool } from '@/tools/google_calendar/invite' import { listTool, listV2Tool } from '@/tools/google_calendar/list' +import { listCalendarsTool, listCalendarsV2Tool } from '@/tools/google_calendar/list_calendars' +import { moveTool, moveV2Tool } from '@/tools/google_calendar/move' import { quickAddTool, quickAddV2Tool } from '@/tools/google_calendar/quick_add' +import { updateTool, updateV2Tool } from '@/tools/google_calendar/update' export const googleCalendarCreateTool = createTool +export const googleCalendarDeleteTool = deleteTool export const googleCalendarGetTool = getTool +export const googleCalendarInstancesTool = instancesTool export const googleCalendarInviteTool = inviteTool export const googleCalendarListTool = listTool +export const googleCalendarListCalendarsTool = listCalendarsTool +export const googleCalendarMoveTool = moveTool export const googleCalendarQuickAddTool = quickAddTool +export const googleCalendarUpdateTool = updateTool export const googleCalendarCreateV2Tool = createV2Tool +export const googleCalendarDeleteV2Tool = deleteV2Tool export const googleCalendarGetV2Tool = getV2Tool +export const googleCalendarInstancesV2Tool = instancesV2Tool export const googleCalendarInviteV2Tool = inviteV2Tool export const googleCalendarListV2Tool = listV2Tool +export const googleCalendarListCalendarsV2Tool = listCalendarsV2Tool +export const googleCalendarMoveV2Tool = moveV2Tool export const googleCalendarQuickAddV2Tool = quickAddV2Tool +export const googleCalendarUpdateV2Tool = updateV2Tool diff --git a/apps/sim/tools/google_calendar/instances.ts b/apps/sim/tools/google_calendar/instances.ts new file mode 100644 index 0000000000..8bb672ab9c --- /dev/null +++ b/apps/sim/tools/google_calendar/instances.ts @@ -0,0 +1,268 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiEventResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +interface GoogleCalendarInstancesParams { + accessToken: string + calendarId?: string + eventId: string + timeMin?: string + timeMax?: string + maxResults?: number + pageToken?: string + showDeleted?: boolean +} + +interface GoogleCalendarInstancesResponse { + success: boolean + output: { + content: string + metadata: { + nextPageToken?: string + timeZone: string + instances: Array<{ + id: string + htmlLink: string + status: string + summary: string + description?: string + location?: string + start: { + dateTime?: string + date?: string + timeZone?: string + } + end: { + dateTime?: string + date?: string + timeZone?: string + } + attendees?: Array<{ + email: string + displayName?: string + responseStatus: string + }> + creator?: { + email: string + displayName?: string + } + organizer?: { + email: string + displayName?: string + } + recurringEventId: string + originalStartTime: { + dateTime?: string + date?: string + timeZone?: string + } + }> + } + } +} + +interface InstanceApiResponse { + kind: string + etag: string + summary: string + description?: string + updated: string + timeZone: string + accessRole: string + nextPageToken?: string + items: Array< + GoogleCalendarApiEventResponse & { + recurringEventId: string + originalStartTime: { + dateTime?: string + date?: string + timeZone?: string + } + } + > +} + +export const instancesTool: ToolConfig< + GoogleCalendarInstancesParams, + GoogleCalendarInstancesResponse +> = { + id: 'google_calendar_instances', + name: 'Google Calendar Get Instances', + description: 'Get instances of a recurring event from Google Calendar', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Calendar ID (defaults to primary)', + }, + eventId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Recurring event ID to get instances of', + }, + timeMin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Lower bound for instances (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)', + }, + timeMax: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Upper bound for instances (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of instances to return (default 250, max 2500)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Token for retrieving subsequent pages of results', + }, + showDeleted: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Include deleted instances', + }, + }, + + request: { + url: (params: GoogleCalendarInstancesParams) => { + const calendarId = params.calendarId || 'primary' + const queryParams = new URLSearchParams() + + if (params.timeMin) queryParams.append('timeMin', params.timeMin) + if (params.timeMax) queryParams.append('timeMax', params.timeMax) + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.showDeleted !== undefined) + queryParams.append('showDeleted', params.showDeleted.toString()) + + const queryString = queryParams.toString() + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}/instances${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params: GoogleCalendarInstancesParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: InstanceApiResponse = await response.json() + const instances = data.items || [] + const instancesCount = instances.length + + return { + success: true, + output: { + content: `Found ${instancesCount} instance${instancesCount !== 1 ? 's' : ''} of the recurring event`, + metadata: { + nextPageToken: data.nextPageToken, + timeZone: data.timeZone, + instances: instances.map((instance) => ({ + id: instance.id, + htmlLink: instance.htmlLink, + status: instance.status, + summary: instance.summary || 'No title', + description: instance.description, + location: instance.location, + start: instance.start, + end: instance.end, + attendees: instance.attendees, + creator: instance.creator, + organizer: instance.organizer, + recurringEventId: instance.recurringEventId, + originalStartTime: instance.originalStartTime, + })), + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of found instances count' }, + metadata: { + type: 'json', + description: 'List of recurring event instances with pagination tokens', + }, + }, +} + +interface GoogleCalendarInstancesV2Response { + success: boolean + output: { + nextPageToken: string | null + timeZone: string | null + instances: Array> + } +} + +export const instancesV2Tool: ToolConfig< + GoogleCalendarInstancesParams, + GoogleCalendarInstancesV2Response +> = { + id: 'google_calendar_instances_v2', + name: 'Google Calendar Get Instances', + description: + 'Get instances of a recurring event from Google Calendar. Returns API-aligned fields only.', + version: '2.0.0', + oauth: instancesTool.oauth, + params: instancesTool.params, + request: instancesTool.request, + transformResponse: async (response: Response) => { + const data: InstanceApiResponse = await response.json() + const instances = data.items || [] + + return { + success: true, + output: { + nextPageToken: data.nextPageToken ?? null, + timeZone: data.timeZone ?? null, + instances: instances.map((instance) => ({ + id: instance.id, + htmlLink: instance.htmlLink, + status: instance.status, + summary: instance.summary ?? null, + description: instance.description ?? null, + location: instance.location ?? null, + start: instance.start, + end: instance.end, + attendees: instance.attendees ?? null, + creator: instance.creator, + organizer: instance.organizer, + recurringEventId: instance.recurringEventId, + originalStartTime: instance.originalStartTime, + })), + }, + } + }, + outputs: { + nextPageToken: { type: 'string', description: 'Next page token', optional: true }, + timeZone: { type: 'string', description: 'Calendar time zone', optional: true }, + instances: { type: 'json', description: 'List of recurring event instances' }, + }, +} diff --git a/apps/sim/tools/google_calendar/list_calendars.ts b/apps/sim/tools/google_calendar/list_calendars.ts new file mode 100644 index 0000000000..77aa38de5f --- /dev/null +++ b/apps/sim/tools/google_calendar/list_calendars.ts @@ -0,0 +1,280 @@ +import { CALENDAR_API_BASE } from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +interface GoogleCalendarListCalendarsParams { + accessToken: string + minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner' + maxResults?: number + pageToken?: string + showDeleted?: boolean + showHidden?: boolean +} + +interface CalendarListEntry { + kind: string + etag: string + id: string + summary: string + description?: string + location?: string + timeZone: string + summaryOverride?: string + colorId: string + backgroundColor: string + foregroundColor: string + hidden?: boolean + selected?: boolean + accessRole: string + defaultReminders: Array<{ + method: string + minutes: number + }> + notificationSettings?: { + notifications: Array<{ + type: string + method: string + }> + } + primary?: boolean + deleted?: boolean + conferenceProperties?: { + allowedConferenceSolutionTypes: string[] + } +} + +interface CalendarListApiResponse { + kind: string + etag: string + nextPageToken?: string + nextSyncToken?: string + items: CalendarListEntry[] +} + +interface GoogleCalendarListCalendarsResponse { + success: boolean + output: { + content: string + metadata: { + nextPageToken?: string + calendars: Array<{ + id: string + summary: string + description?: string + location?: string + timeZone: string + accessRole: string + backgroundColor: string + foregroundColor: string + primary?: boolean + hidden?: boolean + selected?: boolean + }> + } + } +} + +export const listCalendarsTool: ToolConfig< + GoogleCalendarListCalendarsParams, + GoogleCalendarListCalendarsResponse +> = { + id: 'google_calendar_list_calendars', + name: 'Google Calendar List Calendars', + description: "List all calendars in the user's calendar list", + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + minAccessRole: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Minimum access role for returned calendars: freeBusyReader, reader, writer, or owner', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of calendars to return (default 100, max 250)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Token for retrieving subsequent pages of results', + }, + showDeleted: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Include deleted calendars', + }, + showHidden: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Include hidden calendars', + }, + }, + + request: { + url: (params: GoogleCalendarListCalendarsParams) => { + const queryParams = new URLSearchParams() + + if (params.minAccessRole) queryParams.append('minAccessRole', params.minAccessRole) + if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString()) + if (params.pageToken) queryParams.append('pageToken', params.pageToken) + if (params.showDeleted !== undefined) + queryParams.append('showDeleted', params.showDeleted.toString()) + if (params.showHidden !== undefined) + queryParams.append('showHidden', params.showHidden.toString()) + + const queryString = queryParams.toString() + return `${CALENDAR_API_BASE}/users/me/calendarList${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params: GoogleCalendarListCalendarsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: CalendarListApiResponse = await response.json() + const calendars = data.items || [] + const calendarsCount = calendars.length + + return { + success: true, + output: { + content: `Found ${calendarsCount} calendar${calendarsCount !== 1 ? 's' : ''}`, + metadata: { + nextPageToken: data.nextPageToken, + calendars: calendars.map((calendar) => ({ + id: calendar.id, + summary: calendar.summaryOverride || calendar.summary, + description: calendar.description, + location: calendar.location, + timeZone: calendar.timeZone, + accessRole: calendar.accessRole, + backgroundColor: calendar.backgroundColor, + foregroundColor: calendar.foregroundColor, + primary: calendar.primary, + hidden: calendar.hidden, + selected: calendar.selected, + })), + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Summary of found calendars count' }, + metadata: { + type: 'json', + description: 'List of calendars with their details', + }, + }, +} + +interface GoogleCalendarListCalendarsV2Response { + success: boolean + output: { + nextPageToken: string | null + calendars: Array<{ + id: string + summary: string + description: string | null + location: string | null + timeZone: string + accessRole: string + backgroundColor: string + foregroundColor: string + primary: boolean | null + hidden: boolean | null + selected: boolean | null + }> + } +} + +export const listCalendarsV2Tool: ToolConfig< + GoogleCalendarListCalendarsParams, + GoogleCalendarListCalendarsV2Response +> = { + id: 'google_calendar_list_calendars_v2', + name: 'Google Calendar List Calendars', + description: "List all calendars in the user's calendar list. Returns API-aligned fields only.", + version: '2.0.0', + oauth: listCalendarsTool.oauth, + params: listCalendarsTool.params, + request: listCalendarsTool.request, + transformResponse: async (response: Response) => { + const data: CalendarListApiResponse = await response.json() + const calendars = data.items || [] + + return { + success: true, + output: { + nextPageToken: data.nextPageToken ?? null, + calendars: calendars.map((calendar) => ({ + id: calendar.id, + summary: calendar.summaryOverride || calendar.summary, + description: calendar.description ?? null, + location: calendar.location ?? null, + timeZone: calendar.timeZone, + accessRole: calendar.accessRole, + backgroundColor: calendar.backgroundColor, + foregroundColor: calendar.foregroundColor, + primary: calendar.primary ?? null, + hidden: calendar.hidden ?? null, + selected: calendar.selected ?? null, + })), + }, + } + }, + outputs: { + nextPageToken: { type: 'string', description: 'Next page token', optional: true }, + calendars: { + type: 'array', + description: 'List of calendars', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Calendar ID' }, + summary: { type: 'string', description: 'Calendar title' }, + description: { type: 'string', description: 'Calendar description', optional: true }, + location: { type: 'string', description: 'Calendar location', optional: true }, + timeZone: { type: 'string', description: 'Calendar time zone' }, + accessRole: { type: 'string', description: 'Access role for the calendar' }, + backgroundColor: { type: 'string', description: 'Calendar background color' }, + foregroundColor: { type: 'string', description: 'Calendar foreground color' }, + primary: { + type: 'boolean', + description: 'Whether this is the primary calendar', + optional: true, + }, + hidden: { + type: 'boolean', + description: 'Whether the calendar is hidden', + optional: true, + }, + selected: { + type: 'boolean', + description: 'Whether the calendar is selected', + optional: true, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_calendar/move.ts b/apps/sim/tools/google_calendar/move.ts new file mode 100644 index 0000000000..3073ee41d4 --- /dev/null +++ b/apps/sim/tools/google_calendar/move.ts @@ -0,0 +1,208 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiEventResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +interface GoogleCalendarMoveParams { + accessToken: string + calendarId?: string + eventId: string + destinationCalendarId: string + sendUpdates?: 'all' | 'externalOnly' | 'none' +} + +interface GoogleCalendarMoveResponse { + success: boolean + output: { + content: string + metadata: { + id: string + htmlLink: string + status: string + summary: string + description?: string + location?: string + start: { + dateTime?: string + date?: string + timeZone?: string + } + end: { + dateTime?: string + date?: string + timeZone?: string + } + attendees?: Array<{ + email: string + displayName?: string + responseStatus: string + }> + creator?: { + email: string + displayName?: string + } + organizer?: { + email: string + displayName?: string + } + } + } +} + +export const moveTool: ToolConfig = { + id: 'google_calendar_move', + name: 'Google Calendar Move Event', + description: 'Move an event to a different calendar', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Source calendar ID (defaults to primary)', + }, + eventId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Event ID to move', + }, + destinationCalendarId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Destination calendar ID', + }, + sendUpdates: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'How to send updates to attendees: all, externalOnly, or none', + }, + }, + + request: { + url: (params: GoogleCalendarMoveParams) => { + const calendarId = params.calendarId || 'primary' + const queryParams = new URLSearchParams() + + queryParams.append('destination', params.destinationCalendarId) + + if (params.sendUpdates !== undefined) { + queryParams.append('sendUpdates', params.sendUpdates) + } + + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}/move?${queryParams.toString()}` + }, + method: 'POST', + headers: (params: GoogleCalendarMoveParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiEventResponse = await response.json() + + return { + success: true, + output: { + content: `Event "${data.summary}" moved successfully`, + metadata: { + id: data.id, + htmlLink: data.htmlLink, + status: data.status, + summary: data.summary, + description: data.description, + location: data.location, + start: data.start, + end: data.end, + attendees: data.attendees, + creator: data.creator, + organizer: data.organizer, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Event move confirmation message' }, + metadata: { + type: 'json', + description: 'Moved event metadata including new details', + }, + }, +} + +interface GoogleCalendarMoveV2Response { + success: boolean + output: { + id: string + htmlLink: string + status: string + summary: string | null + description: string | null + location: string | null + start: any + end: any + attendees: any | null + creator: any + organizer: any + } +} + +export const moveV2Tool: ToolConfig = { + id: 'google_calendar_move_v2', + name: 'Google Calendar Move Event', + description: 'Move an event to a different calendar. Returns API-aligned fields only.', + version: '2.0.0', + oauth: moveTool.oauth, + params: moveTool.params, + request: moveTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiEventResponse = await response.json() + + return { + success: true, + output: { + id: data.id, + htmlLink: data.htmlLink, + status: data.status, + summary: data.summary ?? null, + description: data.description ?? null, + location: data.location ?? null, + start: data.start, + end: data.end, + attendees: data.attendees ?? null, + creator: data.creator, + organizer: data.organizer, + }, + } + }, + outputs: { + id: { type: 'string', description: 'Event ID' }, + htmlLink: { type: 'string', description: 'Event link' }, + status: { type: 'string', description: 'Event status' }, + summary: { type: 'string', description: 'Event title', optional: true }, + description: { type: 'string', description: 'Event description', optional: true }, + location: { type: 'string', description: 'Event location', optional: true }, + start: { type: 'json', description: 'Event start' }, + end: { type: 'json', description: 'Event end' }, + attendees: { type: 'json', description: 'Event attendees', optional: true }, + creator: { type: 'json', description: 'Event creator' }, + organizer: { type: 'json', description: 'Event organizer' }, + }, +} diff --git a/apps/sim/tools/google_calendar/types.ts b/apps/sim/tools/google_calendar/types.ts index c6e5352597..46b39f8a04 100644 --- a/apps/sim/tools/google_calendar/types.ts +++ b/apps/sim/tools/google_calendar/types.ts @@ -75,6 +75,30 @@ export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams { replaceExisting?: boolean // Whether to replace existing attendees or add to them } +export interface GoogleCalendarMoveParams extends BaseGoogleCalendarParams { + eventId: string + destinationCalendarId: string + sendUpdates?: 'all' | 'externalOnly' | 'none' +} + +export interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams { + eventId: string + timeMin?: string + timeMax?: string + maxResults?: number + pageToken?: string + showDeleted?: boolean +} + +export interface GoogleCalendarListCalendarsParams { + accessToken: string + minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner' + maxResults?: number + pageToken?: string + showDeleted?: boolean + showHidden?: boolean +} + export type GoogleCalendarToolParams = | GoogleCalendarCreateParams | GoogleCalendarListParams @@ -83,6 +107,9 @@ export type GoogleCalendarToolParams = | GoogleCalendarDeleteParams | GoogleCalendarQuickAddParams | GoogleCalendarInviteParams + | GoogleCalendarMoveParams + | GoogleCalendarInstancesParams + | GoogleCalendarListCalendarsParams interface EventMetadata { id: string @@ -277,6 +304,65 @@ export interface GoogleCalendarApiListResponse { items: GoogleCalendarApiEventResponse[] } +export interface GoogleCalendarDeleteResponse extends ToolResponse { + output: { + content: string + metadata: { + eventId: string + deleted: boolean + } + } +} + +export interface GoogleCalendarMoveResponse extends ToolResponse { + output: { + content: string + metadata: EventMetadata + } +} + +export interface GoogleCalendarInstancesResponse extends ToolResponse { + output: { + content: string + metadata: { + nextPageToken?: string + timeZone: string + instances: Array< + EventMetadata & { + recurringEventId: string + originalStartTime: { + dateTime?: string + date?: string + timeZone?: string + } + } + > + } + } +} + +export interface GoogleCalendarListCalendarsResponse extends ToolResponse { + output: { + content: string + metadata: { + nextPageToken?: string + calendars: Array<{ + id: string + summary: string + description?: string + location?: string + timeZone: string + accessRole: string + backgroundColor: string + foregroundColor: string + primary?: boolean + hidden?: boolean + selected?: boolean + }> + } + } +} + export type GoogleCalendarResponse = | GoogleCalendarCreateResponse | GoogleCalendarListResponse @@ -284,3 +370,7 @@ export type GoogleCalendarResponse = | GoogleCalendarQuickAddResponse | GoogleCalendarInviteResponse | GoogleCalendarUpdateResponse + | GoogleCalendarDeleteResponse + | GoogleCalendarMoveResponse + | GoogleCalendarInstancesResponse + | GoogleCalendarListCalendarsResponse diff --git a/apps/sim/tools/google_calendar/update.ts b/apps/sim/tools/google_calendar/update.ts new file mode 100644 index 0000000000..be9ad15d59 --- /dev/null +++ b/apps/sim/tools/google_calendar/update.ts @@ -0,0 +1,255 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiEventResponse, + type GoogleCalendarUpdateParams, + type GoogleCalendarUpdateResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'google_calendar_update', + name: 'Google Calendar Update Event', + description: 'Update an existing event in Google Calendar', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Calendar ID (defaults to primary)', + }, + eventId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Event ID to update', + }, + summary: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New event title/summary', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New event description', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New event location', + }, + startDateTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'New start date and time. MUST include timezone offset (e.g., 2025-06-03T10:00:00-08:00) OR provide timeZone parameter', + }, + endDateTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'New end date and time. MUST include timezone offset (e.g., 2025-06-03T11:00:00-08:00) OR provide timeZone parameter', + }, + timeZone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Time zone (e.g., America/Los_Angeles). Required if datetime does not include offset.', + }, + attendees: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Array of attendee email addresses (replaces existing attendees)', + }, + sendUpdates: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'How to send updates to attendees: all, externalOnly, or none', + }, + }, + + request: { + url: (params: GoogleCalendarUpdateParams) => { + const calendarId = params.calendarId || 'primary' + const queryParams = new URLSearchParams() + + if (params.sendUpdates !== undefined) { + queryParams.append('sendUpdates', params.sendUpdates) + } + + const queryString = queryParams.toString() + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}` + }, + method: 'PATCH', + headers: (params: GoogleCalendarUpdateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleCalendarUpdateParams) => { + const updateData: Record = {} + + if (params.summary !== undefined) { + updateData.summary = params.summary + } + + if (params.description !== undefined) { + updateData.description = params.description + } + + if (params.location !== undefined) { + updateData.location = params.location + } + + if (params.startDateTime !== undefined) { + const needsTimezone = + !params.startDateTime.includes('+') && !params.startDateTime.includes('-', 10) + updateData.start = { + dateTime: params.startDateTime, + ...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}), + } + } + + if (params.endDateTime !== undefined) { + const needsTimezone = + !params.endDateTime.includes('+') && !params.endDateTime.includes('-', 10) + updateData.end = { + dateTime: params.endDateTime, + ...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}), + } + } + + // Handle attendees - convert to array format + if (params.attendees !== undefined) { + let attendeeList: string[] = [] + const attendees = params.attendees as string | string[] + + if (Array.isArray(attendees)) { + attendeeList = attendees.filter((email: string) => email && email.trim().length > 0) + } else if (typeof attendees === 'string' && attendees.trim().length > 0) { + attendeeList = attendees + .split(',') + .map((email: string) => email.trim()) + .filter((email: string) => email.length > 0) + } + + updateData.attendees = attendeeList.map((email: string) => ({ email })) + } + + return updateData + }, + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiEventResponse = await response.json() + + return { + success: true, + output: { + content: `Event "${data.summary}" updated successfully`, + metadata: { + id: data.id, + htmlLink: data.htmlLink, + status: data.status, + summary: data.summary, + description: data.description, + location: data.location, + start: data.start, + end: data.end, + attendees: data.attendees, + creator: data.creator, + organizer: data.organizer, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Event update confirmation message' }, + metadata: { + type: 'json', + description: 'Updated event metadata including ID, status, and details', + }, + }, +} + +interface GoogleCalendarUpdateV2Response { + success: boolean + output: { + id: string + htmlLink: string + status: string + summary: string | null + description: string | null + location: string | null + start: any + end: any + attendees: any | null + creator: any + organizer: any + } +} + +export const updateV2Tool: ToolConfig = + { + id: 'google_calendar_update_v2', + name: 'Google Calendar Update Event', + description: 'Update an existing event in Google Calendar. Returns API-aligned fields only.', + version: '2.0.0', + oauth: updateTool.oauth, + params: updateTool.params, + request: updateTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiEventResponse = await response.json() + + return { + success: true, + output: { + id: data.id, + htmlLink: data.htmlLink, + status: data.status, + summary: data.summary ?? null, + description: data.description ?? null, + location: data.location ?? null, + start: data.start, + end: data.end, + attendees: data.attendees ?? null, + creator: data.creator, + organizer: data.organizer, + }, + } + }, + outputs: { + id: { type: 'string', description: 'Event ID' }, + htmlLink: { type: 'string', description: 'Event link' }, + status: { type: 'string', description: 'Event status' }, + summary: { type: 'string', description: 'Event title', optional: true }, + description: { type: 'string', description: 'Event description', optional: true }, + location: { type: 'string', description: 'Event location', optional: true }, + start: { type: 'json', description: 'Event start' }, + end: { type: 'json', description: 'Event end' }, + attendees: { type: 'json', description: 'Event attendees', optional: true }, + creator: { type: 'json', description: 'Event creator' }, + organizer: { type: 'json', description: 'Event organizer' }, + }, + } diff --git a/apps/sim/tools/google_drive/copy.ts b/apps/sim/tools/google_drive/copy.ts new file mode 100644 index 0000000000..803742e61f --- /dev/null +++ b/apps/sim/tools/google_drive/copy.ts @@ -0,0 +1,111 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveCopyParams extends GoogleDriveToolParams { + fileId: string + newName?: string + destinationFolderId?: string +} + +interface GoogleDriveCopyResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const copyTool: ToolConfig = { + id: 'google_drive_copy', + name: 'Copy Google Drive File', + description: 'Create a copy of a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to copy', + }, + newName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Name for the copied file (defaults to "Copy of [original name]")', + }, + destinationFolderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of the folder to place the copy in (defaults to same location as original)', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/copy`) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.newName) { + body.name = params.newName + } + if (params.destinationFolderId) { + body.parents = [params.destinationFolderId.trim()] + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to copy Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The copied file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID of the copy' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + owners: { type: 'json', description: 'List of file owners' }, + size: { type: 'string', description: 'File size in bytes' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/delete.ts b/apps/sim/tools/google_drive/delete.ts new file mode 100644 index 0000000000..5db1349e74 --- /dev/null +++ b/apps/sim/tools/google_drive/delete.ts @@ -0,0 +1,78 @@ +import type { GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveDeleteParams extends GoogleDriveToolParams { + fileId: string +} + +interface GoogleDriveDeleteResponse extends ToolResponse { + output: { + deleted: boolean + fileId: string + } +} + +export const deleteTool: ToolConfig = { + id: 'google_drive_delete', + name: 'Delete Google Drive File', + description: 'Permanently delete a file from Google Drive (bypasses trash)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to permanently delete', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + const data = await response.json() + throw new Error(data.error?.message || 'Failed to delete Google Drive file') + } + + return { + success: true, + output: { + deleted: true, + fileId: params?.fileId ?? '', + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the file was successfully deleted', + }, + fileId: { + type: 'string', + description: 'The ID of the deleted file', + }, + }, +} diff --git a/apps/sim/tools/google_drive/get_about.ts b/apps/sim/tools/google_drive/get_about.ts new file mode 100644 index 0000000000..59f8a39bdf --- /dev/null +++ b/apps/sim/tools/google_drive/get_about.ts @@ -0,0 +1,137 @@ +import type { GoogleDriveToolParams, GoogleDriveUser } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveGetAboutParams extends GoogleDriveToolParams {} + +interface GoogleDriveGetAboutResponse extends ToolResponse { + output: { + user: GoogleDriveUser & { + emailAddress: string + } + storageQuota: { + limit: string | null + usage: string + usageInDrive: string + usageInDriveTrash: string + } + canCreateDrives: boolean + importFormats: Record + exportFormats: Record + maxUploadSize: string + } +} + +export const getAboutTool: ToolConfig = { + id: 'google_drive_get_about', + name: 'Get Google Drive Info', + description: + 'Get information about the user and their Google Drive (storage quota, capabilities)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + }, + + request: { + url: () => { + const url = new URL('https://www.googleapis.com/drive/v3/about') + url.searchParams.append( + 'fields', + 'user,storageQuota,canCreateDrives,importFormats,exportFormats,maxUploadSize' + ) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get Google Drive info') + } + + return { + success: true, + output: { + user: { + displayName: data.user?.displayName ?? null, + emailAddress: data.user?.emailAddress ?? '', + photoLink: data.user?.photoLink ?? null, + permissionId: data.user?.permissionId ?? null, + me: data.user?.me ?? true, + }, + storageQuota: { + limit: data.storageQuota?.limit ?? null, + usage: data.storageQuota?.usage ?? '0', + usageInDrive: data.storageQuota?.usageInDrive ?? '0', + usageInDriveTrash: data.storageQuota?.usageInDriveTrash ?? '0', + }, + canCreateDrives: data.canCreateDrives ?? false, + importFormats: data.importFormats ?? {}, + exportFormats: data.exportFormats ?? {}, + maxUploadSize: data.maxUploadSize ?? '0', + }, + } + }, + + outputs: { + user: { + type: 'json', + description: 'Information about the authenticated user', + properties: { + displayName: { type: 'string', description: 'User display name' }, + emailAddress: { type: 'string', description: 'User email address' }, + photoLink: { type: 'string', description: 'URL to user profile photo', optional: true }, + permissionId: { type: 'string', description: 'User permission ID' }, + me: { type: 'boolean', description: 'Whether this is the authenticated user' }, + }, + }, + storageQuota: { + type: 'json', + description: 'Storage quota information in bytes', + properties: { + limit: { + type: 'string', + description: 'Total storage limit in bytes (null for unlimited)', + optional: true, + }, + usage: { type: 'string', description: 'Total storage used in bytes' }, + usageInDrive: { type: 'string', description: 'Storage used by Drive files in bytes' }, + usageInDriveTrash: { + type: 'string', + description: 'Storage used by trashed files in bytes', + }, + }, + }, + canCreateDrives: { + type: 'boolean', + description: 'Whether user can create shared drives', + }, + importFormats: { + type: 'json', + description: 'Map of MIME types that can be imported and their target formats', + }, + exportFormats: { + type: 'json', + description: 'Map of Google Workspace MIME types and their exportable formats', + }, + maxUploadSize: { + type: 'string', + description: 'Maximum upload size in bytes', + }, + }, +} diff --git a/apps/sim/tools/google_drive/get_file.ts b/apps/sim/tools/google_drive/get_file.ts new file mode 100644 index 0000000000..ea3cfdc51b --- /dev/null +++ b/apps/sim/tools/google_drive/get_file.ts @@ -0,0 +1,99 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveGetFileParams extends GoogleDriveToolParams { + fileId: string +} + +interface GoogleDriveGetFileResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const getFileTool: ToolConfig = { + id: 'google_drive_get_file', + name: 'Get Google Drive File', + description: 'Get metadata for a specific file in Google Drive by its ID', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to retrieve', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + description: { type: 'string', description: 'File description', optional: true }, + size: { type: 'string', description: 'File size in bytes', optional: true }, + starred: { type: 'boolean', description: 'Whether file is starred' }, + trashed: { type: 'boolean', description: 'Whether file is in trash' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + webContentLink: { type: 'string', description: 'Direct download URL', optional: true }, + iconLink: { type: 'string', description: 'URL to file icon' }, + thumbnailLink: { type: 'string', description: 'URL to thumbnail', optional: true }, + parents: { type: 'json', description: 'Parent folder IDs' }, + owners: { type: 'json', description: 'List of file owners' }, + permissions: { type: 'json', description: 'File permissions', optional: true }, + createdTime: { type: 'string', description: 'File creation time' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + lastModifyingUser: { type: 'json', description: 'User who last modified the file' }, + shared: { type: 'boolean', description: 'Whether file is shared' }, + ownedByMe: { type: 'boolean', description: 'Whether owned by current user' }, + capabilities: { type: 'json', description: 'User capabilities on file' }, + md5Checksum: { type: 'string', description: 'MD5 hash', optional: true }, + version: { type: 'string', description: 'Version number' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/index.ts b/apps/sim/tools/google_drive/index.ts index 4d1a6f1678..58dbdb03e7 100644 --- a/apps/sim/tools/google_drive/index.ts +++ b/apps/sim/tools/google_drive/index.ts @@ -1,11 +1,31 @@ +import { copyTool } from '@/tools/google_drive/copy' import { createFolderTool } from '@/tools/google_drive/create_folder' +import { deleteTool } from '@/tools/google_drive/delete' import { downloadTool } from '@/tools/google_drive/download' +import { getAboutTool } from '@/tools/google_drive/get_about' import { getContentTool } from '@/tools/google_drive/get_content' +import { getFileTool } from '@/tools/google_drive/get_file' import { listTool } from '@/tools/google_drive/list' +import { listPermissionsTool } from '@/tools/google_drive/list_permissions' +import { shareTool } from '@/tools/google_drive/share' +import { trashTool } from '@/tools/google_drive/trash' +import { unshareTool } from '@/tools/google_drive/unshare' +import { untrashTool } from '@/tools/google_drive/untrash' +import { updateTool } from '@/tools/google_drive/update' import { uploadTool } from '@/tools/google_drive/upload' +export const googleDriveCopyTool = copyTool export const googleDriveCreateFolderTool = createFolderTool +export const googleDriveDeleteTool = deleteTool export const googleDriveDownloadTool = downloadTool +export const googleDriveGetAboutTool = getAboutTool export const googleDriveGetContentTool = getContentTool +export const googleDriveGetFileTool = getFileTool export const googleDriveListTool = listTool +export const googleDriveListPermissionsTool = listPermissionsTool +export const googleDriveShareTool = shareTool +export const googleDriveTrashTool = trashTool +export const googleDriveUnshareTool = unshareTool +export const googleDriveUntrashTool = untrashTool +export const googleDriveUpdateTool = updateTool export const googleDriveUploadTool = uploadTool diff --git a/apps/sim/tools/google_drive/list_permissions.ts b/apps/sim/tools/google_drive/list_permissions.ts new file mode 100644 index 0000000000..954dcfa006 --- /dev/null +++ b/apps/sim/tools/google_drive/list_permissions.ts @@ -0,0 +1,124 @@ +import type { GoogleDrivePermission, GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveListPermissionsParams extends GoogleDriveToolParams { + fileId: string +} + +interface GoogleDriveListPermissionsResponse extends ToolResponse { + output: { + permissions: GoogleDrivePermission[] + } +} + +export const listPermissionsTool: ToolConfig< + GoogleDriveListPermissionsParams, + GoogleDriveListPermissionsResponse +> = { + id: 'google_drive_list_permissions', + name: 'List Google Drive Permissions', + description: 'List all permissions (who has access) for a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to list permissions for', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions` + ) + url.searchParams.append('supportsAllDrives', 'true') + url.searchParams.append( + 'fields', + 'permissions(id,type,role,emailAddress,displayName,photoLink,domain,expirationTime,deleted,allowFileDiscovery,pendingOwner,permissionDetails)' + ) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list Google Drive permissions') + } + + const permissions = (data.permissions ?? []).map((p: Record) => ({ + id: p.id ?? null, + type: p.type ?? null, + role: p.role ?? null, + emailAddress: p.emailAddress ?? null, + displayName: p.displayName ?? null, + photoLink: p.photoLink ?? null, + domain: p.domain ?? null, + expirationTime: p.expirationTime ?? null, + deleted: p.deleted ?? false, + allowFileDiscovery: p.allowFileDiscovery ?? null, + pendingOwner: p.pendingOwner ?? false, + permissionDetails: p.permissionDetails ?? null, + })) + + return { + success: true, + output: { + permissions, + }, + } + }, + + outputs: { + permissions: { + type: 'array', + description: 'List of permissions on the file', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Permission ID (use to remove permission)' }, + type: { type: 'string', description: 'Grantee type (user, group, domain, anyone)' }, + role: { + type: 'string', + description: + 'Permission role (owner, organizer, fileOrganizer, writer, commenter, reader)', + }, + emailAddress: { type: 'string', description: 'Email of the grantee' }, + displayName: { type: 'string', description: 'Display name of the grantee' }, + photoLink: { type: 'string', description: 'Photo URL of the grantee' }, + domain: { type: 'string', description: 'Domain of the grantee' }, + expirationTime: { type: 'string', description: 'When permission expires' }, + deleted: { type: 'boolean', description: 'Whether grantee account is deleted' }, + allowFileDiscovery: { + type: 'boolean', + description: 'Whether file is discoverable by grantee', + }, + pendingOwner: { type: 'boolean', description: 'Whether ownership transfer is pending' }, + permissionDetails: { + type: 'json', + description: 'Details about inherited permissions', + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/share.ts b/apps/sim/tools/google_drive/share.ts new file mode 100644 index 0000000000..6e01c9681a --- /dev/null +++ b/apps/sim/tools/google_drive/share.ts @@ -0,0 +1,178 @@ +import type { GoogleDrivePermission, GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveShareParams extends GoogleDriveToolParams { + fileId: string + email?: string + domain?: string + type: 'user' | 'group' | 'domain' | 'anyone' + role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader' + transferOwnership?: boolean + moveToNewOwnersRoot?: boolean + sendNotification?: boolean + emailMessage?: string +} + +interface GoogleDriveShareResponse extends ToolResponse { + output: { + permission: GoogleDrivePermission + } +} + +export const shareTool: ToolConfig = { + id: 'google_drive_share', + name: 'Share Google Drive File', + description: 'Share a file with a user, group, domain, or make it public', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to share', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Type of grantee: user, group, domain, or anyone', + }, + role: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Permission role: owner (transfer ownership), organizer (shared drive only), fileOrganizer (shared drive only), writer (edit), commenter (view and comment), reader (view only)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address of the user or group (required for type=user or type=group)', + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Domain to share with (required for type=domain)', + }, + transferOwnership: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Required when role is owner. Transfers ownership to the specified user.', + }, + moveToNewOwnersRoot: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + "When transferring ownership, move the file to the new owner's My Drive root folder.", + }, + sendNotification: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to send an email notification (default: true)', + }, + emailMessage: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom message to include in the notification email', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions` + ) + url.searchParams.append('supportsAllDrives', 'true') + if (params.transferOwnership) { + url.searchParams.append('transferOwnership', 'true') + } + if (params.moveToNewOwnersRoot) { + url.searchParams.append('moveToNewOwnersRoot', 'true') + } + if (params.sendNotification !== undefined) { + url.searchParams.append('sendNotificationEmail', String(params.sendNotification)) + } + if (params.emailMessage) { + url.searchParams.append('emailMessage', params.emailMessage) + } + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + type: params.type, + role: params.role, + } + if (params.email) { + body.emailAddress = params.email.trim() + } + if (params.domain) { + body.domain = params.domain.trim() + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to share Google Drive file') + } + + return { + success: true, + output: { + permission: { + id: data.id ?? null, + type: data.type ?? null, + role: data.role ?? null, + emailAddress: data.emailAddress ?? null, + displayName: data.displayName ?? null, + domain: data.domain ?? null, + expirationTime: data.expirationTime ?? null, + deleted: data.deleted ?? false, + }, + }, + } + }, + + outputs: { + permission: { + type: 'json', + description: 'The created permission details', + properties: { + id: { type: 'string', description: 'Permission ID' }, + type: { type: 'string', description: 'Grantee type (user, group, domain, anyone)' }, + role: { type: 'string', description: 'Permission role' }, + emailAddress: { type: 'string', description: 'Email of the grantee', optional: true }, + displayName: { type: 'string', description: 'Display name of the grantee', optional: true }, + domain: { type: 'string', description: 'Domain of the grantee', optional: true }, + expirationTime: { type: 'string', description: 'Expiration time', optional: true }, + deleted: { type: 'boolean', description: 'Whether grantee is deleted' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/trash.ts b/apps/sim/tools/google_drive/trash.ts new file mode 100644 index 0000000000..d3d2a7ce57 --- /dev/null +++ b/apps/sim/tools/google_drive/trash.ts @@ -0,0 +1,87 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveTrashParams extends GoogleDriveToolParams { + fileId: string +} + +interface GoogleDriveTrashResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const trashTool: ToolConfig = { + id: 'google_drive_trash', + name: 'Trash Google Drive File', + description: 'Move a file to the trash in Google Drive (can be restored later)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to move to trash', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({ + trashed: true, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to trash Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The trashed file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + trashed: { type: 'boolean', description: 'Whether file is in trash (should be true)' }, + trashedTime: { type: 'string', description: 'When file was trashed' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/unshare.ts b/apps/sim/tools/google_drive/unshare.ts new file mode 100644 index 0000000000..7a135c220c --- /dev/null +++ b/apps/sim/tools/google_drive/unshare.ts @@ -0,0 +1,93 @@ +import type { GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveUnshareParams extends GoogleDriveToolParams { + fileId: string + permissionId: string +} + +interface GoogleDriveUnshareResponse extends ToolResponse { + output: { + removed: boolean + fileId: string + permissionId: string + } +} + +export const unshareTool: ToolConfig = { + id: 'google_drive_unshare', + name: 'Unshare Google Drive File', + description: 'Remove a permission from a file (revoke access)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to modify permissions on', + }, + permissionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the permission to remove (use list_permissions to find this)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions/${params.permissionId?.trim()}` + ) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response, params) => { + if (!response.ok) { + const data = await response.json() + throw new Error(data.error?.message || 'Failed to remove permission from Google Drive file') + } + + return { + success: true, + output: { + removed: true, + fileId: params?.fileId ?? '', + permissionId: params?.permissionId ?? '', + }, + } + }, + + outputs: { + removed: { + type: 'boolean', + description: 'Whether the permission was successfully removed', + }, + fileId: { + type: 'string', + description: 'The ID of the file', + }, + permissionId: { + type: 'string', + description: 'The ID of the removed permission', + }, + }, +} diff --git a/apps/sim/tools/google_drive/untrash.ts b/apps/sim/tools/google_drive/untrash.ts new file mode 100644 index 0000000000..d04a96fb32 --- /dev/null +++ b/apps/sim/tools/google_drive/untrash.ts @@ -0,0 +1,87 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveUntrashParams extends GoogleDriveToolParams { + fileId: string +} + +interface GoogleDriveUntrashResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const untrashTool: ToolConfig = { + id: 'google_drive_untrash', + name: 'Restore Google Drive File', + description: 'Restore a file from the trash in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to restore from trash', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + return url.toString() + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({ + trashed: false, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to restore Google Drive file from trash') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The restored file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + trashed: { type: 'boolean', description: 'Whether file is in trash (should be false)' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/update.ts b/apps/sim/tools/google_drive/update.ts new file mode 100644 index 0000000000..51769421e6 --- /dev/null +++ b/apps/sim/tools/google_drive/update.ts @@ -0,0 +1,140 @@ +import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveUpdateParams extends GoogleDriveToolParams { + fileId: string + name?: string + description?: string + addParents?: string + removeParents?: string + starred?: boolean +} + +interface GoogleDriveUpdateResponse extends ToolResponse { + output: { + file: GoogleDriveFile + } +} + +export const updateTool: ToolConfig = { + id: 'google_drive_update', + name: 'Update Google Drive File', + description: 'Update file metadata in Google Drive (rename, move, star, add description)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the file', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the file', + }, + addParents: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of parent folder IDs to add (moves file to these folders)', + }, + removeParents: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of parent folder IDs to remove', + }, + starred: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to star or unstar the file', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`) + url.searchParams.append('fields', ALL_FILE_FIELDS) + url.searchParams.append('supportsAllDrives', 'true') + if (params.addParents) { + url.searchParams.append('addParents', params.addParents.trim()) + } + if (params.removeParents) { + url.searchParams.append('removeParents', params.removeParents.trim()) + } + return url.toString() + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name !== undefined) { + body.name = params.name + } + if (params.description !== undefined) { + body.description = params.description + } + if (params.starred !== undefined) { + body.starred = params.starred + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to update Google Drive file') + } + + return { + success: true, + output: { + file: data, + }, + } + }, + + outputs: { + file: { + type: 'json', + description: 'The updated file metadata', + properties: { + id: { type: 'string', description: 'Google Drive file ID' }, + name: { type: 'string', description: 'File name' }, + mimeType: { type: 'string', description: 'MIME type' }, + description: { type: 'string', description: 'File description', optional: true }, + starred: { type: 'boolean', description: 'Whether file is starred' }, + webViewLink: { type: 'string', description: 'URL to view in browser' }, + parents: { type: 'json', description: 'Parent folder IDs' }, + modifiedTime: { type: 'string', description: 'Last modification time' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_form/batch_update.ts b/apps/sim/tools/google_form/batch_update.ts new file mode 100644 index 0000000000..12c60d7199 --- /dev/null +++ b/apps/sim/tools/google_form/batch_update.ts @@ -0,0 +1,118 @@ +import type { + GoogleForm, + GoogleFormsBatchUpdateParams, + GoogleFormsBatchUpdateResponse, +} from '@/tools/google_form/types' +import { buildBatchUpdateUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +interface BatchUpdateApiResponse { + replies?: Record[] + writeControl?: { + requiredRevisionId?: string + targetRevisionId?: string + } + form?: GoogleForm +} + +export const batchUpdateTool: ToolConfig< + GoogleFormsBatchUpdateParams, + GoogleFormsBatchUpdateResponse +> = { + id: 'google_forms_batch_update', + name: 'Google Forms: Batch Update', + description: 'Apply multiple updates to a form (add items, update info, change settings, etc.)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form to update', + }, + requests: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of update requests (updateFormInfo, updateSettings, createItem, updateItem, moveItem, deleteItem)', + }, + includeFormInResponse: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to return the updated form in the response', + }, + }, + + request: { + url: (params: GoogleFormsBatchUpdateParams) => buildBatchUpdateUrl(params.formId), + method: 'POST', + headers: (params: GoogleFormsBatchUpdateParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleFormsBatchUpdateParams) => ({ + requests: params.requests, + includeFormInResponse: params.includeFormInResponse ?? false, + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as BatchUpdateApiResponse + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + replies: [], + writeControl: null, + form: null, + }, + error: errorData.error?.message ?? 'Failed to batch update form', + } + } + + return { + success: true, + output: { + replies: data.replies ?? [], + writeControl: data.writeControl ?? null, + form: data.form ?? null, + }, + } + }, + + outputs: { + replies: { + type: 'array', + description: 'The replies from each update request', + items: { + type: 'json', + }, + }, + writeControl: { + type: 'json', + description: 'Write control information with revision IDs', + optional: true, + }, + form: { + type: 'json', + description: 'The updated form (if includeFormInResponse was true)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_form/create_form.ts b/apps/sim/tools/google_form/create_form.ts new file mode 100644 index 0000000000..8b1abcdf52 --- /dev/null +++ b/apps/sim/tools/google_form/create_form.ts @@ -0,0 +1,106 @@ +import type { + GoogleForm, + GoogleFormsCreateFormParams, + GoogleFormsCreateFormResponse, +} from '@/tools/google_form/types' +import { buildCreateFormUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +export const createFormTool: ToolConfig< + GoogleFormsCreateFormParams, + GoogleFormsCreateFormResponse +> = { + id: 'google_forms_create_form', + name: 'Google Forms: Create Form', + description: 'Create a new Google Form with a title', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the form visible to responders', + }, + documentTitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The document title visible in Drive (defaults to form title)', + }, + unpublished: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, create an unpublished form that does not accept responses', + }, + }, + + request: { + url: (params: GoogleFormsCreateFormParams) => buildCreateFormUrl(params.unpublished), + method: 'POST', + headers: (params: GoogleFormsCreateFormParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleFormsCreateFormParams) => ({ + info: { + title: params.title, + ...(params.documentTitle ? { documentTitle: params.documentTitle } : {}), + }, + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleForm + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + formId: '', + title: null, + documentTitle: null, + responderUri: null, + revisionId: null, + }, + error: errorData.error?.message ?? 'Failed to create form', + } + } + + return { + success: true, + output: { + formId: data.formId ?? '', + title: data.info?.title ?? null, + documentTitle: data.info?.documentTitle ?? null, + responderUri: data.responderUri ?? null, + revisionId: data.revisionId ?? null, + }, + } + }, + + outputs: { + formId: { type: 'string', description: 'The ID of the created form' }, + title: { type: 'string', description: 'The form title', optional: true }, + documentTitle: { type: 'string', description: 'The document title in Drive', optional: true }, + responderUri: { + type: 'string', + description: 'The URI to share with responders', + optional: true, + }, + revisionId: { type: 'string', description: 'The revision ID of the form', optional: true }, + }, +} diff --git a/apps/sim/tools/google_form/create_watch.ts b/apps/sim/tools/google_form/create_watch.ts new file mode 100644 index 0000000000..fb145548e5 --- /dev/null +++ b/apps/sim/tools/google_form/create_watch.ts @@ -0,0 +1,120 @@ +import type { + GoogleFormsCreateWatchParams, + GoogleFormsCreateWatchResponse, + GoogleFormsWatch, +} from '@/tools/google_form/types' +import { buildCreateWatchUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +export const createWatchTool: ToolConfig< + GoogleFormsCreateWatchParams, + GoogleFormsCreateWatchResponse +> = { + id: 'google_forms_create_watch', + name: 'Google Forms: Create Watch', + description: 'Create a notification watch for form changes (schema changes or new responses)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form to watch', + }, + eventType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Event type to watch: SCHEMA (form changes) or RESPONSES (new submissions)', + }, + topicName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Cloud Pub/Sub topic name (format: projects/{project}/topics/{topic})', + }, + watchId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom watch ID (4-63 chars, lowercase letters, numbers, hyphens)', + }, + }, + + request: { + url: (params: GoogleFormsCreateWatchParams) => buildCreateWatchUrl(params.formId), + method: 'POST', + headers: (params: GoogleFormsCreateWatchParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleFormsCreateWatchParams) => ({ + watch: { + target: { + topic: { + topicName: params.topicName, + }, + }, + eventType: params.eventType, + }, + ...(params.watchId ? { watchId: params.watchId } : {}), + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleFormsWatch + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + id: '', + eventType: '', + topicName: null, + createTime: null, + expireTime: null, + state: null, + }, + error: errorData.error?.message ?? 'Failed to create watch', + } + } + + return { + success: true, + output: { + id: data.id ?? '', + eventType: data.eventType ?? '', + topicName: data.target?.topic?.topicName ?? null, + createTime: data.createTime ?? null, + expireTime: data.expireTime ?? null, + state: data.state ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'The watch ID' }, + eventType: { type: 'string', description: 'The event type being watched' }, + topicName: { type: 'string', description: 'The Cloud Pub/Sub topic', optional: true }, + createTime: { type: 'string', description: 'When the watch was created', optional: true }, + expireTime: { + type: 'string', + description: 'When the watch expires (7 days after creation)', + optional: true, + }, + state: { type: 'string', description: 'The watch state (ACTIVE, SUSPENDED)', optional: true }, + }, +} diff --git a/apps/sim/tools/google_form/delete_watch.ts b/apps/sim/tools/google_form/delete_watch.ts new file mode 100644 index 0000000000..c6e92b60fa --- /dev/null +++ b/apps/sim/tools/google_form/delete_watch.ts @@ -0,0 +1,76 @@ +import type { + GoogleFormsDeleteWatchParams, + GoogleFormsDeleteWatchResponse, +} from '@/tools/google_form/types' +import { buildDeleteWatchUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteWatchTool: ToolConfig< + GoogleFormsDeleteWatchParams, + GoogleFormsDeleteWatchResponse +> = { + id: 'google_forms_delete_watch', + name: 'Google Forms: Delete Watch', + description: 'Delete a notification watch from a form', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form', + }, + watchId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the watch to delete', + }, + }, + + request: { + url: (params: GoogleFormsDeleteWatchParams) => + buildDeleteWatchUrl(params.formId, params.watchId), + method: 'DELETE', + headers: (params: GoogleFormsDeleteWatchParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = (await response.json()) as { error?: { message?: string } } + return { + success: false, + output: { + deleted: false, + }, + error: data.error?.message ?? 'Failed to delete watch', + } + } + + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the watch was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/google_form/get_form.ts b/apps/sim/tools/google_form/get_form.ts new file mode 100644 index 0000000000..b99541872f --- /dev/null +++ b/apps/sim/tools/google_form/get_form.ts @@ -0,0 +1,119 @@ +import type { + GoogleForm, + GoogleFormsGetFormParams, + GoogleFormsGetFormResponse, +} from '@/tools/google_form/types' +import { buildGetFormUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +export const getFormTool: ToolConfig = { + id: 'google_forms_get_form', + name: 'Google Forms: Get Form', + description: 'Retrieve a form structure including its items, settings, and metadata', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form to retrieve', + }, + }, + + request: { + url: (params: GoogleFormsGetFormParams) => buildGetFormUrl(params.formId), + method: 'GET', + headers: (params: GoogleFormsGetFormParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleForm + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + formId: '', + title: null, + description: null, + documentTitle: null, + responderUri: null, + linkedSheetId: null, + revisionId: null, + items: [], + settings: null, + publishSettings: null, + }, + error: errorData.error?.message ?? 'Failed to get form', + } + } + + return { + success: true, + output: { + formId: data.formId ?? '', + title: data.info?.title ?? null, + description: data.info?.description ?? null, + documentTitle: data.info?.documentTitle ?? null, + responderUri: data.responderUri ?? null, + linkedSheetId: data.linkedSheetId ?? null, + revisionId: data.revisionId ?? null, + items: data.items ?? [], + settings: data.settings ?? null, + publishSettings: data.publishSettings ?? null, + }, + } + }, + + outputs: { + formId: { type: 'string', description: 'The form ID' }, + title: { type: 'string', description: 'The form title visible to responders', optional: true }, + description: { type: 'string', description: 'The form description', optional: true }, + documentTitle: { + type: 'string', + description: 'The document title visible in Drive', + optional: true, + }, + responderUri: { + type: 'string', + description: 'The URI to share with responders', + optional: true, + }, + linkedSheetId: { + type: 'string', + description: 'The ID of the linked Google Sheet', + optional: true, + }, + revisionId: { type: 'string', description: 'The revision ID of the form', optional: true }, + items: { + type: 'array', + description: 'The form items (questions, sections, etc.)', + items: { + type: 'object', + properties: { + itemId: { type: 'string', description: 'Item ID' }, + title: { type: 'string', description: 'Item title' }, + description: { type: 'string', description: 'Item description' }, + }, + }, + }, + settings: { type: 'json', description: 'Form settings', optional: true }, + publishSettings: { type: 'json', description: 'Form publish settings', optional: true }, + }, +} diff --git a/apps/sim/tools/google_form/index.ts b/apps/sim/tools/google_form/index.ts index 2ec798a88f..2c5eaa40cc 100644 --- a/apps/sim/tools/google_form/index.ts +++ b/apps/sim/tools/google_form/index.ts @@ -1,3 +1,21 @@ +import { batchUpdateTool } from '@/tools/google_form/batch_update' +import { createFormTool } from '@/tools/google_form/create_form' +import { createWatchTool } from '@/tools/google_form/create_watch' +import { deleteWatchTool } from '@/tools/google_form/delete_watch' +import { getFormTool } from '@/tools/google_form/get_form' import { getResponsesTool } from '@/tools/google_form/get_responses' +import { listWatchesTool } from '@/tools/google_form/list_watches' +import { renewWatchTool } from '@/tools/google_form/renew_watch' +import { setPublishSettingsTool } from '@/tools/google_form/set_publish_settings' export const googleFormsGetResponsesTool = getResponsesTool +export const googleFormsGetFormTool = getFormTool +export const googleFormsCreateFormTool = createFormTool +export const googleFormsBatchUpdateTool = batchUpdateTool +export const googleFormsSetPublishSettingsTool = setPublishSettingsTool +export const googleFormsCreateWatchTool = createWatchTool +export const googleFormsListWatchesTool = listWatchesTool +export const googleFormsDeleteWatchTool = deleteWatchTool +export const googleFormsRenewWatchTool = renewWatchTool + +export * from './types' diff --git a/apps/sim/tools/google_form/list_watches.ts b/apps/sim/tools/google_form/list_watches.ts new file mode 100644 index 0000000000..843f52e915 --- /dev/null +++ b/apps/sim/tools/google_form/list_watches.ts @@ -0,0 +1,99 @@ +import type { + GoogleFormsListWatchesParams, + GoogleFormsListWatchesResponse, + GoogleFormsWatch, +} from '@/tools/google_form/types' +import { buildListWatchesUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +interface ListWatchesApiResponse { + watches?: GoogleFormsWatch[] +} + +export const listWatchesTool: ToolConfig< + GoogleFormsListWatchesParams, + GoogleFormsListWatchesResponse +> = { + id: 'google_forms_list_watches', + name: 'Google Forms: List Watches', + description: 'List all notification watches for a form', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form', + }, + }, + + request: { + url: (params: GoogleFormsListWatchesParams) => buildListWatchesUrl(params.formId), + method: 'GET', + headers: (params: GoogleFormsListWatchesParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as ListWatchesApiResponse + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + watches: [], + }, + error: errorData.error?.message ?? 'Failed to list watches', + } + } + + const watches = (data.watches ?? []).map((watch) => ({ + id: watch.id, + target: watch.target, + eventType: watch.eventType, + createTime: watch.createTime, + expireTime: watch.expireTime, + state: watch.state, + errorType: watch.errorType, + })) + + return { + success: true, + output: { + watches, + }, + } + }, + + outputs: { + watches: { + type: 'array', + description: 'List of watches for the form', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Watch ID' }, + eventType: { type: 'string', description: 'Event type (SCHEMA or RESPONSES)' }, + createTime: { type: 'string', description: 'When the watch was created' }, + expireTime: { type: 'string', description: 'When the watch expires' }, + state: { type: 'string', description: 'Watch state' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_form/renew_watch.ts b/apps/sim/tools/google_form/renew_watch.ts new file mode 100644 index 0000000000..eb51e16a46 --- /dev/null +++ b/apps/sim/tools/google_form/renew_watch.ts @@ -0,0 +1,87 @@ +import type { + GoogleFormsRenewWatchParams, + GoogleFormsRenewWatchResponse, + GoogleFormsWatch, +} from '@/tools/google_form/types' +import { buildRenewWatchUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +export const renewWatchTool: ToolConfig< + GoogleFormsRenewWatchParams, + GoogleFormsRenewWatchResponse +> = { + id: 'google_forms_renew_watch', + name: 'Google Forms: Renew Watch', + description: 'Renew a notification watch for another 7 days', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form', + }, + watchId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the watch to renew', + }, + }, + + request: { + url: (params: GoogleFormsRenewWatchParams) => buildRenewWatchUrl(params.formId, params.watchId), + method: 'POST', + headers: (params: GoogleFormsRenewWatchParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleFormsWatch + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + id: '', + eventType: null, + expireTime: null, + state: null, + }, + error: errorData.error?.message ?? 'Failed to renew watch', + } + } + + return { + success: true, + output: { + id: data.id ?? '', + eventType: data.eventType ?? null, + expireTime: data.expireTime ?? null, + state: data.state ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'The watch ID' }, + eventType: { type: 'string', description: 'The event type being watched', optional: true }, + expireTime: { type: 'string', description: 'The new expiration time', optional: true }, + state: { type: 'string', description: 'The watch state', optional: true }, + }, +} diff --git a/apps/sim/tools/google_form/set_publish_settings.ts b/apps/sim/tools/google_form/set_publish_settings.ts new file mode 100644 index 0000000000..b0e826e935 --- /dev/null +++ b/apps/sim/tools/google_form/set_publish_settings.ts @@ -0,0 +1,119 @@ +import type { + GoogleFormsPublishSettings, + GoogleFormsSetPublishSettingsParams, + GoogleFormsSetPublishSettingsResponse, +} from '@/tools/google_form/types' +import { buildSetPublishSettingsUrl } from '@/tools/google_form/utils' +import type { ToolConfig } from '@/tools/types' + +interface SetPublishSettingsApiResponse { + formId?: string + publishSettings?: GoogleFormsPublishSettings +} + +export const setPublishSettingsTool: ToolConfig< + GoogleFormsSetPublishSettingsParams, + GoogleFormsSetPublishSettingsResponse +> = { + id: 'google_forms_set_publish_settings', + name: 'Google Forms: Set Publish Settings', + description: 'Update the publish settings of a form (publish/unpublish, accept responses)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-forms', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + formId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Google Form', + }, + isPublished: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Whether the form is published and visible to others', + }, + isAcceptingResponses: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the form accepts responses (forced to false if isPublished is false)', + }, + }, + + request: { + url: (params: GoogleFormsSetPublishSettingsParams) => buildSetPublishSettingsUrl(params.formId), + method: 'POST', + headers: (params: GoogleFormsSetPublishSettingsParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleFormsSetPublishSettingsParams) => ({ + publishSettings: { + publishState: { + isPublished: params.isPublished, + ...(params.isAcceptingResponses !== undefined + ? { isAcceptingResponses: params.isAcceptingResponses } + : {}), + }, + }, + updateMask: 'publishState', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as SetPublishSettingsApiResponse + + if (!response.ok) { + const errorData = data as unknown as { error?: { message?: string } } + return { + success: false, + output: { + formId: '', + publishSettings: {}, + }, + error: errorData.error?.message ?? 'Failed to set publish settings', + } + } + + return { + success: true, + output: { + formId: data.formId ?? '', + publishSettings: data.publishSettings ?? {}, + }, + } + }, + + outputs: { + formId: { type: 'string', description: 'The form ID' }, + publishSettings: { + type: 'json', + description: 'The updated publish settings', + properties: { + publishState: { + type: 'object', + description: 'The publish state', + properties: { + isPublished: { type: 'boolean', description: 'Whether the form is published' }, + isAcceptingResponses: { + type: 'boolean', + description: 'Whether the form accepts responses', + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_form/types.ts b/apps/sim/tools/google_form/types.ts index fea4c42dde..4806914492 100644 --- a/apps/sim/tools/google_form/types.ts +++ b/apps/sim/tools/google_form/types.ts @@ -1,3 +1,9 @@ +import type { ToolResponse } from '@/tools/types' + +// ============================================ +// Common Types +// ============================================ + export interface GoogleFormsResponse { responseId?: string createTime?: string @@ -13,9 +19,250 @@ export interface GoogleFormsResponseList { nextPageToken?: string } +export interface GoogleFormsInfo { + title?: string + description?: string + documentTitle?: string +} + +export interface GoogleFormsSettings { + quizSettings?: { + isQuiz?: boolean + } + emailCollectionType?: + | 'EMAIL_COLLECTION_TYPE_UNSPECIFIED' + | 'DO_NOT_COLLECT' + | 'VERIFIED' + | 'RESPONDER_INPUT' + [key: string]: unknown +} + +export interface GoogleFormsPublishState { + isPublished?: boolean + isAcceptingResponses?: boolean +} + +export interface GoogleFormsPublishSettings { + publishState?: GoogleFormsPublishState +} + +export interface GoogleFormsItem { + itemId?: string + title?: string + description?: string + questionItem?: Record + questionGroupItem?: Record + pageBreakItem?: Record + textItem?: Record + imageItem?: Record + videoItem?: Record +} + +export interface GoogleForm { + formId?: string + info?: GoogleFormsInfo + settings?: GoogleFormsSettings + items?: GoogleFormsItem[] + revisionId?: string + responderUri?: string + linkedSheetId?: string + publishSettings?: GoogleFormsPublishSettings +} + +export interface GoogleFormsWatch { + id?: string + target?: { + topic?: { + topicName?: string + } + } + eventType?: 'EVENT_TYPE_UNSPECIFIED' | 'SCHEMA' | 'RESPONSES' + createTime?: string + expireTime?: string + state?: 'STATE_UNSPECIFIED' | 'ACTIVE' | 'SUSPENDED' + errorType?: string +} + +// ============================================ +// Get Responses Params +// ============================================ + export interface GoogleFormsGetResponsesParams { accessToken: string formId: string responseId?: string pageSize?: number } + +// ============================================ +// Get Form Params & Response +// ============================================ + +export interface GoogleFormsGetFormParams { + accessToken: string + formId: string +} + +export interface GoogleFormsGetFormResponse extends ToolResponse { + output: { + formId: string + title: string | null + description: string | null + documentTitle: string | null + responderUri: string | null + linkedSheetId: string | null + revisionId: string | null + items: GoogleFormsItem[] + settings: GoogleFormsSettings | null + publishSettings: GoogleFormsPublishSettings | null + } +} + +// ============================================ +// Create Form Params & Response +// ============================================ + +export interface GoogleFormsCreateFormParams { + accessToken: string + title: string + documentTitle?: string + unpublished?: boolean +} + +export interface GoogleFormsCreateFormResponse extends ToolResponse { + output: { + formId: string + title: string | null + documentTitle: string | null + responderUri: string | null + revisionId: string | null + } +} + +// ============================================ +// Batch Update Params & Response +// ============================================ + +export interface GoogleFormsBatchUpdateRequest { + updateFormInfo?: { + info: Partial + updateMask: string + } + updateSettings?: { + settings: Partial + updateMask: string + } + createItem?: { + item: GoogleFormsItem + location: { index: number } + } + updateItem?: { + item: GoogleFormsItem + location: { index: number } + updateMask: string + } + moveItem?: { + originalLocation: { index: number } + newLocation: { index: number } + } + deleteItem?: { + location: { index: number } + } +} + +export interface GoogleFormsBatchUpdateParams { + accessToken: string + formId: string + requests: GoogleFormsBatchUpdateRequest[] + includeFormInResponse?: boolean +} + +export interface GoogleFormsBatchUpdateResponse extends ToolResponse { + output: { + replies: Record[] + writeControl: { + requiredRevisionId?: string + targetRevisionId?: string + } | null + form: GoogleForm | null + } +} + +// ============================================ +// Set Publish Settings Params & Response +// ============================================ + +export interface GoogleFormsSetPublishSettingsParams { + accessToken: string + formId: string + isPublished: boolean + isAcceptingResponses?: boolean +} + +export interface GoogleFormsSetPublishSettingsResponse extends ToolResponse { + output: { + formId: string + publishSettings: GoogleFormsPublishSettings + } +} + +// ============================================ +// Watch Params & Responses +// ============================================ + +export interface GoogleFormsCreateWatchParams { + accessToken: string + formId: string + eventType: 'SCHEMA' | 'RESPONSES' + topicName: string + watchId?: string +} + +export interface GoogleFormsCreateWatchResponse extends ToolResponse { + output: { + id: string + eventType: string + topicName: string | null + createTime: string | null + expireTime: string | null + state: string | null + } +} + +export interface GoogleFormsListWatchesParams { + accessToken: string + formId: string +} + +export interface GoogleFormsListWatchesResponse extends ToolResponse { + output: { + watches: GoogleFormsWatch[] + } +} + +export interface GoogleFormsDeleteWatchParams { + accessToken: string + formId: string + watchId: string +} + +export interface GoogleFormsDeleteWatchResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface GoogleFormsRenewWatchParams { + accessToken: string + formId: string + watchId: string +} + +export interface GoogleFormsRenewWatchResponse extends ToolResponse { + output: { + id: string + eventType: string | null + expireTime: string | null + state: string | null + } +} diff --git a/apps/sim/tools/google_form/utils.ts b/apps/sim/tools/google_form/utils.ts index 70dff427a1..0a7e155e21 100644 --- a/apps/sim/tools/google_form/utils.ts +++ b/apps/sim/tools/google_form/utils.ts @@ -22,3 +22,55 @@ export function buildGetResponseUrl(params: { formId: string; responseId: string logger.debug('Built Google Forms get response URL', { finalUrl }) return finalUrl } + +export function buildGetFormUrl(formId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}` + logger.debug('Built Google Forms get form URL', { finalUrl }) + return finalUrl +} + +export function buildCreateFormUrl(unpublished?: boolean): string { + const url = new URL(`${FORMS_API_BASE}/forms`) + if (unpublished) { + url.searchParams.set('unpublished', 'true') + } + const finalUrl = url.toString() + logger.debug('Built Google Forms create form URL', { finalUrl }) + return finalUrl +} + +export function buildBatchUpdateUrl(formId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}:batchUpdate` + logger.debug('Built Google Forms batch update URL', { finalUrl }) + return finalUrl +} + +export function buildSetPublishSettingsUrl(formId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}:setPublishSettings` + logger.debug('Built Google Forms set publish settings URL', { finalUrl }) + return finalUrl +} + +export function buildListWatchesUrl(formId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches` + logger.debug('Built Google Forms list watches URL', { finalUrl }) + return finalUrl +} + +export function buildCreateWatchUrl(formId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches` + logger.debug('Built Google Forms create watch URL', { finalUrl }) + return finalUrl +} + +export function buildDeleteWatchUrl(formId: string, watchId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches/${encodeURIComponent(watchId)}` + logger.debug('Built Google Forms delete watch URL', { finalUrl }) + return finalUrl +} + +export function buildRenewWatchUrl(formId: string, watchId: string): string { + const finalUrl = `${FORMS_API_BASE}/forms/${encodeURIComponent(formId)}/watches/${encodeURIComponent(watchId)}:renew` + logger.debug('Built Google Forms renew watch URL', { finalUrl }) + return finalUrl +} diff --git a/apps/sim/tools/google_groups/add_alias.ts b/apps/sim/tools/google_groups/add_alias.ts new file mode 100644 index 0000000000..6ea8ed30b9 --- /dev/null +++ b/apps/sim/tools/google_groups/add_alias.ts @@ -0,0 +1,75 @@ +import type { ToolConfig } from '@/tools/types' +import type { GoogleGroupsAddAliasParams, GoogleGroupsAddAliasResponse } from './types' + +export const addAliasTool: ToolConfig = { + id: 'google_groups_add_alias', + name: 'Google Groups Add Alias', + description: 'Add an email alias to a Google Group', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-groups', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + groupKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group email address or unique group ID', + }, + alias: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The email alias to add to the group', + }, + }, + + request: { + url: (params) => { + const encodedGroupKey = encodeURIComponent(params.groupKey.trim()) + return `https://admin.googleapis.com/admin/directory/v1/groups/${encodedGroupKey}/aliases` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + alias: params.alias.trim(), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to add group alias') + } + return { + success: true, + output: { + id: data.id ?? null, + primaryEmail: data.primaryEmail ?? null, + alias: data.alias ?? null, + kind: data.kind ?? null, + etag: data.etag ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique group identifier' }, + primaryEmail: { type: 'string', description: "Group's primary email address" }, + alias: { type: 'string', description: 'The alias that was added' }, + kind: { type: 'string', description: 'API resource type' }, + etag: { type: 'string', description: 'Resource version identifier' }, + }, +} diff --git a/apps/sim/tools/google_groups/get_settings.ts b/apps/sim/tools/google_groups/get_settings.ts new file mode 100644 index 0000000000..761ba54148 --- /dev/null +++ b/apps/sim/tools/google_groups/get_settings.ts @@ -0,0 +1,151 @@ +import type { ToolConfig } from '@/tools/types' +import type { GoogleGroupsGetSettingsParams, GoogleGroupsGetSettingsResponse } from './types' + +export const getSettingsTool: ToolConfig< + GoogleGroupsGetSettingsParams, + GoogleGroupsGetSettingsResponse +> = { + id: 'google_groups_get_settings', + name: 'Google Groups Get Settings', + description: + 'Get the settings for a Google Group including access permissions, moderation, and posting options', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-groups', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + groupEmail: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The email address of the group', + }, + }, + + request: { + url: (params) => { + const encodedEmail = encodeURIComponent(params.groupEmail.trim()) + return `https://www.googleapis.com/groups/v1/groups/${encodedEmail}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to get group settings') + } + return { + success: true, + output: { + email: data.email ?? null, + name: data.name ?? null, + description: data.description ?? null, + whoCanJoin: data.whoCanJoin ?? null, + whoCanViewMembership: data.whoCanViewMembership ?? null, + whoCanViewGroup: data.whoCanViewGroup ?? null, + whoCanPostMessage: data.whoCanPostMessage ?? null, + allowExternalMembers: data.allowExternalMembers ?? null, + allowWebPosting: data.allowWebPosting ?? null, + primaryLanguage: data.primaryLanguage ?? null, + isArchived: data.isArchived ?? null, + archiveOnly: data.archiveOnly ?? null, + messageModerationLevel: data.messageModerationLevel ?? null, + spamModerationLevel: data.spamModerationLevel ?? null, + replyTo: data.replyTo ?? null, + customReplyTo: data.customReplyTo ?? null, + includeCustomFooter: data.includeCustomFooter ?? null, + customFooterText: data.customFooterText ?? null, + sendMessageDenyNotification: data.sendMessageDenyNotification ?? null, + defaultMessageDenyNotificationText: data.defaultMessageDenyNotificationText ?? null, + membersCanPostAsTheGroup: data.membersCanPostAsTheGroup ?? null, + includeInGlobalAddressList: data.includeInGlobalAddressList ?? null, + whoCanLeaveGroup: data.whoCanLeaveGroup ?? null, + whoCanContactOwner: data.whoCanContactOwner ?? null, + favoriteRepliesOnTop: data.favoriteRepliesOnTop ?? null, + whoCanApproveMembers: data.whoCanApproveMembers ?? null, + whoCanBanUsers: data.whoCanBanUsers ?? null, + whoCanModerateMembers: data.whoCanModerateMembers ?? null, + whoCanModerateContent: data.whoCanModerateContent ?? null, + whoCanAssistContent: data.whoCanAssistContent ?? null, + enableCollaborativeInbox: data.enableCollaborativeInbox ?? null, + whoCanDiscoverGroup: data.whoCanDiscoverGroup ?? null, + defaultSender: data.defaultSender ?? null, + }, + } + }, + + outputs: { + email: { type: 'string', description: "The group's email address" }, + name: { type: 'string', description: 'The group name (max 75 characters)' }, + description: { type: 'string', description: 'The group description (max 4096 characters)' }, + whoCanJoin: { + type: 'string', + description: + 'Who can join the group (ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN)', + }, + whoCanViewMembership: { type: 'string', description: 'Who can view group membership' }, + whoCanViewGroup: { type: 'string', description: 'Who can view group messages' }, + whoCanPostMessage: { type: 'string', description: 'Who can post messages to the group' }, + allowExternalMembers: { type: 'string', description: 'Whether external users can be members' }, + allowWebPosting: { type: 'string', description: 'Whether web posting is allowed' }, + primaryLanguage: { type: 'string', description: "The group's primary language" }, + isArchived: { type: 'string', description: 'Whether messages are archived' }, + archiveOnly: { type: 'string', description: 'Whether the group is archive-only (inactive)' }, + messageModerationLevel: { type: 'string', description: 'Message moderation level' }, + spamModerationLevel: { + type: 'string', + description: 'Spam handling level (ALLOW, MODERATE, SILENTLY_MODERATE, REJECT)', + }, + replyTo: { type: 'string', description: 'Default reply destination' }, + customReplyTo: { type: 'string', description: 'Custom email for replies' }, + includeCustomFooter: { type: 'string', description: 'Whether to include custom footer' }, + customFooterText: { type: 'string', description: 'Custom footer text (max 1000 characters)' }, + sendMessageDenyNotification: { + type: 'string', + description: 'Whether to send rejection notifications', + }, + defaultMessageDenyNotificationText: { + type: 'string', + description: 'Default rejection message text', + }, + membersCanPostAsTheGroup: { + type: 'string', + description: 'Whether members can post as the group', + }, + includeInGlobalAddressList: { + type: 'string', + description: 'Whether included in Global Address List', + }, + whoCanLeaveGroup: { type: 'string', description: 'Who can leave the group' }, + whoCanContactOwner: { type: 'string', description: 'Who can contact the group owner' }, + favoriteRepliesOnTop: { type: 'string', description: 'Whether favorite replies appear at top' }, + whoCanApproveMembers: { type: 'string', description: 'Who can approve new members' }, + whoCanBanUsers: { type: 'string', description: 'Who can ban users' }, + whoCanModerateMembers: { type: 'string', description: 'Who can manage members' }, + whoCanModerateContent: { type: 'string', description: 'Who can moderate content' }, + whoCanAssistContent: { type: 'string', description: 'Who can assist with content metadata' }, + enableCollaborativeInbox: { + type: 'string', + description: 'Whether collaborative inbox is enabled', + }, + whoCanDiscoverGroup: { type: 'string', description: 'Who can discover the group' }, + defaultSender: { + type: 'string', + description: 'Default sender identity (DEFAULT_SELF or GROUP)', + }, + }, +} diff --git a/apps/sim/tools/google_groups/index.ts b/apps/sim/tools/google_groups/index.ts index c1f7aa42c9..fbedaa7c15 100644 --- a/apps/sim/tools/google_groups/index.ts +++ b/apps/sim/tools/google_groups/index.ts @@ -1,23 +1,33 @@ +import { addAliasTool } from './add_alias' import { addMemberTool } from './add_member' import { createGroupTool } from './create_group' import { deleteGroupTool } from './delete_group' import { getGroupTool } from './get_group' import { getMemberTool } from './get_member' +import { getSettingsTool } from './get_settings' import { hasMemberTool } from './has_member' +import { listAliasesTool } from './list_aliases' import { listGroupsTool } from './list_groups' import { listMembersTool } from './list_members' +import { removeAliasTool } from './remove_alias' import { removeMemberTool } from './remove_member' import { updateGroupTool } from './update_group' import { updateMemberTool } from './update_member' +import { updateSettingsTool } from './update_settings' +export const googleGroupsAddAliasTool = addAliasTool export const googleGroupsAddMemberTool = addMemberTool export const googleGroupsCreateGroupTool = createGroupTool export const googleGroupsDeleteGroupTool = deleteGroupTool export const googleGroupsGetGroupTool = getGroupTool export const googleGroupsGetMemberTool = getMemberTool +export const googleGroupsGetSettingsTool = getSettingsTool export const googleGroupsHasMemberTool = hasMemberTool +export const googleGroupsListAliasesTool = listAliasesTool export const googleGroupsListGroupsTool = listGroupsTool export const googleGroupsListMembersTool = listMembersTool +export const googleGroupsRemoveAliasTool = removeAliasTool export const googleGroupsRemoveMemberTool = removeMemberTool export const googleGroupsUpdateGroupTool = updateGroupTool export const googleGroupsUpdateMemberTool = updateMemberTool +export const googleGroupsUpdateSettingsTool = updateSettingsTool diff --git a/apps/sim/tools/google_groups/list_aliases.ts b/apps/sim/tools/google_groups/list_aliases.ts new file mode 100644 index 0000000000..d207ff6a49 --- /dev/null +++ b/apps/sim/tools/google_groups/list_aliases.ts @@ -0,0 +1,74 @@ +import type { ToolConfig } from '@/tools/types' +import type { GoogleGroupsListAliasesParams, GoogleGroupsListAliasesResponse } from './types' + +export const listAliasesTool: ToolConfig< + GoogleGroupsListAliasesParams, + GoogleGroupsListAliasesResponse +> = { + id: 'google_groups_list_aliases', + name: 'Google Groups List Aliases', + description: 'List all email aliases for a Google Group', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-groups', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + groupKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group email address or unique group ID', + }, + }, + + request: { + url: (params) => { + const encodedGroupKey = encodeURIComponent(params.groupKey.trim()) + return `https://admin.googleapis.com/admin/directory/v1/groups/${encodedGroupKey}/aliases` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to list group aliases') + } + return { + success: true, + output: { + aliases: data.aliases ?? [], + }, + } + }, + + outputs: { + aliases: { + type: 'array', + description: 'List of email aliases for the group', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Unique group identifier' }, + primaryEmail: { type: 'string', description: "Group's primary email address" }, + alias: { type: 'string', description: 'Alias email address' }, + kind: { type: 'string', description: 'API resource type' }, + etag: { type: 'string', description: 'Resource version identifier' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_groups/remove_alias.ts b/apps/sim/tools/google_groups/remove_alias.ts new file mode 100644 index 0000000000..46a503a1ba --- /dev/null +++ b/apps/sim/tools/google_groups/remove_alias.ts @@ -0,0 +1,68 @@ +import type { ToolConfig } from '@/tools/types' +import type { GoogleGroupsRemoveAliasParams, GoogleGroupsRemoveAliasResponse } from './types' + +export const removeAliasTool: ToolConfig< + GoogleGroupsRemoveAliasParams, + GoogleGroupsRemoveAliasResponse +> = { + id: 'google_groups_remove_alias', + name: 'Google Groups Remove Alias', + description: 'Remove an email alias from a Google Group', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-groups', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + groupKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group email address or unique group ID', + }, + alias: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The email alias to remove from the group', + }, + }, + + request: { + url: (params) => { + const encodedGroupKey = encodeURIComponent(params.groupKey.trim()) + const encodedAlias = encodeURIComponent(params.alias.trim()) + return `https://admin.googleapis.com/admin/directory/v1/groups/${encodedGroupKey}/aliases/${encodedAlias}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const data = await response.json() + throw new Error(data.error?.message || 'Failed to remove group alias') + } + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the alias was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/google_groups/types.ts b/apps/sim/tools/google_groups/types.ts index 5de0f4d74e..e56809f94e 100644 --- a/apps/sim/tools/google_groups/types.ts +++ b/apps/sim/tools/google_groups/types.ts @@ -103,9 +103,197 @@ export interface GoogleGroupsHasMemberParams extends GoogleGroupsCommonParams { memberKey: string } +/** + * Parameters for listing group aliases + */ +export interface GoogleGroupsListAliasesParams extends GoogleGroupsCommonParams { + groupKey: string +} + +/** + * Parameters for adding a group alias + */ +export interface GoogleGroupsAddAliasParams extends GoogleGroupsCommonParams { + groupKey: string + alias: string +} + +/** + * Parameters for removing a group alias + */ +export interface GoogleGroupsRemoveAliasParams extends GoogleGroupsCommonParams { + groupKey: string + alias: string +} + +/** + * Parameters for getting group settings + */ +export interface GoogleGroupsGetSettingsParams extends GoogleGroupsCommonParams { + groupEmail: string +} + +/** + * Parameters for updating group settings + */ +export interface GoogleGroupsUpdateSettingsParams extends GoogleGroupsCommonParams { + groupEmail: string + name?: string + description?: string + whoCanJoin?: string + whoCanViewMembership?: string + whoCanViewGroup?: string + whoCanPostMessage?: string + allowExternalMembers?: string + allowWebPosting?: string + primaryLanguage?: string + isArchived?: string + archiveOnly?: string + messageModerationLevel?: string + spamModerationLevel?: string + replyTo?: string + customReplyTo?: string + includeCustomFooter?: string + customFooterText?: string + sendMessageDenyNotification?: string + defaultMessageDenyNotificationText?: string + membersCanPostAsTheGroup?: string + includeInGlobalAddressList?: string + whoCanLeaveGroup?: string + whoCanContactOwner?: string + favoriteRepliesOnTop?: string + whoCanApproveMembers?: string + whoCanBanUsers?: string + whoCanModerateMembers?: string + whoCanModerateContent?: string + whoCanAssistContent?: string + enableCollaborativeInbox?: string + whoCanDiscoverGroup?: string + defaultSender?: string +} + /** * Standard response for Google Groups operations */ export interface GoogleGroupsResponse extends ToolResponse { output: Record } + +/** + * Response for listing group aliases + */ +export interface GoogleGroupsListAliasesResponse extends ToolResponse { + output: { + aliases: Array<{ + id?: string + primaryEmail?: string + alias?: string + kind?: string + etag?: string + }> + } +} + +/** + * Response for adding a group alias + */ +export interface GoogleGroupsAddAliasResponse extends ToolResponse { + output: { + id: string | null + primaryEmail: string | null + alias: string | null + kind: string | null + etag: string | null + } +} + +/** + * Response for removing a group alias + */ +export interface GoogleGroupsRemoveAliasResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +/** + * Response for getting group settings + */ +export interface GoogleGroupsGetSettingsResponse extends ToolResponse { + output: { + email: string | null + name: string | null + description: string | null + whoCanJoin: string | null + whoCanViewMembership: string | null + whoCanViewGroup: string | null + whoCanPostMessage: string | null + allowExternalMembers: string | null + allowWebPosting: string | null + primaryLanguage: string | null + isArchived: string | null + archiveOnly: string | null + messageModerationLevel: string | null + spamModerationLevel: string | null + replyTo: string | null + customReplyTo: string | null + includeCustomFooter: string | null + customFooterText: string | null + sendMessageDenyNotification: string | null + defaultMessageDenyNotificationText: string | null + membersCanPostAsTheGroup: string | null + includeInGlobalAddressList: string | null + whoCanLeaveGroup: string | null + whoCanContactOwner: string | null + favoriteRepliesOnTop: string | null + whoCanApproveMembers: string | null + whoCanBanUsers: string | null + whoCanModerateMembers: string | null + whoCanModerateContent: string | null + whoCanAssistContent: string | null + enableCollaborativeInbox: string | null + whoCanDiscoverGroup: string | null + defaultSender: string | null + } +} + +/** + * Response for updating group settings + */ +export interface GoogleGroupsUpdateSettingsResponse extends ToolResponse { + output: { + email: string | null + name: string | null + description: string | null + whoCanJoin: string | null + whoCanViewMembership: string | null + whoCanViewGroup: string | null + whoCanPostMessage: string | null + allowExternalMembers: string | null + allowWebPosting: string | null + primaryLanguage: string | null + isArchived: string | null + archiveOnly: string | null + messageModerationLevel: string | null + spamModerationLevel: string | null + replyTo: string | null + customReplyTo: string | null + includeCustomFooter: string | null + customFooterText: string | null + sendMessageDenyNotification: string | null + defaultMessageDenyNotificationText: string | null + membersCanPostAsTheGroup: string | null + includeInGlobalAddressList: string | null + whoCanLeaveGroup: string | null + whoCanContactOwner: string | null + favoriteRepliesOnTop: string | null + whoCanApproveMembers: string | null + whoCanBanUsers: string | null + whoCanModerateMembers: string | null + whoCanModerateContent: string | null + whoCanAssistContent: string | null + enableCollaborativeInbox: string | null + whoCanDiscoverGroup: string | null + defaultSender: string | null + } +} diff --git a/apps/sim/tools/google_groups/update_settings.ts b/apps/sim/tools/google_groups/update_settings.ts new file mode 100644 index 0000000000..143a7d93f7 --- /dev/null +++ b/apps/sim/tools/google_groups/update_settings.ts @@ -0,0 +1,396 @@ +import type { ToolConfig } from '@/tools/types' +import type { GoogleGroupsUpdateSettingsParams, GoogleGroupsUpdateSettingsResponse } from './types' + +export const updateSettingsTool: ToolConfig< + GoogleGroupsUpdateSettingsParams, + GoogleGroupsUpdateSettingsResponse +> = { + id: 'google_groups_update_settings', + name: 'Google Groups Update Settings', + description: + 'Update the settings for a Google Group including access permissions, moderation, and posting options', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-groups', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + groupEmail: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The email address of the group', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group name (max 75 characters)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The group description (max 4096 characters)', + }, + whoCanJoin: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can join: ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN', + }, + whoCanViewMembership: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can view membership: ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW', + }, + whoCanViewGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW', + }, + whoCanPostMessage: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can post: NONE_CAN_POST, ALL_MANAGERS_CAN_POST, ALL_MEMBERS_CAN_POST, ALL_OWNERS_CAN_POST, ALL_IN_DOMAIN_CAN_POST, ANYONE_CAN_POST', + }, + allowExternalMembers: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether external users can be members: true or false', + }, + allowWebPosting: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether web posting is allowed: true or false', + }, + primaryLanguage: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "The group's primary language (e.g., en)", + }, + isArchived: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether messages are archived: true or false', + }, + archiveOnly: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether the group is archive-only (inactive): true or false', + }, + messageModerationLevel: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Message moderation: MODERATE_ALL_MESSAGES, MODERATE_NON_MEMBERS, MODERATE_NEW_MEMBERS, MODERATE_NONE', + }, + spamModerationLevel: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Spam handling: ALLOW, MODERATE, SILENTLY_MODERATE, REJECT', + }, + replyTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Default reply: REPLY_TO_CUSTOM, REPLY_TO_SENDER, REPLY_TO_LIST, REPLY_TO_OWNER, REPLY_TO_IGNORE, REPLY_TO_MANAGERS', + }, + customReplyTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom email for replies (when replyTo is REPLY_TO_CUSTOM)', + }, + includeCustomFooter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include custom footer: true or false', + }, + customFooterText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom footer text (max 1000 characters)', + }, + sendMessageDenyNotification: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether to send rejection notifications: true or false', + }, + defaultMessageDenyNotificationText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Default rejection message text', + }, + membersCanPostAsTheGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether members can post as the group: true or false', + }, + includeInGlobalAddressList: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether included in Global Address List: true or false', + }, + whoCanLeaveGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Who can leave: ALL_MANAGERS_CAN_LEAVE, ALL_MEMBERS_CAN_LEAVE, NONE_CAN_LEAVE', + }, + whoCanContactOwner: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can contact owner: ALL_IN_DOMAIN_CAN_CONTACT, ALL_MANAGERS_CAN_CONTACT, ALL_MEMBERS_CAN_CONTACT, ANYONE_CAN_CONTACT', + }, + favoriteRepliesOnTop: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether favorite replies appear at top: true or false', + }, + whoCanApproveMembers: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can approve members: ALL_OWNERS_CAN_APPROVE, ALL_MANAGERS_CAN_APPROVE, ALL_MEMBERS_CAN_APPROVE, NONE_CAN_APPROVE', + }, + whoCanBanUsers: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE', + }, + whoCanModerateMembers: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Who can manage members: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE', + }, + whoCanModerateContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Who can moderate content: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE', + }, + whoCanAssistContent: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can assist with content metadata: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE', + }, + enableCollaborativeInbox: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Whether collaborative inbox is enabled: true or false', + }, + whoCanDiscoverGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can discover: ANYONE_CAN_DISCOVER, ALL_IN_DOMAIN_CAN_DISCOVER, ALL_MEMBERS_CAN_DISCOVER', + }, + defaultSender: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Default sender: DEFAULT_SELF or GROUP', + }, + }, + + request: { + url: (params) => { + const encodedEmail = encodeURIComponent(params.groupEmail.trim()) + return `https://www.googleapis.com/groups/v1/groups/${encodedEmail}` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.name !== undefined) body.name = params.name + if (params.description !== undefined) body.description = params.description + if (params.whoCanJoin !== undefined) body.whoCanJoin = params.whoCanJoin + if (params.whoCanViewMembership !== undefined) + body.whoCanViewMembership = params.whoCanViewMembership + if (params.whoCanViewGroup !== undefined) body.whoCanViewGroup = params.whoCanViewGroup + if (params.whoCanPostMessage !== undefined) body.whoCanPostMessage = params.whoCanPostMessage + if (params.allowExternalMembers !== undefined) + body.allowExternalMembers = params.allowExternalMembers + if (params.allowWebPosting !== undefined) body.allowWebPosting = params.allowWebPosting + if (params.primaryLanguage !== undefined) body.primaryLanguage = params.primaryLanguage + if (params.isArchived !== undefined) body.isArchived = params.isArchived + if (params.archiveOnly !== undefined) body.archiveOnly = params.archiveOnly + if (params.messageModerationLevel !== undefined) + body.messageModerationLevel = params.messageModerationLevel + if (params.spamModerationLevel !== undefined) + body.spamModerationLevel = params.spamModerationLevel + if (params.replyTo !== undefined) body.replyTo = params.replyTo + if (params.customReplyTo !== undefined) body.customReplyTo = params.customReplyTo + if (params.includeCustomFooter !== undefined) + body.includeCustomFooter = params.includeCustomFooter + if (params.customFooterText !== undefined) body.customFooterText = params.customFooterText + if (params.sendMessageDenyNotification !== undefined) + body.sendMessageDenyNotification = params.sendMessageDenyNotification + if (params.defaultMessageDenyNotificationText !== undefined) + body.defaultMessageDenyNotificationText = params.defaultMessageDenyNotificationText + if (params.membersCanPostAsTheGroup !== undefined) + body.membersCanPostAsTheGroup = params.membersCanPostAsTheGroup + if (params.includeInGlobalAddressList !== undefined) + body.includeInGlobalAddressList = params.includeInGlobalAddressList + if (params.whoCanLeaveGroup !== undefined) body.whoCanLeaveGroup = params.whoCanLeaveGroup + if (params.whoCanContactOwner !== undefined) + body.whoCanContactOwner = params.whoCanContactOwner + if (params.favoriteRepliesOnTop !== undefined) + body.favoriteRepliesOnTop = params.favoriteRepliesOnTop + if (params.whoCanApproveMembers !== undefined) + body.whoCanApproveMembers = params.whoCanApproveMembers + if (params.whoCanBanUsers !== undefined) body.whoCanBanUsers = params.whoCanBanUsers + if (params.whoCanModerateMembers !== undefined) + body.whoCanModerateMembers = params.whoCanModerateMembers + if (params.whoCanModerateContent !== undefined) + body.whoCanModerateContent = params.whoCanModerateContent + if (params.whoCanAssistContent !== undefined) + body.whoCanAssistContent = params.whoCanAssistContent + if (params.enableCollaborativeInbox !== undefined) + body.enableCollaborativeInbox = params.enableCollaborativeInbox + if (params.whoCanDiscoverGroup !== undefined) + body.whoCanDiscoverGroup = params.whoCanDiscoverGroup + if (params.defaultSender !== undefined) body.defaultSender = params.defaultSender + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error?.message || 'Failed to update group settings') + } + return { + success: true, + output: { + email: data.email ?? null, + name: data.name ?? null, + description: data.description ?? null, + whoCanJoin: data.whoCanJoin ?? null, + whoCanViewMembership: data.whoCanViewMembership ?? null, + whoCanViewGroup: data.whoCanViewGroup ?? null, + whoCanPostMessage: data.whoCanPostMessage ?? null, + allowExternalMembers: data.allowExternalMembers ?? null, + allowWebPosting: data.allowWebPosting ?? null, + primaryLanguage: data.primaryLanguage ?? null, + isArchived: data.isArchived ?? null, + archiveOnly: data.archiveOnly ?? null, + messageModerationLevel: data.messageModerationLevel ?? null, + spamModerationLevel: data.spamModerationLevel ?? null, + replyTo: data.replyTo ?? null, + customReplyTo: data.customReplyTo ?? null, + includeCustomFooter: data.includeCustomFooter ?? null, + customFooterText: data.customFooterText ?? null, + sendMessageDenyNotification: data.sendMessageDenyNotification ?? null, + defaultMessageDenyNotificationText: data.defaultMessageDenyNotificationText ?? null, + membersCanPostAsTheGroup: data.membersCanPostAsTheGroup ?? null, + includeInGlobalAddressList: data.includeInGlobalAddressList ?? null, + whoCanLeaveGroup: data.whoCanLeaveGroup ?? null, + whoCanContactOwner: data.whoCanContactOwner ?? null, + favoriteRepliesOnTop: data.favoriteRepliesOnTop ?? null, + whoCanApproveMembers: data.whoCanApproveMembers ?? null, + whoCanBanUsers: data.whoCanBanUsers ?? null, + whoCanModerateMembers: data.whoCanModerateMembers ?? null, + whoCanModerateContent: data.whoCanModerateContent ?? null, + whoCanAssistContent: data.whoCanAssistContent ?? null, + enableCollaborativeInbox: data.enableCollaborativeInbox ?? null, + whoCanDiscoverGroup: data.whoCanDiscoverGroup ?? null, + defaultSender: data.defaultSender ?? null, + }, + } + }, + + outputs: { + email: { type: 'string', description: "The group's email address" }, + name: { type: 'string', description: 'The group name' }, + description: { type: 'string', description: 'The group description' }, + whoCanJoin: { type: 'string', description: 'Who can join the group' }, + whoCanViewMembership: { type: 'string', description: 'Who can view group membership' }, + whoCanViewGroup: { type: 'string', description: 'Who can view group messages' }, + whoCanPostMessage: { type: 'string', description: 'Who can post messages to the group' }, + allowExternalMembers: { type: 'string', description: 'Whether external users can be members' }, + allowWebPosting: { type: 'string', description: 'Whether web posting is allowed' }, + primaryLanguage: { type: 'string', description: "The group's primary language" }, + isArchived: { type: 'string', description: 'Whether messages are archived' }, + archiveOnly: { type: 'string', description: 'Whether the group is archive-only' }, + messageModerationLevel: { type: 'string', description: 'Message moderation level' }, + spamModerationLevel: { type: 'string', description: 'Spam handling level' }, + replyTo: { type: 'string', description: 'Default reply destination' }, + customReplyTo: { type: 'string', description: 'Custom email for replies' }, + includeCustomFooter: { type: 'string', description: 'Whether to include custom footer' }, + customFooterText: { type: 'string', description: 'Custom footer text' }, + sendMessageDenyNotification: { + type: 'string', + description: 'Whether to send rejection notifications', + }, + defaultMessageDenyNotificationText: { + type: 'string', + description: 'Default rejection message text', + }, + membersCanPostAsTheGroup: { + type: 'string', + description: 'Whether members can post as the group', + }, + includeInGlobalAddressList: { + type: 'string', + description: 'Whether included in Global Address List', + }, + whoCanLeaveGroup: { type: 'string', description: 'Who can leave the group' }, + whoCanContactOwner: { type: 'string', description: 'Who can contact the group owner' }, + favoriteRepliesOnTop: { type: 'string', description: 'Whether favorite replies appear at top' }, + whoCanApproveMembers: { type: 'string', description: 'Who can approve new members' }, + whoCanBanUsers: { type: 'string', description: 'Who can ban users' }, + whoCanModerateMembers: { type: 'string', description: 'Who can manage members' }, + whoCanModerateContent: { type: 'string', description: 'Who can moderate content' }, + whoCanAssistContent: { type: 'string', description: 'Who can assist with content metadata' }, + enableCollaborativeInbox: { + type: 'string', + description: 'Whether collaborative inbox is enabled', + }, + whoCanDiscoverGroup: { type: 'string', description: 'Who can discover the group' }, + defaultSender: { type: 'string', description: 'Default sender identity' }, + }, +} diff --git a/apps/sim/tools/google_sheets/append.ts b/apps/sim/tools/google_sheets/append.ts index 86e59afebe..d5283512a4 100644 --- a/apps/sim/tools/google_sheets/append.ts +++ b/apps/sim/tools/google_sheets/append.ts @@ -1,6 +1,8 @@ import type { GoogleSheetsAppendResponse, GoogleSheetsToolParams, + GoogleSheetsV2AppendResponse, + GoogleSheetsV2ToolParams, } from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' @@ -226,3 +228,197 @@ export const appendTool: ToolConfig = { + id: 'google_sheets_append_v2', + name: 'Append to Google Sheets V2', + description: 'Append data to the end of a specific sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet to append to', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to append to', + }, + values: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.', + }, + valueInputOption: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The format of the data to append', + }, + insertDataOption: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'How to insert the data (OVERWRITE or INSERT_ROWS)', + }, + includeValuesInResponse: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Whether to include the appended values in the response', + }, + }, + + request: { + url: (params) => { + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + const url = new URL( + `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(sheetName)}:append` + ) + + const valueInputOption = params.valueInputOption || 'USER_ENTERED' + url.searchParams.append('valueInputOption', valueInputOption) + + if (params.insertDataOption) { + url.searchParams.append('insertDataOption', params.insertDataOption) + } + + if (params.includeValuesInResponse) { + url.searchParams.append('includeValuesInResponse', 'true') + } + + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let processedValues: any = params.values || [] + + if (typeof processedValues === 'string') { + try { + processedValues = JSON.parse(processedValues) + } catch (_error) { + try { + const sanitizedInput = (processedValues as string) + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + processedValues = JSON.parse(sanitizedInput) + } catch (_secondError) { + processedValues = [[processedValues]] + } + } + } + + if ( + Array.isArray(processedValues) && + processedValues.length > 0 && + typeof processedValues[0] === 'object' && + !Array.isArray(processedValues[0]) + ) { + const allKeys = new Set() + processedValues.forEach((obj: any) => { + if (obj && typeof obj === 'object') { + Object.keys(obj).forEach((key) => allKeys.add(key)) + } + }) + const headers = Array.from(allKeys) + + const rows = processedValues.map((obj: any) => { + if (!obj || typeof obj !== 'object') { + return Array(headers.length).fill('') + } + return headers.map((key) => { + const value = obj[key] + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value === undefined ? '' : value + }) + }) + + processedValues = [headers, ...rows] + } else if (!Array.isArray(processedValues)) { + processedValues = [[String(processedValues)]] + } else if (!processedValues.every((item: any) => Array.isArray(item))) { + processedValues = (processedValues as any[]).map((row: any) => + Array.isArray(row) ? row : [String(row)] + ) + } + + const body: Record = { + majorDimension: params.majorDimension || 'ROWS', + values: processedValues, + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const metadata = { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + } + + return { + success: true, + output: { + tableRange: data.tableRange ?? '', + updatedRange: data.updates?.updatedRange ?? '', + updatedRows: data.updates?.updatedRows ?? 0, + updatedColumns: data.updates?.updatedColumns ?? 0, + updatedCells: data.updates?.updatedCells ?? 0, + metadata: { + spreadsheetId: metadata.spreadsheetId, + spreadsheetUrl: metadata.spreadsheetUrl, + }, + }, + } + }, + + outputs: { + tableRange: { type: 'string', description: 'Range of the table where data was appended' }, + updatedRange: { type: 'string', description: 'Range of cells that were updated' }, + updatedRows: { type: 'number', description: 'Number of rows updated' }, + updatedColumns: { type: 'number', description: 'Number of columns updated' }, + updatedCells: { type: 'number', description: 'Number of cells updated' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/append_v2.ts b/apps/sim/tools/google_sheets/append_v2.ts deleted file mode 100644 index a3f64cb6c6..0000000000 --- a/apps/sim/tools/google_sheets/append_v2.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { - GoogleSheetsV2AppendResponse, - GoogleSheetsV2ToolParams, -} from '@/tools/google_sheets/types' -import type { ToolConfig } from '@/tools/types' - -export const appendV2Tool: ToolConfig = { - id: 'google_sheets_append_v2', - name: 'Append to Google Sheets V2', - description: 'Append data to the end of a specific sheet in a Google Sheets spreadsheet', - version: '2.0.0', - - oauth: { - required: true, - provider: 'google-sheets', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the Google Sheets API', - }, - spreadsheetId: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'The ID of the spreadsheet to append to', - }, - sheetName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the sheet/tab to append to', - }, - values: { - type: 'array', - required: true, - visibility: 'user-or-llm', - description: - 'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.', - }, - valueInputOption: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The format of the data to append', - }, - insertDataOption: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'How to insert the data (OVERWRITE or INSERT_ROWS)', - }, - includeValuesInResponse: { - type: 'boolean', - required: false, - visibility: 'hidden', - description: 'Whether to include the appended values in the response', - }, - }, - - request: { - url: (params) => { - const sheetName = params.sheetName?.trim() - if (!sheetName) { - throw new Error('Sheet name is required') - } - - const url = new URL( - `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(sheetName)}:append` - ) - - const valueInputOption = params.valueInputOption || 'USER_ENTERED' - url.searchParams.append('valueInputOption', valueInputOption) - - if (params.insertDataOption) { - url.searchParams.append('insertDataOption', params.insertDataOption) - } - - if (params.includeValuesInResponse) { - url.searchParams.append('includeValuesInResponse', 'true') - } - - return url.toString() - }, - method: 'POST', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }), - body: (params) => { - let processedValues: any = params.values || [] - - if (typeof processedValues === 'string') { - try { - processedValues = JSON.parse(processedValues) - } catch (_error) { - try { - const sanitizedInput = (processedValues as string) - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t') - processedValues = JSON.parse(sanitizedInput) - } catch (_secondError) { - processedValues = [[processedValues]] - } - } - } - - if ( - Array.isArray(processedValues) && - processedValues.length > 0 && - typeof processedValues[0] === 'object' && - !Array.isArray(processedValues[0]) - ) { - const allKeys = new Set() - processedValues.forEach((obj: any) => { - if (obj && typeof obj === 'object') { - Object.keys(obj).forEach((key) => allKeys.add(key)) - } - }) - const headers = Array.from(allKeys) - - const rows = processedValues.map((obj: any) => { - if (!obj || typeof obj !== 'object') { - return Array(headers.length).fill('') - } - return headers.map((key) => { - const value = obj[key] - if (value !== null && typeof value === 'object') { - return JSON.stringify(value) - } - return value === undefined ? '' : value - }) - }) - - processedValues = [headers, ...rows] - } else if (!Array.isArray(processedValues)) { - processedValues = [[String(processedValues)]] - } else if (!processedValues.every((item: any) => Array.isArray(item))) { - processedValues = (processedValues as any[]).map((row: any) => - Array.isArray(row) ? row : [String(row)] - ) - } - - const body: Record = { - majorDimension: params.majorDimension || 'ROWS', - values: processedValues, - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - - const metadata = { - spreadsheetId, - spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, - } - - return { - success: true, - output: { - tableRange: data.tableRange ?? '', - updatedRange: data.updates?.updatedRange ?? '', - updatedRows: data.updates?.updatedRows ?? 0, - updatedColumns: data.updates?.updatedColumns ?? 0, - updatedCells: data.updates?.updatedCells ?? 0, - metadata: { - spreadsheetId: metadata.spreadsheetId, - spreadsheetUrl: metadata.spreadsheetUrl, - }, - }, - } - }, - - outputs: { - tableRange: { type: 'string', description: 'Range of the table where data was appended' }, - updatedRange: { type: 'string', description: 'Range of cells that were updated' }, - updatedRows: { type: 'number', description: 'Number of rows updated' }, - updatedColumns: { type: 'number', description: 'Number of columns updated' }, - updatedCells: { type: 'number', description: 'Number of cells updated' }, - metadata: { - type: 'json', - description: 'Spreadsheet metadata including ID and URL', - properties: { - spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, - spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, - }, - }, - }, -} diff --git a/apps/sim/tools/google_sheets/batch_clear.ts b/apps/sim/tools/google_sheets/batch_clear.ts new file mode 100644 index 0000000000..ac76c60b6d --- /dev/null +++ b/apps/sim/tools/google_sheets/batch_clear.ts @@ -0,0 +1,112 @@ +import type { + GoogleSheetsV2BatchClearParams, + GoogleSheetsV2BatchClearResponse, +} from '@/tools/google_sheets/types' +import type { ToolConfig } from '@/tools/types' + +export const batchClearV2Tool: ToolConfig< + GoogleSheetsV2BatchClearParams, + GoogleSheetsV2BatchClearResponse +> = { + id: 'google_sheets_batch_clear_v2', + name: 'Batch Clear Google Sheets V2', + description: 'Clear multiple ranges in a Google Sheets spreadsheet in a single request', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + ranges: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of ranges to clear (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Each range should include sheet name.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchClear` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const ranges = params.ranges + if (!ranges || !Array.isArray(ranges) || ranges.length === 0) { + throw new Error('At least one range is required') + } + + return { + ranges, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const spreadsheetId = data.spreadsheetId ?? '' + const clearedRanges = data.clearedRanges ?? [] + + return { + success: true, + output: { + spreadsheetId, + clearedRanges, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The spreadsheet ID' }, + clearedRanges: { + type: 'array', + description: 'Array of ranges that were cleared', + items: { + type: 'string', + }, + }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/batch_get.ts b/apps/sim/tools/google_sheets/batch_get.ts new file mode 100644 index 0000000000..bb83bdf4ff --- /dev/null +++ b/apps/sim/tools/google_sheets/batch_get.ts @@ -0,0 +1,143 @@ +import type { + GoogleSheetsV2BatchGetParams, + GoogleSheetsV2BatchGetResponse, +} from '@/tools/google_sheets/types' +import type { ToolConfig } from '@/tools/types' + +export const batchGetV2Tool: ToolConfig< + GoogleSheetsV2BatchGetParams, + GoogleSheetsV2BatchGetResponse +> = { + id: 'google_sheets_batch_get_v2', + name: 'Batch Read Google Sheets V2', + description: 'Read multiple ranges from a Google Sheets spreadsheet in a single request', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + ranges: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of ranges to read (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Each range should include sheet name.', + }, + majorDimension: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The major dimension of values: "ROWS" (default) or "COLUMNS"', + }, + valueRenderOption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'How values should be rendered: "FORMATTED_VALUE" (default), "UNFORMATTED_VALUE", or "FORMULA"', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const ranges = params.ranges + if (!ranges || !Array.isArray(ranges) || ranges.length === 0) { + throw new Error('At least one range is required') + } + + const queryParams = new URLSearchParams() + ranges.forEach((range: string) => { + queryParams.append('ranges', range) + }) + + if (params.majorDimension) { + queryParams.append('majorDimension', params.majorDimension) + } + + if (params.valueRenderOption) { + queryParams.append('valueRenderOption', params.valueRenderOption) + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchGet?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const valueRanges = + data.valueRanges?.map((vr: any) => ({ + range: vr.range ?? '', + majorDimension: vr.majorDimension ?? 'ROWS', + values: vr.values ?? [], + })) ?? [] + + const spreadsheetId = data.spreadsheetId ?? '' + + return { + success: true, + output: { + spreadsheetId, + valueRanges, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The spreadsheet ID' }, + valueRanges: { + type: 'array', + description: 'Array of value ranges read from the spreadsheet', + items: { + type: 'object', + properties: { + range: { type: 'string', description: 'The range that was read' }, + majorDimension: { type: 'string', description: 'Major dimension (ROWS or COLUMNS)' }, + values: { type: 'array', description: 'The cell values as a 2D array' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/batch_update.ts b/apps/sim/tools/google_sheets/batch_update.ts new file mode 100644 index 0000000000..2b18975e71 --- /dev/null +++ b/apps/sim/tools/google_sheets/batch_update.ts @@ -0,0 +1,149 @@ +import type { + GoogleSheetsV2BatchUpdateParams, + GoogleSheetsV2BatchUpdateResponse, +} from '@/tools/google_sheets/types' +import type { ToolConfig } from '@/tools/types' + +export const batchUpdateV2Tool: ToolConfig< + GoogleSheetsV2BatchUpdateParams, + GoogleSheetsV2BatchUpdateResponse +> = { + id: 'google_sheets_batch_update_v2', + name: 'Batch Update Google Sheets V2', + description: 'Update multiple ranges in a Google Sheets spreadsheet in a single request', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + data: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of value ranges to update. Each item should have "range" (e.g., "Sheet1!A1:D10") and "values" (2D array).', + }, + valueInputOption: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'How input data should be interpreted: "RAW" or "USER_ENTERED" (default). USER_ENTERED parses formulas.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const data = params.data + if (!data || !Array.isArray(data) || data.length === 0) { + throw new Error('At least one data range is required') + } + + return { + valueInputOption: params.valueInputOption ?? 'USER_ENTERED', + data: data.map((item: any) => ({ + range: item.range, + values: item.values, + })), + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const responses = + data.responses?.map((r: any) => ({ + spreadsheetId: r.spreadsheetId ?? '', + updatedRange: r.updatedRange ?? '', + updatedRows: r.updatedRows ?? 0, + updatedColumns: r.updatedColumns ?? 0, + updatedCells: r.updatedCells ?? 0, + })) ?? [] + + const spreadsheetId = data.spreadsheetId ?? '' + + return { + success: true, + output: { + spreadsheetId, + totalUpdatedRows: data.totalUpdatedRows ?? 0, + totalUpdatedColumns: data.totalUpdatedColumns ?? 0, + totalUpdatedCells: data.totalUpdatedCells ?? 0, + totalUpdatedSheets: data.totalUpdatedSheets ?? 0, + responses, + metadata: { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + }, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The spreadsheet ID' }, + totalUpdatedRows: { type: 'number', description: 'Total number of rows updated' }, + totalUpdatedColumns: { type: 'number', description: 'Total number of columns updated' }, + totalUpdatedCells: { type: 'number', description: 'Total number of cells updated' }, + totalUpdatedSheets: { type: 'number', description: 'Total number of sheets updated' }, + responses: { + type: 'array', + description: 'Array of update responses for each range', + items: { + type: 'object', + properties: { + spreadsheetId: { type: 'string', description: 'The spreadsheet ID' }, + updatedRange: { type: 'string', description: 'The range that was updated' }, + updatedRows: { type: 'number', description: 'Number of rows updated in this range' }, + updatedColumns: { + type: 'number', + description: 'Number of columns updated in this range', + }, + updatedCells: { type: 'number', description: 'Number of cells updated in this range' }, + }, + }, + }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/read_v2.ts b/apps/sim/tools/google_sheets/clear.ts similarity index 66% rename from apps/sim/tools/google_sheets/read_v2.ts rename to apps/sim/tools/google_sheets/clear.ts index 8f06e7b666..5435e4e75a 100644 --- a/apps/sim/tools/google_sheets/read_v2.ts +++ b/apps/sim/tools/google_sheets/clear.ts @@ -1,13 +1,13 @@ import type { - GoogleSheetsV2ReadResponse, - GoogleSheetsV2ToolParams, + GoogleSheetsV2ClearParams, + GoogleSheetsV2ClearResponse, } from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' -export const readV2Tool: ToolConfig = { - id: 'google_sheets_read_v2', - name: 'Read from Google Sheets V2', - description: 'Read data from a specific sheet in a Google Sheets spreadsheet', +export const clearV2Tool: ToolConfig = { + id: 'google_sheets_clear_v2', + name: 'Clear Google Sheets Range V2', + description: 'Clear values from a specific range in a Google Sheets spreadsheet', version: '2.0.0', oauth: { @@ -32,14 +32,13 @@ export const readV2Tool: ToolConfig { if (!params.accessToken) { throw new Error('Access token is required') @@ -68,16 +67,16 @@ export const readV2Tool: ToolConfig ({}), }, - transformResponse: async (response: Response, params?: GoogleSheetsV2ToolParams) => { + transformResponse: async (response: Response, params?: GoogleSheetsV2ClearParams) => { const data = await response.json() - const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - + const spreadsheetId = params?.spreadsheetId ?? '' const metadata = { spreadsheetId, spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, @@ -86,9 +85,8 @@ export const readV2Tool: ToolConfig = { + id: 'google_sheets_copy_sheet_v2', + name: 'Copy Sheet V2', + description: 'Copy a sheet from one spreadsheet to another', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + sourceSpreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the source spreadsheet', + }, + sheetId: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the sheet to copy (numeric ID, not the sheet name). Use Get Spreadsheet to find sheet IDs.', + }, + destinationSpreadsheetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination spreadsheet where the sheet will be copied', + }, + }, + + request: { + url: (params) => { + const sourceSpreadsheetId = params.sourceSpreadsheetId?.trim() + if (!sourceSpreadsheetId) { + throw new Error('Source spreadsheet ID is required') + } + + const sheetId = params.sheetId + if (sheetId === undefined || sheetId === null) { + throw new Error('Sheet ID is required') + } + + return `https://sheets.googleapis.com/v4/spreadsheets/${sourceSpreadsheetId}/sheets/${sheetId}:copyTo` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const destinationSpreadsheetId = params.destinationSpreadsheetId?.trim() + if (!destinationSpreadsheetId) { + throw new Error('Destination spreadsheet ID is required') + } + + return { + destinationSpreadsheetId, + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2CopySheetParams) => { + const data = await response.json() + + return { + success: true, + output: { + sheetId: data.sheetId ?? 0, + title: data.title ?? '', + index: data.index ?? 0, + sheetType: data.sheetType ?? 'GRID', + destinationSpreadsheetId: params?.destinationSpreadsheetId ?? '', + destinationSpreadsheetUrl: `https://docs.google.com/spreadsheets/d/${params?.destinationSpreadsheetId ?? ''}`, + }, + } + }, + + outputs: { + sheetId: { + type: 'number', + description: 'The ID of the newly created sheet in the destination', + }, + title: { type: 'string', description: 'The title of the copied sheet' }, + index: { type: 'number', description: 'The index (position) of the copied sheet' }, + sheetType: { type: 'string', description: 'The type of the sheet (GRID, CHART, etc.)' }, + destinationSpreadsheetId: { + type: 'string', + description: 'The ID of the destination spreadsheet', + }, + destinationSpreadsheetUrl: { + type: 'string', + description: 'URL to the destination spreadsheet', + }, + }, +} diff --git a/apps/sim/tools/google_sheets/create_spreadsheet.ts b/apps/sim/tools/google_sheets/create_spreadsheet.ts new file mode 100644 index 0000000000..b403c3fe46 --- /dev/null +++ b/apps/sim/tools/google_sheets/create_spreadsheet.ts @@ -0,0 +1,139 @@ +import type { + GoogleSheetsV2CreateSpreadsheetParams, + GoogleSheetsV2CreateSpreadsheetResponse, +} from '@/tools/google_sheets/types' +import type { ToolConfig } from '@/tools/types' + +export const createSpreadsheetV2Tool: ToolConfig< + GoogleSheetsV2CreateSpreadsheetParams, + GoogleSheetsV2CreateSpreadsheetResponse +> = { + id: 'google_sheets_create_spreadsheet_v2', + name: 'Create Spreadsheet V2', + description: 'Create a new Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the new spreadsheet', + }, + sheetTitles: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Array of sheet names to create (e.g., ["Sheet1", "Data", "Summary"]). Defaults to a single "Sheet1".', + }, + locale: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The locale of the spreadsheet (e.g., "en_US")', + }, + timeZone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The time zone of the spreadsheet (e.g., "America/New_York")', + }, + }, + + request: { + url: () => 'https://sheets.googleapis.com/v4/spreadsheets', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const title = params.title?.trim() + if (!title) { + throw new Error('Spreadsheet title is required') + } + + const sheetTitles = params.sheetTitles ?? ['Sheet1'] + const sheets = sheetTitles.map((sheetTitle: string, index: number) => ({ + properties: { + title: sheetTitle, + index, + }, + })) + + const body: any = { + properties: { + title, + }, + sheets, + } + + if (params.locale) { + body.properties.locale = params.locale + } + + if (params.timeZone) { + body.properties.timeZone = params.timeZone + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const sheets = + data.sheets?.map((sheet: any) => ({ + sheetId: sheet.properties?.sheetId ?? 0, + title: sheet.properties?.title ?? '', + index: sheet.properties?.index ?? 0, + })) ?? [] + + return { + success: true, + output: { + spreadsheetId: data.spreadsheetId ?? '', + title: data.properties?.title ?? '', + spreadsheetUrl: data.spreadsheetUrl ?? '', + sheets, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The ID of the created spreadsheet' }, + title: { type: 'string', description: 'The title of the created spreadsheet' }, + spreadsheetUrl: { type: 'string', description: 'URL to the created spreadsheet' }, + sheets: { + type: 'array', + description: 'List of sheets created in the spreadsheet', + items: { + type: 'object', + properties: { + sheetId: { type: 'number', description: 'The sheet ID' }, + title: { type: 'string', description: 'The sheet title/name' }, + index: { type: 'number', description: 'The sheet index (position)' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/get_spreadsheet.ts b/apps/sim/tools/google_sheets/get_spreadsheet.ts new file mode 100644 index 0000000000..afb06a91db --- /dev/null +++ b/apps/sim/tools/google_sheets/get_spreadsheet.ts @@ -0,0 +1,112 @@ +import type { + GoogleSheetsV2GetSpreadsheetParams, + GoogleSheetsV2GetSpreadsheetResponse, +} from '@/tools/google_sheets/types' +import type { ToolConfig } from '@/tools/types' + +export const getSpreadsheetV2Tool: ToolConfig< + GoogleSheetsV2GetSpreadsheetParams, + GoogleSheetsV2GetSpreadsheetResponse +> = { + id: 'google_sheets_get_spreadsheet_v2', + name: 'Get Spreadsheet Info V2', + description: 'Get metadata about a Google Sheets spreadsheet including title and sheet list', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + includeGridData: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include grid data (cell values). Defaults to false.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const includeGridData = params.includeGridData ? 'true' : 'false' + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}?includeGridData=${includeGridData}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const sheets = + data.sheets?.map((sheet: any) => ({ + sheetId: sheet.properties?.sheetId ?? 0, + title: sheet.properties?.title ?? '', + index: sheet.properties?.index ?? 0, + rowCount: sheet.properties?.gridProperties?.rowCount ?? null, + columnCount: sheet.properties?.gridProperties?.columnCount ?? null, + hidden: sheet.properties?.hidden ?? false, + })) ?? [] + + return { + success: true, + output: { + spreadsheetId: data.spreadsheetId ?? '', + title: data.properties?.title ?? '', + locale: data.properties?.locale ?? null, + timeZone: data.properties?.timeZone ?? null, + spreadsheetUrl: data.spreadsheetUrl ?? '', + sheets, + }, + } + }, + + outputs: { + spreadsheetId: { type: 'string', description: 'The spreadsheet ID' }, + title: { type: 'string', description: 'The title of the spreadsheet' }, + locale: { type: 'string', description: 'The locale of the spreadsheet', optional: true }, + timeZone: { type: 'string', description: 'The time zone of the spreadsheet', optional: true }, + spreadsheetUrl: { type: 'string', description: 'URL to the spreadsheet' }, + sheets: { + type: 'array', + description: 'List of sheets in the spreadsheet', + items: { + type: 'object', + properties: { + sheetId: { type: 'number', description: 'The sheet ID' }, + title: { type: 'string', description: 'The sheet title/name' }, + index: { type: 'number', description: 'The sheet index (position)' }, + rowCount: { type: 'number', description: 'Number of rows in the sheet' }, + columnCount: { type: 'number', description: 'Number of columns in the sheet' }, + hidden: { type: 'boolean', description: 'Whether the sheet is hidden' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/index.ts b/apps/sim/tools/google_sheets/index.ts index e8612b80de..750e0a3b26 100644 --- a/apps/sim/tools/google_sheets/index.ts +++ b/apps/sim/tools/google_sheets/index.ts @@ -1,11 +1,14 @@ -import { appendTool } from '@/tools/google_sheets/append' -import { appendV2Tool } from '@/tools/google_sheets/append_v2' -import { readTool } from '@/tools/google_sheets/read' -import { readV2Tool } from '@/tools/google_sheets/read_v2' -import { updateTool } from '@/tools/google_sheets/update' -import { updateV2Tool } from '@/tools/google_sheets/update_v2' -import { writeTool } from '@/tools/google_sheets/write' -import { writeV2Tool } from '@/tools/google_sheets/write_v2' +import { appendTool, appendV2Tool } from '@/tools/google_sheets/append' +import { batchClearV2Tool } from '@/tools/google_sheets/batch_clear' +import { batchGetV2Tool } from '@/tools/google_sheets/batch_get' +import { batchUpdateV2Tool } from '@/tools/google_sheets/batch_update' +import { clearV2Tool } from '@/tools/google_sheets/clear' +import { copySheetV2Tool } from '@/tools/google_sheets/copy_sheet' +import { createSpreadsheetV2Tool } from '@/tools/google_sheets/create_spreadsheet' +import { getSpreadsheetV2Tool } from '@/tools/google_sheets/get_spreadsheet' +import { readTool, readV2Tool } from '@/tools/google_sheets/read' +import { updateTool, updateV2Tool } from '@/tools/google_sheets/update' +import { writeTool, writeV2Tool } from '@/tools/google_sheets/write' // V1 exports export const googleSheetsReadTool = readTool @@ -18,3 +21,10 @@ export const googleSheetsReadV2Tool = readV2Tool export const googleSheetsWriteV2Tool = writeV2Tool export const googleSheetsUpdateV2Tool = updateV2Tool export const googleSheetsAppendV2Tool = appendV2Tool +export const googleSheetsClearV2Tool = clearV2Tool +export const googleSheetsGetSpreadsheetV2Tool = getSpreadsheetV2Tool +export const googleSheetsCreateSpreadsheetV2Tool = createSpreadsheetV2Tool +export const googleSheetsBatchGetV2Tool = batchGetV2Tool +export const googleSheetsBatchUpdateV2Tool = batchUpdateV2Tool +export const googleSheetsBatchClearV2Tool = batchClearV2Tool +export const googleSheetsCopySheetV2Tool = copySheetV2Tool diff --git a/apps/sim/tools/google_sheets/read.ts b/apps/sim/tools/google_sheets/read.ts index 1e591eac6f..11b521aa2c 100644 --- a/apps/sim/tools/google_sheets/read.ts +++ b/apps/sim/tools/google_sheets/read.ts @@ -1,4 +1,9 @@ -import type { GoogleSheetsReadResponse, GoogleSheetsToolParams } from '@/tools/google_sheets/types' +import type { + GoogleSheetsReadResponse, + GoogleSheetsToolParams, + GoogleSheetsV2ReadResponse, + GoogleSheetsV2ToolParams, +} from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' export const readTool: ToolConfig = { @@ -111,3 +116,111 @@ export const readTool: ToolConfig = { + id: 'google_sheets_read_v2', + name: 'Read from Google Sheets V2', + description: 'Read data from a specific sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to read from', + }, + cellRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to read (e.g. "A1:D10"). Defaults to "A1:Z1000" if not specified.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + const cellRange = params.cellRange?.trim() || 'A1:Z1000' + const fullRange = `${sheetName}!${cellRange}` + + return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(fullRange)}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: GoogleSheetsV2ToolParams) => { + const data = await response.json() + + const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const metadata = { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + } + + return { + success: true, + output: { + sheetName: params?.sheetName ?? '', + range: data.range ?? '', + values: data.values ?? [], + metadata: { + spreadsheetId: metadata.spreadsheetId, + spreadsheetUrl: metadata.spreadsheetUrl, + }, + }, + } + }, + + outputs: { + sheetName: { type: 'string', description: 'Name of the sheet that was read' }, + range: { type: 'string', description: 'The range of cells that was read' }, + values: { type: 'array', description: 'The cell values as a 2D array' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/types.ts b/apps/sim/tools/google_sheets/types.ts index 8d51ae065d..74fdc84a1a 100644 --- a/apps/sim/tools/google_sheets/types.ts +++ b/apps/sim/tools/google_sheets/types.ts @@ -136,3 +136,157 @@ export type GoogleSheetsV2Response = | GoogleSheetsV2WriteResponse | GoogleSheetsV2UpdateResponse | GoogleSheetsV2AppendResponse + | GoogleSheetsV2ClearResponse + | GoogleSheetsV2GetSpreadsheetResponse + | GoogleSheetsV2CreateSpreadsheetResponse + | GoogleSheetsV2BatchGetResponse + | GoogleSheetsV2BatchUpdateResponse + | GoogleSheetsV2BatchClearResponse + | GoogleSheetsV2CopySheetResponse + +// V2 Clear Types +export interface GoogleSheetsV2ClearParams { + accessToken: string + spreadsheetId: string + sheetName: string + cellRange?: string +} + +export interface GoogleSheetsV2ClearResponse extends ToolResponse { + output: { + clearedRange: string + sheetName: string + metadata: GoogleSheetsMetadata + } +} + +// V2 Get Spreadsheet Types +export interface GoogleSheetsV2GetSpreadsheetParams { + accessToken: string + spreadsheetId: string + includeGridData?: boolean +} + +export interface GoogleSheetsV2GetSpreadsheetResponse extends ToolResponse { + output: { + spreadsheetId: string + title: string + locale: string | null + timeZone: string | null + spreadsheetUrl: string + sheets: { + sheetId: number + title: string + index: number + rowCount: number | null + columnCount: number | null + hidden: boolean + }[] + } +} + +// V2 Create Spreadsheet Types +export interface GoogleSheetsV2CreateSpreadsheetParams { + accessToken: string + title: string + sheetTitles?: string[] + locale?: string + timeZone?: string +} + +export interface GoogleSheetsV2CreateSpreadsheetResponse extends ToolResponse { + output: { + spreadsheetId: string + title: string + spreadsheetUrl: string + sheets: { + sheetId: number + title: string + index: number + }[] + } +} + +// V2 Batch Get Types +export interface GoogleSheetsV2BatchGetParams { + accessToken: string + spreadsheetId: string + ranges: string[] + majorDimension?: 'ROWS' | 'COLUMNS' + valueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA' +} + +export interface GoogleSheetsV2BatchGetResponse extends ToolResponse { + output: { + spreadsheetId: string + valueRanges: { + range: string + majorDimension: string + values: any[][] + }[] + metadata: GoogleSheetsMetadata + } +} + +// V2 Batch Update Types +export interface GoogleSheetsV2BatchUpdateParams { + accessToken: string + spreadsheetId: string + data: { + range: string + values: any[][] + }[] + valueInputOption?: 'RAW' | 'USER_ENTERED' +} + +export interface GoogleSheetsV2BatchUpdateResponse extends ToolResponse { + output: { + spreadsheetId: string + totalUpdatedRows: number + totalUpdatedColumns: number + totalUpdatedCells: number + totalUpdatedSheets: number + responses: { + spreadsheetId: string + updatedRange: string + updatedRows: number + updatedColumns: number + updatedCells: number + }[] + metadata: GoogleSheetsMetadata + } +} + +// V2 Batch Clear Types +export interface GoogleSheetsV2BatchClearParams { + accessToken: string + spreadsheetId: string + ranges: string[] +} + +export interface GoogleSheetsV2BatchClearResponse extends ToolResponse { + output: { + spreadsheetId: string + clearedRanges: string[] + metadata: GoogleSheetsMetadata + } +} + +// V2 Copy Sheet Types +export interface GoogleSheetsV2CopySheetParams { + accessToken: string + sourceSpreadsheetId: string + sheetId: number + destinationSpreadsheetId: string +} + +export interface GoogleSheetsV2CopySheetResponse extends ToolResponse { + output: { + sheetId: number + title: string + index: number + sheetType: string + destinationSpreadsheetId: string + destinationSpreadsheetUrl: string + } +} diff --git a/apps/sim/tools/google_sheets/update.ts b/apps/sim/tools/google_sheets/update.ts index 07eb18dd36..c3d100bea7 100644 --- a/apps/sim/tools/google_sheets/update.ts +++ b/apps/sim/tools/google_sheets/update.ts @@ -1,6 +1,8 @@ import type { GoogleSheetsToolParams, GoogleSheetsUpdateResponse, + GoogleSheetsV2ToolParams, + GoogleSheetsV2UpdateResponse, } from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' @@ -179,3 +181,183 @@ export const updateTool: ToolConfig = { + id: 'google_sheets_update_v2', + name: 'Update Google Sheets V2', + description: 'Update data in a specific sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet to update', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to update', + }, + cellRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to update (e.g. "A1:D10", "A1"). Defaults to "A1" if not specified.', + }, + values: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'The data to update as a 2D array (e.g. [["Name", "Age"], ["Alice", 30]]) or array of objects.', + }, + valueInputOption: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The format of the data to update', + }, + includeValuesInResponse: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Whether to include the updated values in the response', + }, + }, + + request: { + url: (params) => { + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + const cellRange = params.cellRange?.trim() || 'A1' + const fullRange = `${sheetName}!${cellRange}` + + const url = new URL( + `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}` + ) + + const valueInputOption = params.valueInputOption || 'USER_ENTERED' + url.searchParams.append('valueInputOption', valueInputOption) + + if (params.includeValuesInResponse) { + url.searchParams.append('includeValuesInResponse', 'true') + } + + return url.toString() + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let processedValues: any = params.values || [] + + // Minimal shape enforcement: Google requires a 2D array + if (!Array.isArray(processedValues)) { + processedValues = [[processedValues]] + } else if (!processedValues.every((item: any) => Array.isArray(item))) { + processedValues = (processedValues as any[]).map((row: any) => + Array.isArray(row) ? row : [row] + ) + } + + // Handle array of objects + if ( + Array.isArray(processedValues) && + processedValues.length > 0 && + typeof processedValues[0] === 'object' && + !Array.isArray(processedValues[0]) + ) { + const allKeys = new Set() + processedValues.forEach((obj: any) => { + if (obj && typeof obj === 'object') { + Object.keys(obj).forEach((key) => allKeys.add(key)) + } + }) + const headers = Array.from(allKeys) + + const rows = processedValues.map((obj: any) => { + if (!obj || typeof obj !== 'object') { + return Array(headers.length).fill('') + } + return headers.map((key) => { + const value = obj[key] + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value === undefined ? '' : value + }) + }) + + processedValues = [headers, ...rows] + } + + const body: Record = { + majorDimension: params.majorDimension || 'ROWS', + values: processedValues, + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const metadata = { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + } + + return { + success: true, + output: { + updatedRange: data.updatedRange ?? null, + updatedRows: data.updatedRows ?? 0, + updatedColumns: data.updatedColumns ?? 0, + updatedCells: data.updatedCells ?? 0, + metadata: { + spreadsheetId: metadata.spreadsheetId, + spreadsheetUrl: metadata.spreadsheetUrl, + }, + }, + } + }, + + outputs: { + updatedRange: { type: 'string', description: 'Range of cells that were updated' }, + updatedRows: { type: 'number', description: 'Number of rows updated' }, + updatedColumns: { type: 'number', description: 'Number of columns updated' }, + updatedCells: { type: 'number', description: 'Number of cells updated' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/update_v2.ts b/apps/sim/tools/google_sheets/update_v2.ts deleted file mode 100644 index 55a7b347af..0000000000 --- a/apps/sim/tools/google_sheets/update_v2.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { - GoogleSheetsV2ToolParams, - GoogleSheetsV2UpdateResponse, -} from '@/tools/google_sheets/types' -import type { ToolConfig } from '@/tools/types' - -export const updateV2Tool: ToolConfig = { - id: 'google_sheets_update_v2', - name: 'Update Google Sheets V2', - description: 'Update data in a specific sheet in a Google Sheets spreadsheet', - version: '2.0.0', - - oauth: { - required: true, - provider: 'google-sheets', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the Google Sheets API', - }, - spreadsheetId: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'The ID of the spreadsheet to update', - }, - sheetName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the sheet/tab to update', - }, - cellRange: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'The cell range to update (e.g. "A1:D10", "A1"). Defaults to "A1" if not specified.', - }, - values: { - type: 'array', - required: true, - visibility: 'user-or-llm', - description: - 'The data to update as a 2D array (e.g. [["Name", "Age"], ["Alice", 30]]) or array of objects.', - }, - valueInputOption: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The format of the data to update', - }, - includeValuesInResponse: { - type: 'boolean', - required: false, - visibility: 'hidden', - description: 'Whether to include the updated values in the response', - }, - }, - - request: { - url: (params) => { - const sheetName = params.sheetName?.trim() - if (!sheetName) { - throw new Error('Sheet name is required') - } - - const cellRange = params.cellRange?.trim() || 'A1' - const fullRange = `${sheetName}!${cellRange}` - - const url = new URL( - `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}` - ) - - const valueInputOption = params.valueInputOption || 'USER_ENTERED' - url.searchParams.append('valueInputOption', valueInputOption) - - if (params.includeValuesInResponse) { - url.searchParams.append('includeValuesInResponse', 'true') - } - - return url.toString() - }, - method: 'PUT', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }), - body: (params) => { - let processedValues: any = params.values || [] - - // Minimal shape enforcement: Google requires a 2D array - if (!Array.isArray(processedValues)) { - processedValues = [[processedValues]] - } else if (!processedValues.every((item: any) => Array.isArray(item))) { - processedValues = (processedValues as any[]).map((row: any) => - Array.isArray(row) ? row : [row] - ) - } - - // Handle array of objects - if ( - Array.isArray(processedValues) && - processedValues.length > 0 && - typeof processedValues[0] === 'object' && - !Array.isArray(processedValues[0]) - ) { - const allKeys = new Set() - processedValues.forEach((obj: any) => { - if (obj && typeof obj === 'object') { - Object.keys(obj).forEach((key) => allKeys.add(key)) - } - }) - const headers = Array.from(allKeys) - - const rows = processedValues.map((obj: any) => { - if (!obj || typeof obj !== 'object') { - return Array(headers.length).fill('') - } - return headers.map((key) => { - const value = obj[key] - if (value !== null && typeof value === 'object') { - return JSON.stringify(value) - } - return value === undefined ? '' : value - }) - }) - - processedValues = [headers, ...rows] - } - - const body: Record = { - majorDimension: params.majorDimension || 'ROWS', - values: processedValues, - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - - const metadata = { - spreadsheetId, - spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, - } - - return { - success: true, - output: { - updatedRange: data.updatedRange ?? null, - updatedRows: data.updatedRows ?? 0, - updatedColumns: data.updatedColumns ?? 0, - updatedCells: data.updatedCells ?? 0, - metadata: { - spreadsheetId: metadata.spreadsheetId, - spreadsheetUrl: metadata.spreadsheetUrl, - }, - }, - } - }, - - outputs: { - updatedRange: { type: 'string', description: 'Range of cells that were updated' }, - updatedRows: { type: 'number', description: 'Number of rows updated' }, - updatedColumns: { type: 'number', description: 'Number of columns updated' }, - updatedCells: { type: 'number', description: 'Number of cells updated' }, - metadata: { - type: 'json', - description: 'Spreadsheet metadata including ID and URL', - properties: { - spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, - spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, - }, - }, - }, -} diff --git a/apps/sim/tools/google_sheets/write.ts b/apps/sim/tools/google_sheets/write.ts index 3115418cac..154b991d86 100644 --- a/apps/sim/tools/google_sheets/write.ts +++ b/apps/sim/tools/google_sheets/write.ts @@ -1,4 +1,9 @@ -import type { GoogleSheetsToolParams, GoogleSheetsWriteResponse } from '@/tools/google_sheets/types' +import type { + GoogleSheetsToolParams, + GoogleSheetsV2ToolParams, + GoogleSheetsV2WriteResponse, + GoogleSheetsWriteResponse, +} from '@/tools/google_sheets/types' import type { ToolConfig } from '@/tools/types' export const writeTool: ToolConfig = { @@ -177,3 +182,175 @@ export const writeTool: ToolConfig = { + id: 'google_sheets_write_v2', + name: 'Write to Google Sheets V2', + description: 'Write data to a specific sheet in a Google Sheets spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'google-sheets', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Sheets API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to write to', + }, + cellRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to write to (e.g. "A1:D10", "A1"). Defaults to "A1" if not specified.', + }, + values: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.', + }, + valueInputOption: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The format of the data to write', + }, + includeValuesInResponse: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Whether to include the written values in the response', + }, + }, + + request: { + url: (params) => { + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + // Build the range: SheetName!CellRange or just SheetName!A1 + const cellRange = params.cellRange?.trim() || 'A1' + const fullRange = `${sheetName}!${cellRange}` + + const url = new URL( + `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}` + ) + + const valueInputOption = params.valueInputOption || 'USER_ENTERED' + url.searchParams.append('valueInputOption', valueInputOption) + + if (params.includeValuesInResponse) { + url.searchParams.append('includeValuesInResponse', 'true') + } + + return url.toString() + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let processedValues: any = params.values || [] + + // Handle array of objects + if ( + Array.isArray(processedValues) && + processedValues.length > 0 && + typeof processedValues[0] === 'object' && + !Array.isArray(processedValues[0]) + ) { + const allKeys = new Set() + processedValues.forEach((obj: any) => { + if (obj && typeof obj === 'object') { + Object.keys(obj).forEach((key) => allKeys.add(key)) + } + }) + const headers = Array.from(allKeys) + + const rows = processedValues.map((obj: any) => { + if (!obj || typeof obj !== 'object') { + return Array(headers.length).fill('') + } + return headers.map((key) => { + const value = obj[key] + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value === undefined ? '' : value + }) + }) + + processedValues = [headers, ...rows] + } + + const body: Record = { + majorDimension: params.majorDimension || 'ROWS', + values: processedValues, + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const metadata = { + spreadsheetId, + spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, + } + + return { + success: true, + output: { + updatedRange: data.updatedRange ?? null, + updatedRows: data.updatedRows ?? 0, + updatedColumns: data.updatedColumns ?? 0, + updatedCells: data.updatedCells ?? 0, + metadata: { + spreadsheetId: metadata.spreadsheetId, + spreadsheetUrl: metadata.spreadsheetUrl, + }, + }, + } + }, + + outputs: { + updatedRange: { type: 'string', description: 'Range of cells that were updated' }, + updatedRows: { type: 'number', description: 'Number of rows updated' }, + updatedColumns: { type: 'number', description: 'Number of columns updated' }, + updatedCells: { type: 'number', description: 'Number of cells updated' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_sheets/write_v2.ts b/apps/sim/tools/google_sheets/write_v2.ts deleted file mode 100644 index f72ac5ba4b..0000000000 --- a/apps/sim/tools/google_sheets/write_v2.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { - GoogleSheetsV2ToolParams, - GoogleSheetsV2WriteResponse, -} from '@/tools/google_sheets/types' -import type { ToolConfig } from '@/tools/types' - -export const writeV2Tool: ToolConfig = { - id: 'google_sheets_write_v2', - name: 'Write to Google Sheets V2', - description: 'Write data to a specific sheet in a Google Sheets spreadsheet', - version: '2.0.0', - - oauth: { - required: true, - provider: 'google-sheets', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the Google Sheets API', - }, - spreadsheetId: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'The ID of the spreadsheet', - }, - sheetName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the sheet/tab to write to', - }, - cellRange: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'The cell range to write to (e.g. "A1:D10", "A1"). Defaults to "A1" if not specified.', - }, - values: { - type: 'array', - required: true, - visibility: 'user-or-llm', - description: - 'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.', - }, - valueInputOption: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The format of the data to write', - }, - includeValuesInResponse: { - type: 'boolean', - required: false, - visibility: 'hidden', - description: 'Whether to include the written values in the response', - }, - }, - - request: { - url: (params) => { - const sheetName = params.sheetName?.trim() - if (!sheetName) { - throw new Error('Sheet name is required') - } - - // Build the range: SheetName!CellRange or just SheetName!A1 - const cellRange = params.cellRange?.trim() || 'A1' - const fullRange = `${sheetName}!${cellRange}` - - const url = new URL( - `https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}` - ) - - const valueInputOption = params.valueInputOption || 'USER_ENTERED' - url.searchParams.append('valueInputOption', valueInputOption) - - if (params.includeValuesInResponse) { - url.searchParams.append('includeValuesInResponse', 'true') - } - - return url.toString() - }, - method: 'PUT', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }), - body: (params) => { - let processedValues: any = params.values || [] - - // Handle array of objects - if ( - Array.isArray(processedValues) && - processedValues.length > 0 && - typeof processedValues[0] === 'object' && - !Array.isArray(processedValues[0]) - ) { - const allKeys = new Set() - processedValues.forEach((obj: any) => { - if (obj && typeof obj === 'object') { - Object.keys(obj).forEach((key) => allKeys.add(key)) - } - }) - const headers = Array.from(allKeys) - - const rows = processedValues.map((obj: any) => { - if (!obj || typeof obj !== 'object') { - return Array(headers.length).fill('') - } - return headers.map((key) => { - const value = obj[key] - if (value !== null && typeof value === 'object') { - return JSON.stringify(value) - } - return value === undefined ? '' : value - }) - }) - - processedValues = [headers, ...rows] - } - - const body: Record = { - majorDimension: params.majorDimension || 'ROWS', - values: processedValues, - } - - return body - }, - }, - - transformResponse: async (response: Response) => { - const data = await response.json() - - const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : [] - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - - const metadata = { - spreadsheetId, - spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`, - } - - return { - success: true, - output: { - updatedRange: data.updatedRange ?? null, - updatedRows: data.updatedRows ?? 0, - updatedColumns: data.updatedColumns ?? 0, - updatedCells: data.updatedCells ?? 0, - metadata: { - spreadsheetId: metadata.spreadsheetId, - spreadsheetUrl: metadata.spreadsheetUrl, - }, - }, - } - }, - - outputs: { - updatedRange: { type: 'string', description: 'Range of cells that were updated' }, - updatedRows: { type: 'number', description: 'Number of rows updated' }, - updatedColumns: { type: 'number', description: 'Number of columns updated' }, - updatedCells: { type: 'number', description: 'Number of cells updated' }, - metadata: { - type: 'json', - description: 'Spreadsheet metadata including ID and URL', - properties: { - spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' }, - spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, - }, - }, - }, -} diff --git a/apps/sim/tools/google_slides/add_image.ts b/apps/sim/tools/google_slides/add_image.ts index 9056667fc6..b8f257d21a 100644 --- a/apps/sim/tools/google_slides/add_image.ts +++ b/apps/sim/tools/google_slides/add_image.ts @@ -51,36 +51,43 @@ export const addImageTool: ToolConfig = { presentationId: { type: 'string', required: true, + visibility: 'user-only', description: 'The ID of the presentation', }, pageObjectId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The object ID of the slide/page to add the image to', }, imageUrl: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The publicly accessible URL of the image (must be PNG, JPEG, or GIF, max 50MB)', }, width: { type: 'number', required: false, + visibility: 'user-or-llm', description: 'Width of the image in points (default: 300)', }, height: { type: 'number', required: false, + visibility: 'user-or-llm', description: 'Height of the image in points (default: 200)', }, positionX: { type: 'number', required: false, + visibility: 'user-or-llm', description: 'X position from the left edge in points (default: 100)', }, positionY: { type: 'number', required: false, + visibility: 'user-or-llm', description: 'Y position from the top edge in points (default: 100)', }, }, diff --git a/apps/sim/tools/google_slides/add_slide.ts b/apps/sim/tools/google_slides/add_slide.ts index eb86707466..1f44d84ce4 100644 --- a/apps/sim/tools/google_slides/add_slide.ts +++ b/apps/sim/tools/google_slides/add_slide.ts @@ -60,23 +60,27 @@ export const addSlideTool: ToolConfig = { presentationId: { type: 'string', required: true, + visibility: 'user-only', description: 'The ID of the presentation', }, layout: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'The predefined layout for the slide (BLANK, TITLE, TITLE_AND_BODY, TITLE_ONLY, SECTION_HEADER, etc.). Defaults to BLANK.', }, insertionIndex: { type: 'number', required: false, + visibility: 'user-or-llm', description: 'The optional zero-based index indicating where to insert the slide. If not specified, the slide is added at the end.', }, placeholderIdMappings: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'JSON array of placeholder mappings to assign custom object IDs to placeholders. Format: [{"layoutPlaceholder":{"type":"TITLE"},"objectId":"custom_title_id"}]', }, diff --git a/apps/sim/tools/google_slides/create_shape.ts b/apps/sim/tools/google_slides/create_shape.ts new file mode 100644 index 0000000000..d0dde32775 --- /dev/null +++ b/apps/sim/tools/google_slides/create_shape.ts @@ -0,0 +1,358 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateShapeTool') + +interface CreateShapeParams { + accessToken: string + presentationId: string + pageObjectId: string + shapeType: string + width?: number + height?: number + positionX?: number + positionY?: number +} + +interface CreateShapeResponse { + success: boolean + output: { + shapeId: string + shapeType: string + metadata: { + presentationId: string + pageObjectId: string + url: string + } + } +} + +// EMU (English Metric Units) conversion: 1 pt = 12700 EMU +const PT_TO_EMU = 12700 + +// Common shape types available in Google Slides API +const SHAPE_TYPES = [ + 'TEXT_BOX', + 'RECTANGLE', + 'ROUND_RECTANGLE', + 'ELLIPSE', + 'ARC', + 'BENT_ARROW', + 'BENT_UP_ARROW', + 'BEVEL', + 'BLOCK_ARC', + 'BRACE_PAIR', + 'BRACKET_PAIR', + 'CAN', + 'CHEVRON', + 'CHORD', + 'CLOUD', + 'CORNER', + 'CUBE', + 'CURVED_DOWN_ARROW', + 'CURVED_LEFT_ARROW', + 'CURVED_RIGHT_ARROW', + 'CURVED_UP_ARROW', + 'DECAGON', + 'DIAGONAL_STRIPE', + 'DIAMOND', + 'DODECAGON', + 'DONUT', + 'DOUBLE_WAVE', + 'DOWN_ARROW', + 'DOWN_ARROW_CALLOUT', + 'FOLDED_CORNER', + 'FRAME', + 'HALF_FRAME', + 'HEART', + 'HEPTAGON', + 'HEXAGON', + 'HOME_PLATE', + 'HORIZONTAL_SCROLL', + 'IRREGULAR_SEAL_1', + 'IRREGULAR_SEAL_2', + 'LEFT_ARROW', + 'LEFT_ARROW_CALLOUT', + 'LEFT_BRACE', + 'LEFT_BRACKET', + 'LEFT_RIGHT_ARROW', + 'LEFT_RIGHT_ARROW_CALLOUT', + 'LEFT_RIGHT_UP_ARROW', + 'LEFT_UP_ARROW', + 'LIGHTNING_BOLT', + 'MATH_DIVIDE', + 'MATH_EQUAL', + 'MATH_MINUS', + 'MATH_MULTIPLY', + 'MATH_NOT_EQUAL', + 'MATH_PLUS', + 'MOON', + 'NO_SMOKING', + 'NOTCHED_RIGHT_ARROW', + 'OCTAGON', + 'PARALLELOGRAM', + 'PENTAGON', + 'PIE', + 'PLAQUE', + 'PLUS', + 'QUAD_ARROW', + 'QUAD_ARROW_CALLOUT', + 'RIBBON', + 'RIBBON_2', + 'RIGHT_ARROW', + 'RIGHT_ARROW_CALLOUT', + 'RIGHT_BRACE', + 'RIGHT_BRACKET', + 'ROUND_1_RECTANGLE', + 'ROUND_2_DIAGONAL_RECTANGLE', + 'ROUND_2_SAME_RECTANGLE', + 'RIGHT_TRIANGLE', + 'SMILEY_FACE', + 'SNIP_1_RECTANGLE', + 'SNIP_2_DIAGONAL_RECTANGLE', + 'SNIP_2_SAME_RECTANGLE', + 'SNIP_ROUND_RECTANGLE', + 'STAR_10', + 'STAR_12', + 'STAR_16', + 'STAR_24', + 'STAR_32', + 'STAR_4', + 'STAR_5', + 'STAR_6', + 'STAR_7', + 'STAR_8', + 'STRIPED_RIGHT_ARROW', + 'SUN', + 'TRAPEZOID', + 'TRIANGLE', + 'UP_ARROW', + 'UP_ARROW_CALLOUT', + 'UP_DOWN_ARROW', + 'UTURN_ARROW', + 'VERTICAL_SCROLL', + 'WAVE', + 'WEDGE_ELLIPSE_CALLOUT', + 'WEDGE_RECTANGLE_CALLOUT', + 'WEDGE_ROUND_RECTANGLE_CALLOUT', + 'FLOW_CHART_ALTERNATE_PROCESS', + 'FLOW_CHART_COLLATE', + 'FLOW_CHART_CONNECTOR', + 'FLOW_CHART_DECISION', + 'FLOW_CHART_DELAY', + 'FLOW_CHART_DISPLAY', + 'FLOW_CHART_DOCUMENT', + 'FLOW_CHART_EXTRACT', + 'FLOW_CHART_INPUT_OUTPUT', + 'FLOW_CHART_INTERNAL_STORAGE', + 'FLOW_CHART_MAGNETIC_DISK', + 'FLOW_CHART_MAGNETIC_DRUM', + 'FLOW_CHART_MAGNETIC_TAPE', + 'FLOW_CHART_MANUAL_INPUT', + 'FLOW_CHART_MANUAL_OPERATION', + 'FLOW_CHART_MERGE', + 'FLOW_CHART_MULTIDOCUMENT', + 'FLOW_CHART_OFFLINE_STORAGE', + 'FLOW_CHART_OFFPAGE_CONNECTOR', + 'FLOW_CHART_ONLINE_STORAGE', + 'FLOW_CHART_OR', + 'FLOW_CHART_PREDEFINED_PROCESS', + 'FLOW_CHART_PREPARATION', + 'FLOW_CHART_PROCESS', + 'FLOW_CHART_PUNCHED_CARD', + 'FLOW_CHART_PUNCHED_TAPE', + 'FLOW_CHART_SORT', + 'FLOW_CHART_SUMMING_JUNCTION', + 'FLOW_CHART_TERMINATOR', + 'ARROW_EAST', + 'ARROW_NORTH_EAST', + 'ARROW_NORTH', + 'SPEECH', + 'STARBURST', + 'TEARDROP', + 'ELLIPSE_RIBBON', + 'ELLIPSE_RIBBON_2', + 'CLOUD_CALLOUT', + 'CUSTOM', +] as const + +export const createShapeTool: ToolConfig = { + id: 'google_slides_create_shape', + name: 'Create Shape in Google Slides', + description: + 'Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object ID of the slide/page to add the shape to', + }, + shapeType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The type of shape to create. Common types: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, DIAMOND, STAR_5, ARROW_EAST, HEART, CLOUD', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Width of the shape in points (default: 200)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Height of the shape in points (default: 100)', + }, + positionX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position from the left edge in points (default: 100)', + }, + positionY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position from the top edge in points (default: 100)', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const pageObjectId = params.pageObjectId?.trim() + if (!pageObjectId) { + throw new Error('Page Object ID is required') + } + + let shapeType = (params.shapeType || 'RECTANGLE').toUpperCase() + if (!SHAPE_TYPES.includes(shapeType as (typeof SHAPE_TYPES)[number])) { + logger.warn(`Invalid shape type "${params.shapeType}", defaulting to RECTANGLE`) + shapeType = 'RECTANGLE' + } + + // Generate a unique object ID for the new shape + const shapeObjectId = `shape_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + + // Convert points to EMU + const widthEmu = (params.width || 200) * PT_TO_EMU + const heightEmu = (params.height || 100) * PT_TO_EMU + const translateX = (params.positionX || 100) * PT_TO_EMU + const translateY = (params.positionY || 100) * PT_TO_EMU + + return { + requests: [ + { + createShape: { + objectId: shapeObjectId, + shapeType, + elementProperties: { + pageObjectId, + size: { + width: { + magnitude: widthEmu, + unit: 'EMU', + }, + height: { + magnitude: heightEmu, + unit: 'EMU', + }, + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX, + translateY, + unit: 'EMU', + }, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create shape') + } + + const createShapeReply = data.replies?.[0]?.createShape + const shapeId = createShapeReply?.objectId || '' + + const presentationId = params?.presentationId?.trim() || '' + const pageObjectId = params?.pageObjectId?.trim() || '' + const shapeType = (params?.shapeType || 'RECTANGLE').toUpperCase() + + return { + success: true, + output: { + shapeId, + shapeType, + metadata: { + presentationId, + pageObjectId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + shapeId: { + type: 'string', + description: 'The object ID of the newly created shape', + }, + shapeType: { + type: 'string', + description: 'The type of shape that was created', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and page object ID', + }, + }, +} diff --git a/apps/sim/tools/google_slides/create_table.ts b/apps/sim/tools/google_slides/create_table.ts new file mode 100644 index 0000000000..b219158869 --- /dev/null +++ b/apps/sim/tools/google_slides/create_table.ts @@ -0,0 +1,227 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesCreateTableTool') + +interface CreateTableParams { + accessToken: string + presentationId: string + pageObjectId: string + rows: number + columns: number + width?: number + height?: number + positionX?: number + positionY?: number +} + +interface CreateTableResponse { + success: boolean + output: { + tableId: string + rows: number + columns: number + metadata: { + presentationId: string + pageObjectId: string + url: string + } + } +} + +// EMU (English Metric Units) conversion: 1 pt = 12700 EMU +const PT_TO_EMU = 12700 + +export const createTableTool: ToolConfig = { + id: 'google_slides_create_table', + name: 'Create Table in Google Slides', + description: 'Create a new table on a slide in a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object ID of the slide/page to add the table to', + }, + rows: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of rows in the table (minimum 1)', + }, + columns: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of columns in the table (minimum 1)', + }, + width: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Width of the table in points (default: 400)', + }, + height: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Height of the table in points (default: 200)', + }, + positionX: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'X position from the left edge in points (default: 100)', + }, + positionY: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Y position from the top edge in points (default: 100)', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const pageObjectId = params.pageObjectId?.trim() + if (!pageObjectId) { + throw new Error('Page Object ID is required') + } + + const rows = params.rows + const columns = params.columns + + if (!rows || rows < 1) { + throw new Error('Rows must be at least 1') + } + if (!columns || columns < 1) { + throw new Error('Columns must be at least 1') + } + + // Generate a unique object ID for the new table + const tableObjectId = `table_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + + // Convert points to EMU + const widthEmu = (params.width || 400) * PT_TO_EMU + const heightEmu = (params.height || 200) * PT_TO_EMU + const translateX = (params.positionX || 100) * PT_TO_EMU + const translateY = (params.positionY || 100) * PT_TO_EMU + + return { + requests: [ + { + createTable: { + objectId: tableObjectId, + rows, + columns, + elementProperties: { + pageObjectId, + size: { + width: { + magnitude: widthEmu, + unit: 'EMU', + }, + height: { + magnitude: heightEmu, + unit: 'EMU', + }, + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX, + translateY, + unit: 'EMU', + }, + }, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to create table') + } + + const createTableReply = data.replies?.[0]?.createTable + const tableId = createTableReply?.objectId || '' + + const presentationId = params?.presentationId?.trim() || '' + const pageObjectId = params?.pageObjectId?.trim() || '' + + return { + success: true, + output: { + tableId, + rows: params?.rows ?? 0, + columns: params?.columns ?? 0, + metadata: { + presentationId, + pageObjectId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + tableId: { + type: 'string', + description: 'The object ID of the newly created table', + }, + rows: { + type: 'number', + description: 'Number of rows in the table', + }, + columns: { + type: 'number', + description: 'Number of columns in the table', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and page object ID', + }, + }, +} diff --git a/apps/sim/tools/google_slides/delete_object.ts b/apps/sim/tools/google_slides/delete_object.ts new file mode 100644 index 0000000000..3e784b3aa5 --- /dev/null +++ b/apps/sim/tools/google_slides/delete_object.ts @@ -0,0 +1,131 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDeleteObjectTool') + +interface DeleteObjectParams { + accessToken: string + presentationId: string + objectId: string +} + +interface DeleteObjectResponse { + success: boolean + output: { + deleted: boolean + objectId: string + metadata: { + presentationId: string + url: string + } + } +} + +export const deleteObjectTool: ToolConfig = { + id: 'google_slides_delete_object', + name: 'Delete Object from Google Slides', + description: + 'Delete a page element (shape, image, table, etc.) or an entire slide from a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object ID of the element or slide to delete', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) { + throw new Error('Object ID is required') + } + + return { + requests: [ + { + deleteObject: { + objectId, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to delete object') + } + + const presentationId = params?.presentationId?.trim() || '' + const objectId = params?.objectId?.trim() || '' + + return { + success: true, + output: { + deleted: true, + objectId, + metadata: { + presentationId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the object was successfully deleted', + }, + objectId: { + type: 'string', + description: 'The object ID that was deleted', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and URL', + }, + }, +} diff --git a/apps/sim/tools/google_slides/duplicate_object.ts b/apps/sim/tools/google_slides/duplicate_object.ts new file mode 100644 index 0000000000..56d2e91725 --- /dev/null +++ b/apps/sim/tools/google_slides/duplicate_object.ts @@ -0,0 +1,152 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesDuplicateObjectTool') + +interface DuplicateObjectParams { + accessToken: string + presentationId: string + objectId: string + objectIds?: string +} + +interface DuplicateObjectResponse { + success: boolean + output: { + duplicatedObjectId: string + metadata: { + presentationId: string + sourceObjectId: string + url: string + } + } +} + +export const duplicateObjectTool: ToolConfig = { + id: 'google_slides_duplicate_object', + name: 'Duplicate Object in Google Slides', + description: + 'Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object ID of the element or slide to duplicate', + }, + objectIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Optional JSON object mapping source object IDs (within the slide being duplicated) to new object IDs for the duplicates. Format: {"sourceId1":"newId1","sourceId2":"newId2"}', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) { + throw new Error('Object ID is required') + } + + const duplicateRequest: Record = { + objectId, + } + + // Parse objectIds mapping if provided + if (params.objectIds?.trim()) { + try { + const mapping = JSON.parse(params.objectIds) + if (typeof mapping === 'object' && !Array.isArray(mapping)) { + duplicateRequest.objectIds = mapping + } + } catch (e) { + logger.warn('Invalid objectIds JSON, ignoring:', e) + } + } + + return { + requests: [ + { + duplicateObject: duplicateRequest, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to duplicate object') + } + + const duplicateReply = data.replies?.[0]?.duplicateObject + const duplicatedObjectId = duplicateReply?.objectId ?? '' + + const presentationId = params?.presentationId?.trim() || '' + const objectId = params?.objectId?.trim() || '' + + return { + success: true, + output: { + duplicatedObjectId, + metadata: { + presentationId, + sourceObjectId: objectId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + duplicatedObjectId: { + type: 'string', + description: 'The object ID of the newly created duplicate', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and source object ID', + }, + }, +} diff --git a/apps/sim/tools/google_slides/get_page.ts b/apps/sim/tools/google_slides/get_page.ts new file mode 100644 index 0000000000..7d7a169be9 --- /dev/null +++ b/apps/sim/tools/google_slides/get_page.ts @@ -0,0 +1,142 @@ +import { createLogger } from '@sim/logger' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('GoogleSlidesGetPageTool') + +interface GetPageParams { + accessToken: string + presentationId: string + pageObjectId: string +} + +interface GetPageResponse { + success: boolean + output: { + objectId: string + pageType: string + pageElements: any[] + slideProperties: { + layoutObjectId: string | null + masterObjectId: string | null + notesPage: any | null + } | null + metadata: { + presentationId: string + url: string + } + } +} + +export const getPageTool: ToolConfig = { + id: 'google_slides_get_page', + name: 'Get Slide Page', + description: + 'Get detailed information about a specific slide/page in a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + pageObjectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The object ID of the slide/page to retrieve', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + const pageObjectId = params.pageObjectId?.trim() + + if (!presentationId) { + throw new Error('Presentation ID is required') + } + if (!pageObjectId) { + throw new Error('Page Object ID is required') + } + + return `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to get page') + } + + const presentationId = params?.presentationId?.trim() || '' + + return { + success: true, + output: { + objectId: data.objectId, + pageType: data.pageType ?? 'SLIDE', + pageElements: data.pageElements ?? [], + slideProperties: data.slideProperties + ? { + layoutObjectId: data.slideProperties.layoutObjectId ?? null, + masterObjectId: data.slideProperties.masterObjectId ?? null, + notesPage: data.slideProperties.notesPage ?? null, + } + : null, + metadata: { + presentationId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + objectId: { + type: 'string', + description: 'The object ID of the page', + }, + pageType: { + type: 'string', + description: 'The type of page (SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER)', + }, + pageElements: { + type: 'json', + description: 'Array of page elements (shapes, images, tables, etc.) on this page', + }, + slideProperties: { + type: 'json', + description: 'Properties specific to slides (layout, master, notes)', + optional: true, + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and URL', + }, + }, +} diff --git a/apps/sim/tools/google_slides/get_thumbnail.ts b/apps/sim/tools/google_slides/get_thumbnail.ts index 4af7443caf..0dedc15975 100644 --- a/apps/sim/tools/google_slides/get_thumbnail.ts +++ b/apps/sim/tools/google_slides/get_thumbnail.ts @@ -53,22 +53,26 @@ export const getThumbnailTool: ToolConfig = { + id: 'google_slides_insert_text', + name: 'Insert Text in Google Slides', + description: + 'Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or table cells.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + objectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The object ID of the shape or table cell to insert text into. For table cells, use the cell object ID.', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The text to insert', + }, + insertionIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'The zero-based index at which to insert the text. If not specified, text is inserted at the beginning (index 0).', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const objectId = params.objectId?.trim() + if (!objectId) { + throw new Error('Object ID is required') + } + + if (params.text === undefined || params.text === null) { + throw new Error('Text is required') + } + + return { + requests: [ + { + insertText: { + objectId, + text: params.text, + insertionIndex: params.insertionIndex ?? 0, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to insert text') + } + + const presentationId = params?.presentationId?.trim() || '' + const objectId = params?.objectId?.trim() || '' + + return { + success: true, + output: { + inserted: true, + objectId, + text: params?.text ?? '', + metadata: { + presentationId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + inserted: { + type: 'boolean', + description: 'Whether the text was successfully inserted', + }, + objectId: { + type: 'string', + description: 'The object ID where text was inserted', + }, + text: { + type: 'string', + description: 'The text that was inserted', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and URL', + }, + }, +} diff --git a/apps/sim/tools/google_slides/replace_all_text.ts b/apps/sim/tools/google_slides/replace_all_text.ts index 3d2abd680d..5d7b3310b7 100644 --- a/apps/sim/tools/google_slides/replace_all_text.ts +++ b/apps/sim/tools/google_slides/replace_all_text.ts @@ -46,26 +46,31 @@ export const replaceAllTextTool: ToolConfig = { + id: 'google_slides_update_slides_position', + name: 'Reorder Slides in Google Slides', + description: 'Move one or more slides to a new position in a Google Slides presentation', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Slides API', + }, + presentationId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the presentation', + }, + slideObjectIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated list of slide object IDs to move. The slides will maintain their relative order.', + }, + insertionIndex: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: + 'The zero-based index where the slides should be moved. All slides with indices greater than or equal to this will be shifted right.', + }, + }, + + request: { + url: (params) => { + const presentationId = params.presentationId?.trim() + if (!presentationId) { + throw new Error('Presentation ID is required') + } + return `https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate` + }, + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + if (!params.slideObjectIds?.trim()) { + throw new Error('Slide object IDs are required') + } + if (params.insertionIndex === undefined || params.insertionIndex < 0) { + throw new Error('Insertion index must be a non-negative number') + } + + const slideObjectIds = params.slideObjectIds + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) + + if (slideObjectIds.length === 0) { + throw new Error('At least one slide object ID is required') + } + + return { + requests: [ + { + updateSlidesPosition: { + slideObjectIds, + insertionIndex: params.insertionIndex, + }, + }, + ], + } + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json() + + if (!response.ok) { + logger.error('Google Slides API error:', { data }) + throw new Error(data.error?.message || 'Failed to update slides position') + } + + const presentationId = params?.presentationId?.trim() || '' + const slideObjectIds = + params?.slideObjectIds + ?.split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0) ?? [] + + return { + success: true, + output: { + moved: true, + slideObjectIds, + insertionIndex: params?.insertionIndex ?? 0, + metadata: { + presentationId, + url: `https://docs.google.com/presentation/d/${presentationId}/edit`, + }, + }, + } + }, + + outputs: { + moved: { + type: 'boolean', + description: 'Whether the slides were successfully moved', + }, + slideObjectIds: { + type: 'array', + description: 'The slide object IDs that were moved', + items: { + type: 'string', + }, + }, + insertionIndex: { + type: 'number', + description: 'The index where the slides were moved to', + }, + metadata: { + type: 'json', + description: 'Operation metadata including presentation ID and URL', + }, + }, +} diff --git a/apps/sim/tools/google_slides/write.ts b/apps/sim/tools/google_slides/write.ts index dbe5e1e3e2..87f0a3a986 100644 --- a/apps/sim/tools/google_slides/write.ts +++ b/apps/sim/tools/google_slides/write.ts @@ -23,16 +23,19 @@ export const writeTool: ToolConfig = { url: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The URL to send the request to', }, method: { type: 'string', default: 'GET', + visibility: 'user-or-llm', description: 'HTTP method (GET, POST, PUT, PATCH, DELETE)', }, headers: { type: 'object', + visibility: 'user-or-llm', description: 'HTTP headers to include', }, body: { type: 'object', + visibility: 'user-or-llm', description: 'Request body (for POST, PUT, PATCH)', }, params: { type: 'object', + visibility: 'user-or-llm', description: 'URL query parameters to append', }, pathParams: { type: 'object', + visibility: 'user-or-llm', description: 'URL path parameters to replace (e.g., :id in /users/:id)', }, formData: { type: 'object', + visibility: 'user-or-llm', description: 'Form data to send (will set appropriate Content-Type)', }, }, diff --git a/apps/sim/tools/http/webhook_request.ts b/apps/sim/tools/http/webhook_request.ts index f04da4c1f8..33d7c5051a 100644 --- a/apps/sim/tools/http/webhook_request.ts +++ b/apps/sim/tools/http/webhook_request.ts @@ -21,18 +21,22 @@ export const webhookRequestTool: ToolConfig = { 'Content-Type': 'application/json', 'X-Webhook-Timestamp': timestamp.toString(), @@ -54,7 +57,6 @@ export const webhookRequestTool: ToolConfig = { conversationId: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Conversation identifier (e.g., user-123, session-abc). If a memory with this conversationId already exists, the new message will be appended to it.', }, id: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.', }, diff --git a/apps/sim/tools/memory/delete.ts b/apps/sim/tools/memory/delete.ts index aa5a577e19..b32d1fcbf1 100644 --- a/apps/sim/tools/memory/delete.ts +++ b/apps/sim/tools/memory/delete.ts @@ -11,12 +11,14 @@ export const memoryDeleteTool: ToolConfig = { conversationId: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Conversation identifier (e.g., user-123, session-abc). Deletes all memories for this conversation.', }, id: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.', }, diff --git a/apps/sim/tools/memory/get.ts b/apps/sim/tools/memory/get.ts index 21830a42ec..2712563761 100644 --- a/apps/sim/tools/memory/get.ts +++ b/apps/sim/tools/memory/get.ts @@ -11,12 +11,14 @@ export const memoryGetTool: ToolConfig = { conversationId: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Conversation identifier (e.g., user-123, session-abc). Returns memories for this conversation.', }, id: { type: 'string', required: false, + visibility: 'user-or-llm', description: 'Legacy parameter for conversation identifier. Use conversationId instead. Provided for backwards compatibility.', }, diff --git a/apps/sim/tools/microsoft_excel/index.ts b/apps/sim/tools/microsoft_excel/index.ts index c39b792560..bf1e8b1aad 100644 --- a/apps/sim/tools/microsoft_excel/index.ts +++ b/apps/sim/tools/microsoft_excel/index.ts @@ -1,9 +1,7 @@ -import { readTool } from '@/tools/microsoft_excel/read' -import { readV2Tool } from '@/tools/microsoft_excel/read_v2' +import { readTool, readV2Tool } from '@/tools/microsoft_excel/read' import { tableAddTool } from '@/tools/microsoft_excel/table_add' import { worksheetAddTool } from '@/tools/microsoft_excel/worksheet_add' -import { writeTool } from '@/tools/microsoft_excel/write' -import { writeV2Tool } from '@/tools/microsoft_excel/write_v2' +import { writeTool, writeV2Tool } from '@/tools/microsoft_excel/write' // V1 exports export const microsoftExcelReadTool = readTool diff --git a/apps/sim/tools/microsoft_excel/read.ts b/apps/sim/tools/microsoft_excel/read.ts index 38e1ff2a50..49e8540ce5 100644 --- a/apps/sim/tools/microsoft_excel/read.ts +++ b/apps/sim/tools/microsoft_excel/read.ts @@ -2,6 +2,8 @@ import type { ExcelCellValue, MicrosoftExcelReadResponse, MicrosoftExcelToolParams, + MicrosoftExcelV2ReadResponse, + MicrosoftExcelV2ToolParams, } from '@/tools/microsoft_excel/types' import { getSpreadsheetWebUrl, @@ -213,3 +215,126 @@ export const readTool: ToolConfig = { + id: 'microsoft_excel_read_v2', + name: 'Read from Microsoft Excel V2', + description: 'Read data from a specific sheet in a Microsoft Excel spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet to read from', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to read from', + }, + cellRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to read (e.g., "A1:D10"). If not specified, reads the entire used range.', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + const encodedSheetName = encodeURIComponent(sheetName) + + // If no cell range specified, fetch usedRange + if (!params.cellRange) { + return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/usedRange(valuesOnly=true)` + } + + const cellRange = params.cellRange.trim() + const encodedAddress = encodeURIComponent(cellRange) + + return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => { + const data = await response.json() + + const urlParts = response.url.split('/drive/items/') + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + + const address: string = data.address || data.addressLocal || '' + const rawValues: ExcelCellValue[][] = data.values || [] + const values = trimTrailingEmptyRowsAndColumns(rawValues) + + // Extract sheet name from address (format: SheetName!A1:B2) + const sheetName = params?.sheetName || address.split('!')[0] || '' + + return { + success: true, + output: { + sheetName, + range: address, + values, + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + sheetName: { type: 'string', description: 'Name of the sheet that was read' }, + range: { type: 'string', description: 'The range that was read' }, + values: { type: 'array', description: 'Array of rows containing cell values' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/read_v2.ts b/apps/sim/tools/microsoft_excel/read_v2.ts deleted file mode 100644 index 1d4e047690..0000000000 --- a/apps/sim/tools/microsoft_excel/read_v2.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { - ExcelCellValue, - MicrosoftExcelV2ReadResponse, - MicrosoftExcelV2ToolParams, -} from '@/tools/microsoft_excel/types' -import { - getSpreadsheetWebUrl, - trimTrailingEmptyRowsAndColumns, -} from '@/tools/microsoft_excel/utils' -import type { ToolConfig } from '@/tools/types' - -export const readV2Tool: ToolConfig = { - id: 'microsoft_excel_read_v2', - name: 'Read from Microsoft Excel V2', - description: 'Read data from a specific sheet in a Microsoft Excel spreadsheet', - version: '2.0.0', - - oauth: { - required: true, - provider: 'microsoft-excel', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the Microsoft Excel API', - }, - spreadsheetId: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'The ID of the spreadsheet to read from', - }, - sheetName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the sheet/tab to read from', - }, - cellRange: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'The cell range to read (e.g., "A1:D10"). If not specified, reads the entire used range.', - }, - }, - - request: { - url: (params) => { - const spreadsheetId = params.spreadsheetId?.trim() - if (!spreadsheetId) { - throw new Error('Spreadsheet ID is required') - } - - const sheetName = params.sheetName?.trim() - if (!sheetName) { - throw new Error('Sheet name is required') - } - - const encodedSheetName = encodeURIComponent(sheetName) - - // If no cell range specified, fetch usedRange - if (!params.cellRange) { - return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/usedRange(valuesOnly=true)` - } - - const cellRange = params.cellRange.trim() - const encodedAddress = encodeURIComponent(cellRange) - - return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')` - }, - method: 'GET', - headers: (params) => { - if (!params.accessToken) { - throw new Error('Access token is required') - } - - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, - }, - - transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => { - const data = await response.json() - - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - - const accessToken = params?.accessToken - if (!accessToken) { - throw new Error('Access token is required') - } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) - - const address: string = data.address || data.addressLocal || '' - const rawValues: ExcelCellValue[][] = data.values || [] - const values = trimTrailingEmptyRowsAndColumns(rawValues) - - // Extract sheet name from address (format: SheetName!A1:B2) - const sheetName = params?.sheetName || address.split('!')[0] || '' - - return { - success: true, - output: { - sheetName, - range: address, - values, - metadata: { - spreadsheetId, - spreadsheetUrl: webUrl, - }, - }, - } - }, - - outputs: { - sheetName: { type: 'string', description: 'Name of the sheet that was read' }, - range: { type: 'string', description: 'The range that was read' }, - values: { type: 'array', description: 'Array of rows containing cell values' }, - metadata: { - type: 'json', - description: 'Spreadsheet metadata including ID and URL', - properties: { - spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' }, - spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, - }, - }, - }, -} diff --git a/apps/sim/tools/microsoft_excel/write.ts b/apps/sim/tools/microsoft_excel/write.ts index 07aa4acf88..d400bf28a4 100644 --- a/apps/sim/tools/microsoft_excel/write.ts +++ b/apps/sim/tools/microsoft_excel/write.ts @@ -1,5 +1,7 @@ import type { MicrosoftExcelToolParams, + MicrosoftExcelV2ToolParams, + MicrosoftExcelV2WriteResponse, MicrosoftExcelWriteResponse, } from '@/tools/microsoft_excel/types' import { getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' @@ -182,3 +184,181 @@ export const writeTool: ToolConfig = { + id: 'microsoft_excel_write_v2', + name: 'Write to Microsoft Excel V2', + description: 'Write data to a specific sheet in a Microsoft Excel spreadsheet', + version: '2.0.0', + + oauth: { + required: true, + provider: 'microsoft-excel', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Excel API', + }, + spreadsheetId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the spreadsheet to write to', + }, + sheetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the sheet/tab to write to', + }, + cellRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The cell range to write to (e.g., "A1:D10", "A1"). Defaults to "A1" if not specified.', + }, + values: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.', + }, + valueInputOption: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The format of the data to write', + }, + includeValuesInResponse: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Whether to include the written values in the response', + }, + }, + + request: { + url: (params) => { + const spreadsheetId = params.spreadsheetId?.trim() + if (!spreadsheetId) { + throw new Error('Spreadsheet ID is required') + } + + const sheetName = params.sheetName?.trim() + if (!sheetName) { + throw new Error('Sheet name is required') + } + + const cellRange = params.cellRange?.trim() || 'A1' + const encodedSheetName = encodeURIComponent(sheetName) + const encodedAddress = encodeURIComponent(cellRange) + + const url = new URL( + `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')` + ) + + const valueInputOption = params.valueInputOption || 'USER_ENTERED' + url.searchParams.append('valueInputOption', valueInputOption) + + if (params.includeValuesInResponse) { + url.searchParams.append('includeValuesInResponse', 'true') + } + + return url.toString() + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let processedValues: any = params.values || [] + + // Handle array of objects + if ( + Array.isArray(processedValues) && + processedValues.length > 0 && + typeof processedValues[0] === 'object' && + !Array.isArray(processedValues[0]) + ) { + const allKeys = new Set() + processedValues.forEach((obj: any) => { + if (obj && typeof obj === 'object') { + Object.keys(obj).forEach((key) => allKeys.add(key)) + } + }) + const headers = Array.from(allKeys) + + const rows = processedValues.map((obj: any) => { + if (!obj || typeof obj !== 'object') { + return Array(headers.length).fill('') + } + return headers.map((key) => { + const value = obj[key] + if (value !== null && typeof value === 'object') { + return JSON.stringify(value) + } + return value === undefined ? '' : value + }) + }) + + processedValues = [headers, ...rows] + } + + const body: Record = { + majorDimension: params.majorDimension || 'ROWS', + values: processedValues, + } + + return body + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => { + const data = await response.json() + + const urlParts = response.url.split('/drive/items/') + const spreadsheetId = urlParts[1]?.split('/')[0] || '' + + const accessToken = params?.accessToken + if (!accessToken) { + throw new Error('Access token is required') + } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + + return { + success: true, + output: { + updatedRange: data.address ?? null, + updatedRows: data.rowCount ?? 0, + updatedColumns: data.columnCount ?? 0, + updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0), + metadata: { + spreadsheetId, + spreadsheetUrl: webUrl, + }, + }, + } + }, + + outputs: { + updatedRange: { type: 'string', description: 'Range of cells that were updated' }, + updatedRows: { type: 'number', description: 'Number of rows updated' }, + updatedColumns: { type: 'number', description: 'Number of columns updated' }, + updatedCells: { type: 'number', description: 'Number of cells updated' }, + metadata: { + type: 'json', + description: 'Spreadsheet metadata including ID and URL', + properties: { + spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' }, + spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_excel/write_v2.ts b/apps/sim/tools/microsoft_excel/write_v2.ts deleted file mode 100644 index 25069f3fbd..0000000000 --- a/apps/sim/tools/microsoft_excel/write_v2.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { - MicrosoftExcelV2ToolParams, - MicrosoftExcelV2WriteResponse, -} from '@/tools/microsoft_excel/types' -import { getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' -import type { ToolConfig } from '@/tools/types' - -export const writeV2Tool: ToolConfig = { - id: 'microsoft_excel_write_v2', - name: 'Write to Microsoft Excel V2', - description: 'Write data to a specific sheet in a Microsoft Excel spreadsheet', - version: '2.0.0', - - oauth: { - required: true, - provider: 'microsoft-excel', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'The access token for the Microsoft Excel API', - }, - spreadsheetId: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'The ID of the spreadsheet to write to', - }, - sheetName: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The name of the sheet/tab to write to', - }, - cellRange: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: - 'The cell range to write to (e.g., "A1:D10", "A1"). Defaults to "A1" if not specified.', - }, - values: { - type: 'array', - required: true, - visibility: 'user-or-llm', - description: - 'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.', - }, - valueInputOption: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The format of the data to write', - }, - includeValuesInResponse: { - type: 'boolean', - required: false, - visibility: 'hidden', - description: 'Whether to include the written values in the response', - }, - }, - - request: { - url: (params) => { - const spreadsheetId = params.spreadsheetId?.trim() - if (!spreadsheetId) { - throw new Error('Spreadsheet ID is required') - } - - const sheetName = params.sheetName?.trim() - if (!sheetName) { - throw new Error('Sheet name is required') - } - - const cellRange = params.cellRange?.trim() || 'A1' - const encodedSheetName = encodeURIComponent(sheetName) - const encodedAddress = encodeURIComponent(cellRange) - - const url = new URL( - `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')` - ) - - const valueInputOption = params.valueInputOption || 'USER_ENTERED' - url.searchParams.append('valueInputOption', valueInputOption) - - if (params.includeValuesInResponse) { - url.searchParams.append('includeValuesInResponse', 'true') - } - - return url.toString() - }, - method: 'PATCH', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - }), - body: (params) => { - let processedValues: any = params.values || [] - - // Handle array of objects - if ( - Array.isArray(processedValues) && - processedValues.length > 0 && - typeof processedValues[0] === 'object' && - !Array.isArray(processedValues[0]) - ) { - const allKeys = new Set() - processedValues.forEach((obj: any) => { - if (obj && typeof obj === 'object') { - Object.keys(obj).forEach((key) => allKeys.add(key)) - } - }) - const headers = Array.from(allKeys) - - const rows = processedValues.map((obj: any) => { - if (!obj || typeof obj !== 'object') { - return Array(headers.length).fill('') - } - return headers.map((key) => { - const value = obj[key] - if (value !== null && typeof value === 'object') { - return JSON.stringify(value) - } - return value === undefined ? '' : value - }) - }) - - processedValues = [headers, ...rows] - } - - const body: Record = { - majorDimension: params.majorDimension || 'ROWS', - values: processedValues, - } - - return body - }, - }, - - transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => { - const data = await response.json() - - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' - - const accessToken = params?.accessToken - if (!accessToken) { - throw new Error('Access token is required') - } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) - - return { - success: true, - output: { - updatedRange: data.address ?? null, - updatedRows: data.rowCount ?? 0, - updatedColumns: data.columnCount ?? 0, - updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0), - metadata: { - spreadsheetId, - spreadsheetUrl: webUrl, - }, - }, - } - }, - - outputs: { - updatedRange: { type: 'string', description: 'Range of cells that were updated' }, - updatedRows: { type: 'number', description: 'Number of rows updated' }, - updatedColumns: { type: 'number', description: 'Number of columns updated' }, - updatedCells: { type: 'number', description: 'Number of cells updated' }, - metadata: { - type: 'json', - description: 'Spreadsheet metadata including ID and URL', - properties: { - spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' }, - spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' }, - }, - }, - }, -} diff --git a/apps/sim/tools/polymarket/get_event.ts b/apps/sim/tools/polymarket/get_event.ts index 0371360973..c1dd865127 100644 --- a/apps/sim/tools/polymarket/get_event.ts +++ b/apps/sim/tools/polymarket/get_event.ts @@ -28,12 +28,14 @@ export const polymarketGetEventTool: ToolConfig< type: 'string', required: false, description: 'The event ID. Required if slug is not provided.', + visibility: 'user-or-llm', }, slug: { type: 'string', required: false, description: 'The event slug (e.g., "2024-presidential-election"). Required if eventId is not provided.', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_events.ts b/apps/sim/tools/polymarket/get_events.ts index 6735573f9d..b603948ffc 100644 --- a/apps/sim/tools/polymarket/get_events.ts +++ b/apps/sim/tools/polymarket/get_events.ts @@ -30,31 +30,37 @@ export const polymarketGetEventsTool: ToolConfig< type: 'string', required: false, description: 'Filter by closed status (true/false). Use false for active events only.', + visibility: 'user-or-llm', }, order: { type: 'string', required: false, description: 'Sort field (e.g., volume, liquidity, startDate, endDate, createdAt)', + visibility: 'user-or-llm', }, ascending: { type: 'string', required: false, description: 'Sort direction (true for ascending, false for descending)', + visibility: 'user-or-llm', }, tagId: { type: 'string', required: false, description: 'Filter by tag ID', + visibility: 'user-or-llm', }, limit: { type: 'string', required: false, description: 'Number of results per page (max 50)', + visibility: 'user-or-llm', }, offset: { type: 'string', required: false, description: 'Pagination offset (skip this many results)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_last_trade_price.ts b/apps/sim/tools/polymarket/get_last_trade_price.ts index cb12f81bb2..3280e0f03c 100644 --- a/apps/sim/tools/polymarket/get_last_trade_price.ts +++ b/apps/sim/tools/polymarket/get_last_trade_price.ts @@ -26,6 +26,7 @@ export const polymarketGetLastTradePriceTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_market.ts b/apps/sim/tools/polymarket/get_market.ts index 7b69d8ba80..1c4e9c27bc 100644 --- a/apps/sim/tools/polymarket/get_market.ts +++ b/apps/sim/tools/polymarket/get_market.ts @@ -28,12 +28,14 @@ export const polymarketGetMarketTool: ToolConfig< type: 'string', required: false, description: 'The market ID. Required if slug is not provided.', + visibility: 'user-or-llm', }, slug: { type: 'string', required: false, description: 'The market slug (e.g., "will-trump-win"). Required if marketId is not provided.', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_markets.ts b/apps/sim/tools/polymarket/get_markets.ts index fa15e69553..2183113ed5 100644 --- a/apps/sim/tools/polymarket/get_markets.ts +++ b/apps/sim/tools/polymarket/get_markets.ts @@ -30,31 +30,37 @@ export const polymarketGetMarketsTool: ToolConfig< type: 'string', required: false, description: 'Filter by closed status (true/false). Use false for active markets only.', + visibility: 'user-or-llm', }, order: { type: 'string', required: false, description: 'Sort field (e.g., volumeNum, liquidityNum, startDate, endDate, createdAt)', + visibility: 'user-or-llm', }, ascending: { type: 'string', required: false, description: 'Sort direction (true for ascending, false for descending)', + visibility: 'user-or-llm', }, tagId: { type: 'string', required: false, description: 'Filter by tag ID', + visibility: 'user-or-llm', }, limit: { type: 'string', required: false, description: 'Number of results per page (max 50)', + visibility: 'user-or-llm', }, offset: { type: 'string', required: false, description: 'Pagination offset (skip this many results)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_midpoint.ts b/apps/sim/tools/polymarket/get_midpoint.ts index 85dd64408a..7ea9abc1ad 100644 --- a/apps/sim/tools/polymarket/get_midpoint.ts +++ b/apps/sim/tools/polymarket/get_midpoint.ts @@ -26,6 +26,7 @@ export const polymarketGetMidpointTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_orderbook.ts b/apps/sim/tools/polymarket/get_orderbook.ts index 890a5c01b9..90a95d9b6d 100644 --- a/apps/sim/tools/polymarket/get_orderbook.ts +++ b/apps/sim/tools/polymarket/get_orderbook.ts @@ -27,6 +27,7 @@ export const polymarketGetOrderbookTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_positions.ts b/apps/sim/tools/polymarket/get_positions.ts index 3f164f6954..bbeda19094 100644 --- a/apps/sim/tools/polymarket/get_positions.ts +++ b/apps/sim/tools/polymarket/get_positions.ts @@ -28,11 +28,13 @@ export const polymarketGetPositionsTool: ToolConfig< type: 'string', required: true, description: 'User wallet address', + visibility: 'user-or-llm', }, market: { type: 'string', required: false, description: 'Optional market ID to filter positions', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_price.ts b/apps/sim/tools/polymarket/get_price.ts index c5c56924bc..8506c51011 100644 --- a/apps/sim/tools/polymarket/get_price.ts +++ b/apps/sim/tools/polymarket/get_price.ts @@ -27,11 +27,13 @@ export const polymarketGetPriceTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, side: { type: 'string', required: true, description: 'Order side: buy or sell', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_price_history.ts b/apps/sim/tools/polymarket/get_price_history.ts index f3c3783284..e7ed1b3308 100644 --- a/apps/sim/tools/polymarket/get_price_history.ts +++ b/apps/sim/tools/polymarket/get_price_history.ts @@ -31,27 +31,32 @@ export const polymarketGetPriceHistoryTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, interval: { type: 'string', required: false, description: 'Duration ending at current time (1m, 1h, 6h, 1d, 1w, max). Mutually exclusive with startTs/endTs.', + visibility: 'user-or-llm', }, fidelity: { type: 'number', required: false, description: 'Data resolution in minutes (e.g., 60 for hourly)', + visibility: 'user-or-llm', }, startTs: { type: 'number', required: false, description: 'Start timestamp (Unix seconds UTC)', + visibility: 'user-or-llm', }, endTs: { type: 'number', required: false, description: 'End timestamp (Unix seconds UTC)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_series.ts b/apps/sim/tools/polymarket/get_series.ts index 5a35a1285e..a3d528ecc6 100644 --- a/apps/sim/tools/polymarket/get_series.ts +++ b/apps/sim/tools/polymarket/get_series.ts @@ -25,11 +25,13 @@ export const polymarketGetSeriesTool: ToolConfig< type: 'string', required: false, description: 'Number of results per page (max 50)', + visibility: 'user-or-llm', }, offset: { type: 'string', required: false, description: 'Pagination offset (skip this many results)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_series_by_id.ts b/apps/sim/tools/polymarket/get_series_by_id.ts index f57b3c8674..4904a46f5b 100644 --- a/apps/sim/tools/polymarket/get_series_by_id.ts +++ b/apps/sim/tools/polymarket/get_series_by_id.ts @@ -27,6 +27,7 @@ export const polymarketGetSeriesByIdTool: ToolConfig< type: 'string', required: true, description: 'The series ID', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_spread.ts b/apps/sim/tools/polymarket/get_spread.ts index 707366893d..80c2c55197 100644 --- a/apps/sim/tools/polymarket/get_spread.ts +++ b/apps/sim/tools/polymarket/get_spread.ts @@ -27,6 +27,7 @@ export const polymarketGetSpreadTool: ToolConfig< type: 'string', required: true, description: 'The CLOB token ID (from market clobTokenIds)', + visibility: 'user-or-llm', }, }, diff --git a/apps/sim/tools/polymarket/get_tags.ts b/apps/sim/tools/polymarket/get_tags.ts index b0c5bcba32..1b492eaba2 100644 --- a/apps/sim/tools/polymarket/get_tags.ts +++ b/apps/sim/tools/polymarket/get_tags.ts @@ -23,11 +23,13 @@ export const polymarketGetTagsTool: ToolConfig = apiKey: { type: 'string', required: false, + visibility: 'user-only', description: 'Qdrant API key (optional)', }, collection: { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d16bcc04cd..875d7683fc 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -233,18 +233,30 @@ import { githubAddLabelsV2Tool, githubCancelWorkflowRunTool, githubCancelWorkflowRunV2Tool, + githubCheckStarTool, + githubCheckStarV2Tool, githubCloseIssueTool, githubCloseIssueV2Tool, githubClosePRTool, githubClosePRV2Tool, githubCommentTool, githubCommentV2Tool, + githubCompareCommitsTool, + githubCompareCommitsV2Tool, githubCreateBranchTool, githubCreateBranchV2Tool, + githubCreateCommentReactionTool, + githubCreateCommentReactionV2Tool, githubCreateFileTool, githubCreateFileV2Tool, + githubCreateGistTool, + githubCreateGistV2Tool, + githubCreateIssueReactionTool, + githubCreateIssueReactionV2Tool, githubCreateIssueTool, githubCreateIssueV2Tool, + githubCreateMilestoneTool, + githubCreateMilestoneV2Tool, githubCreatePRTool, githubCreatePRV2Tool, githubCreateProjectTool, @@ -253,22 +265,40 @@ import { githubCreateReleaseV2Tool, githubDeleteBranchTool, githubDeleteBranchV2Tool, + githubDeleteCommentReactionTool, + githubDeleteCommentReactionV2Tool, githubDeleteCommentTool, githubDeleteCommentV2Tool, githubDeleteFileTool, githubDeleteFileV2Tool, + githubDeleteGistTool, + githubDeleteGistV2Tool, + githubDeleteIssueReactionTool, + githubDeleteIssueReactionV2Tool, + githubDeleteMilestoneTool, + githubDeleteMilestoneV2Tool, githubDeleteProjectTool, githubDeleteProjectV2Tool, githubDeleteReleaseTool, githubDeleteReleaseV2Tool, + githubForkGistTool, + githubForkGistV2Tool, + githubForkRepoTool, + githubForkRepoV2Tool, githubGetBranchProtectionTool, githubGetBranchProtectionV2Tool, githubGetBranchTool, githubGetBranchV2Tool, + githubGetCommitTool, + githubGetCommitV2Tool, githubGetFileContentTool, githubGetFileContentV2Tool, + githubGetGistTool, + githubGetGistV2Tool, githubGetIssueTool, githubGetIssueV2Tool, + githubGetMilestoneTool, + githubGetMilestoneV2Tool, githubGetPRFilesTool, githubGetPRFilesV2Tool, githubGetProjectTool, @@ -287,10 +317,18 @@ import { githubLatestCommitV2Tool, githubListBranchesTool, githubListBranchesV2Tool, + githubListCommitsTool, + githubListCommitsV2Tool, + githubListForksTool, + githubListForksV2Tool, + githubListGistsTool, + githubListGistsV2Tool, githubListIssueCommentsTool, githubListIssueCommentsV2Tool, githubListIssuesTool, githubListIssuesV2Tool, + githubListMilestonesTool, + githubListMilestonesV2Tool, githubListPRCommentsTool, githubListPRCommentsV2Tool, githubListPRsTool, @@ -299,6 +337,8 @@ import { githubListProjectsV2Tool, githubListReleasesTool, githubListReleasesV2Tool, + githubListStargazersTool, + githubListStargazersV2Tool, githubListWorkflowRunsTool, githubListWorkflowRunsV2Tool, githubListWorkflowsTool, @@ -315,16 +355,38 @@ import { githubRequestReviewersV2Tool, githubRerunWorkflowTool, githubRerunWorkflowV2Tool, + githubSearchCodeTool, + githubSearchCodeV2Tool, + githubSearchCommitsTool, + githubSearchCommitsV2Tool, + githubSearchIssuesTool, + githubSearchIssuesV2Tool, + githubSearchReposTool, + githubSearchReposV2Tool, + githubSearchUsersTool, + githubSearchUsersV2Tool, + githubStarGistTool, + githubStarGistV2Tool, + githubStarRepoTool, + githubStarRepoV2Tool, githubTriggerWorkflowTool, githubTriggerWorkflowV2Tool, + githubUnstarGistTool, + githubUnstarGistV2Tool, + githubUnstarRepoTool, + githubUnstarRepoV2Tool, githubUpdateBranchProtectionTool, githubUpdateBranchProtectionV2Tool, githubUpdateCommentTool, githubUpdateCommentV2Tool, githubUpdateFileTool, githubUpdateFileV2Tool, + githubUpdateGistTool, + githubUpdateGistV2Tool, githubUpdateIssueTool, githubUpdateIssueV2Tool, + githubUpdateMilestoneTool, + githubUpdateMilestoneV2Tool, githubUpdatePRTool, githubUpdatePRV2Tool, githubUpdateProjectTool, @@ -383,40 +445,82 @@ import { googleSearchTool } from '@/tools/google' import { googleCalendarCreateTool, googleCalendarCreateV2Tool, + googleCalendarDeleteTool, + googleCalendarDeleteV2Tool, googleCalendarGetTool, googleCalendarGetV2Tool, + googleCalendarInstancesTool, + googleCalendarInstancesV2Tool, googleCalendarInviteTool, googleCalendarInviteV2Tool, + googleCalendarListCalendarsTool, + googleCalendarListCalendarsV2Tool, googleCalendarListTool, googleCalendarListV2Tool, + googleCalendarMoveTool, + googleCalendarMoveV2Tool, googleCalendarQuickAddTool, googleCalendarQuickAddV2Tool, + googleCalendarUpdateTool, + googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from '@/tools/google_docs' import { + googleDriveCopyTool, googleDriveCreateFolderTool, + googleDriveDeleteTool, googleDriveDownloadTool, + googleDriveGetAboutTool, googleDriveGetContentTool, + googleDriveGetFileTool, + googleDriveListPermissionsTool, googleDriveListTool, + googleDriveShareTool, + googleDriveTrashTool, + googleDriveUnshareTool, + googleDriveUntrashTool, + googleDriveUpdateTool, googleDriveUploadTool, } from '@/tools/google_drive' -import { googleFormsGetResponsesTool } from '@/tools/google_form' import { + googleFormsBatchUpdateTool, + googleFormsCreateFormTool, + googleFormsCreateWatchTool, + googleFormsDeleteWatchTool, + googleFormsGetFormTool, + googleFormsGetResponsesTool, + googleFormsListWatchesTool, + googleFormsRenewWatchTool, + googleFormsSetPublishSettingsTool, +} from '@/tools/google_form' +import { + googleGroupsAddAliasTool, googleGroupsAddMemberTool, googleGroupsCreateGroupTool, googleGroupsDeleteGroupTool, googleGroupsGetGroupTool, googleGroupsGetMemberTool, + googleGroupsGetSettingsTool, googleGroupsHasMemberTool, + googleGroupsListAliasesTool, googleGroupsListGroupsTool, googleGroupsListMembersTool, + googleGroupsRemoveAliasTool, googleGroupsRemoveMemberTool, googleGroupsUpdateGroupTool, googleGroupsUpdateMemberTool, + googleGroupsUpdateSettingsTool, } from '@/tools/google_groups' import { googleSheetsAppendTool, googleSheetsAppendV2Tool, + googleSheetsBatchClearV2Tool, + googleSheetsBatchGetV2Tool, + googleSheetsBatchUpdateV2Tool, + googleSheetsClearV2Tool, + googleSheetsCopySheetV2Tool, + googleSheetsCreateSpreadsheetV2Tool, + googleSheetsGetSpreadsheetV2Tool, googleSheetsReadTool, googleSheetsReadV2Tool, googleSheetsUpdateTool, @@ -427,10 +531,17 @@ import { import { googleSlidesAddImageTool, googleSlidesAddSlideTool, + googleSlidesCreateShapeTool, + googleSlidesCreateTableTool, googleSlidesCreateTool, + googleSlidesDeleteObjectTool, + googleSlidesDuplicateObjectTool, + googleSlidesGetPageTool, googleSlidesGetThumbnailTool, + googleSlidesInsertTextTool, googleSlidesReadTool, googleSlidesReplaceAllTextTool, + googleSlidesUpdateSlidesPositionTool, googleSlidesWriteTool, } from '@/tools/google_slides' import { @@ -2026,6 +2137,75 @@ export const tools: Record = { github_update_project_v2: githubUpdateProjectV2Tool, github_delete_project: githubDeleteProjectTool, github_delete_project_v2: githubDeleteProjectV2Tool, + // New GitHub tools - Search + github_search_code: githubSearchCodeTool, + github_search_code_v2: githubSearchCodeV2Tool, + github_search_commits: githubSearchCommitsTool, + github_search_commits_v2: githubSearchCommitsV2Tool, + github_search_issues: githubSearchIssuesTool, + github_search_issues_v2: githubSearchIssuesV2Tool, + github_search_repos: githubSearchReposTool, + github_search_repos_v2: githubSearchReposV2Tool, + github_search_users: githubSearchUsersTool, + github_search_users_v2: githubSearchUsersV2Tool, + // New GitHub tools - Commits + github_list_commits: githubListCommitsTool, + github_list_commits_v2: githubListCommitsV2Tool, + github_get_commit: githubGetCommitTool, + github_get_commit_v2: githubGetCommitV2Tool, + github_compare_commits: githubCompareCommitsTool, + github_compare_commits_v2: githubCompareCommitsV2Tool, + // New GitHub tools - Gists + github_create_gist: githubCreateGistTool, + github_create_gist_v2: githubCreateGistV2Tool, + github_get_gist: githubGetGistTool, + github_get_gist_v2: githubGetGistV2Tool, + github_list_gists: githubListGistsTool, + github_list_gists_v2: githubListGistsV2Tool, + github_update_gist: githubUpdateGistTool, + github_update_gist_v2: githubUpdateGistV2Tool, + github_delete_gist: githubDeleteGistTool, + github_delete_gist_v2: githubDeleteGistV2Tool, + github_fork_gist: githubForkGistTool, + github_fork_gist_v2: githubForkGistV2Tool, + github_star_gist: githubStarGistTool, + github_star_gist_v2: githubStarGistV2Tool, + github_unstar_gist: githubUnstarGistTool, + github_unstar_gist_v2: githubUnstarGistV2Tool, + // New GitHub tools - Forks + github_fork_repo: githubForkRepoTool, + github_fork_repo_v2: githubForkRepoV2Tool, + github_list_forks: githubListForksTool, + github_list_forks_v2: githubListForksV2Tool, + // New GitHub tools - Milestones + github_create_milestone: githubCreateMilestoneTool, + github_create_milestone_v2: githubCreateMilestoneV2Tool, + github_get_milestone: githubGetMilestoneTool, + github_get_milestone_v2: githubGetMilestoneV2Tool, + github_list_milestones: githubListMilestonesTool, + github_list_milestones_v2: githubListMilestonesV2Tool, + github_update_milestone: githubUpdateMilestoneTool, + github_update_milestone_v2: githubUpdateMilestoneV2Tool, + github_delete_milestone: githubDeleteMilestoneTool, + github_delete_milestone_v2: githubDeleteMilestoneV2Tool, + // New GitHub tools - Reactions + github_create_issue_reaction: githubCreateIssueReactionTool, + github_create_issue_reaction_v2: githubCreateIssueReactionV2Tool, + github_delete_issue_reaction: githubDeleteIssueReactionTool, + github_delete_issue_reaction_v2: githubDeleteIssueReactionV2Tool, + github_create_comment_reaction: githubCreateCommentReactionTool, + github_create_comment_reaction_v2: githubCreateCommentReactionV2Tool, + github_delete_comment_reaction: githubDeleteCommentReactionTool, + github_delete_comment_reaction_v2: githubDeleteCommentReactionV2Tool, + // New GitHub tools - Stars + github_star_repo: githubStarRepoTool, + github_star_repo_v2: githubStarRepoV2Tool, + github_unstar_repo: githubUnstarRepoTool, + github_unstar_repo_v2: githubUnstarRepoV2Tool, + github_check_star: githubCheckStarTool, + github_check_star_v2: githubCheckStarV2Tool, + github_list_stargazers: githubListStargazersTool, + github_list_stargazers_v2: githubListStargazersV2Tool, gitlab_list_projects: gitlabListProjectsTool, gitlab_get_project: gitlabGetProjectTool, gitlab_list_issues: gitlabListIssuesTool, @@ -2103,11 +2283,21 @@ export const tools: Record = { reddit_edit: redditEditTool, reddit_delete: redditDeleteTool, reddit_subscribe: redditSubscribeTool, + google_drive_copy: googleDriveCopyTool, + google_drive_create_folder: googleDriveCreateFolderTool, + google_drive_delete: googleDriveDeleteTool, + google_drive_download: googleDriveDownloadTool, + google_drive_get_about: googleDriveGetAboutTool, google_drive_get_content: googleDriveGetContentTool, + google_drive_get_file: googleDriveGetFileTool, google_drive_list: googleDriveListTool, + google_drive_list_permissions: googleDriveListPermissionsTool, + google_drive_share: googleDriveShareTool, + google_drive_trash: googleDriveTrashTool, + google_drive_unshare: googleDriveUnshareTool, + google_drive_untrash: googleDriveUntrashTool, + google_drive_update: googleDriveUpdateTool, google_drive_upload: googleDriveUploadTool, - google_drive_download: googleDriveDownloadTool, - google_drive_create_folder: googleDriveCreateFolderTool, google_docs_read: googleDocsReadTool, google_docs_write: googleDocsWriteTool, google_docs_create: googleDocsCreateTool, @@ -2119,6 +2309,13 @@ export const tools: Record = { google_sheets_write_v2: googleSheetsWriteV2Tool, google_sheets_update_v2: googleSheetsUpdateV2Tool, google_sheets_append_v2: googleSheetsAppendV2Tool, + google_sheets_clear_v2: googleSheetsClearV2Tool, + google_sheets_get_spreadsheet_v2: googleSheetsGetSpreadsheetV2Tool, + google_sheets_create_spreadsheet_v2: googleSheetsCreateSpreadsheetV2Tool, + google_sheets_batch_get_v2: googleSheetsBatchGetV2Tool, + google_sheets_batch_update_v2: googleSheetsBatchUpdateV2Tool, + google_sheets_batch_clear_v2: googleSheetsBatchClearV2Tool, + google_sheets_copy_sheet_v2: googleSheetsCopySheetV2Tool, google_slides_read: googleSlidesReadTool, google_slides_write: googleSlidesWriteTool, google_slides_create: googleSlidesCreateTool, @@ -2126,6 +2323,13 @@ export const tools: Record = { google_slides_add_slide: googleSlidesAddSlideTool, google_slides_get_thumbnail: googleSlidesGetThumbnailTool, google_slides_add_image: googleSlidesAddImageTool, + google_slides_get_page: googleSlidesGetPageTool, + google_slides_delete_object: googleSlidesDeleteObjectTool, + google_slides_duplicate_object: googleSlidesDuplicateObjectTool, + google_slides_update_slides_position: googleSlidesUpdateSlidesPositionTool, + google_slides_create_table: googleSlidesCreateTableTool, + google_slides_create_shape: googleSlidesCreateShapeTool, + google_slides_insert_text: googleSlidesInsertTextTool, perplexity_chat: perplexityChatTool, perplexity_search: perplexitySearchTool, pulse_parser: pulseParserTool, @@ -2509,15 +2713,33 @@ export const tools: Record = { microsoft_planner_update_task_details: microsoftPlannerUpdateTaskDetailsTool, google_calendar_create: googleCalendarCreateTool, google_calendar_create_v2: googleCalendarCreateV2Tool, + google_calendar_delete: googleCalendarDeleteTool, + google_calendar_delete_v2: googleCalendarDeleteV2Tool, google_calendar_get: googleCalendarGetTool, google_calendar_get_v2: googleCalendarGetV2Tool, + google_calendar_instances: googleCalendarInstancesTool, + google_calendar_instances_v2: googleCalendarInstancesV2Tool, + google_calendar_invite: googleCalendarInviteTool, + google_calendar_invite_v2: googleCalendarInviteV2Tool, google_calendar_list: googleCalendarListTool, google_calendar_list_v2: googleCalendarListV2Tool, + google_calendar_list_calendars: googleCalendarListCalendarsTool, + google_calendar_list_calendars_v2: googleCalendarListCalendarsV2Tool, + google_calendar_move: googleCalendarMoveTool, + google_calendar_move_v2: googleCalendarMoveV2Tool, google_calendar_quick_add: googleCalendarQuickAddTool, google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool, - google_calendar_invite: googleCalendarInviteTool, - google_calendar_invite_v2: googleCalendarInviteV2Tool, + google_calendar_update: googleCalendarUpdateTool, + google_calendar_update_v2: googleCalendarUpdateV2Tool, google_forms_get_responses: googleFormsGetResponsesTool, + google_forms_get_form: googleFormsGetFormTool, + google_forms_create_form: googleFormsCreateFormTool, + google_forms_batch_update: googleFormsBatchUpdateTool, + google_forms_set_publish_settings: googleFormsSetPublishSettingsTool, + google_forms_create_watch: googleFormsCreateWatchTool, + google_forms_list_watches: googleFormsListWatchesTool, + google_forms_delete_watch: googleFormsDeleteWatchTool, + google_forms_renew_watch: googleFormsRenewWatchTool, workflow_executor: workflowExecutorTool, wealthbox_read_contact: wealthboxReadContactTool, wealthbox_write_contact: wealthboxWriteContactTool, @@ -2567,17 +2789,22 @@ export const tools: Record = { google_vault_create_matters: createMattersTool, google_vault_list_matters: listMattersTool, google_vault_download_export_file: downloadExportFileTool, - google_groups_list_groups: googleGroupsListGroupsTool, - google_groups_get_group: googleGroupsGetGroupTool, + google_groups_add_alias: googleGroupsAddAliasTool, + google_groups_add_member: googleGroupsAddMemberTool, google_groups_create_group: googleGroupsCreateGroupTool, - google_groups_update_group: googleGroupsUpdateGroupTool, google_groups_delete_group: googleGroupsDeleteGroupTool, - google_groups_list_members: googleGroupsListMembersTool, + google_groups_get_group: googleGroupsGetGroupTool, google_groups_get_member: googleGroupsGetMemberTool, - google_groups_add_member: googleGroupsAddMemberTool, + google_groups_get_settings: googleGroupsGetSettingsTool, + google_groups_has_member: googleGroupsHasMemberTool, + google_groups_list_aliases: googleGroupsListAliasesTool, + google_groups_list_groups: googleGroupsListGroupsTool, + google_groups_list_members: googleGroupsListMembersTool, + google_groups_remove_alias: googleGroupsRemoveAliasTool, google_groups_remove_member: googleGroupsRemoveMemberTool, + google_groups_update_group: googleGroupsUpdateGroupTool, google_groups_update_member: googleGroupsUpdateMemberTool, - google_groups_has_member: googleGroupsHasMemberTool, + google_groups_update_settings: googleGroupsUpdateSettingsTool, qdrant_fetch_points: qdrantFetchTool, qdrant_search_vector: qdrantSearchTool, qdrant_upsert_points: qdrantUpsertTool, diff --git a/apps/sim/tools/schema-enrichers.ts b/apps/sim/tools/schema-enrichers.ts index f5b5532195..d8e60fe598 100644 --- a/apps/sim/tools/schema-enrichers.ts +++ b/apps/sim/tools/schema-enrichers.ts @@ -18,8 +18,6 @@ function mapFieldTypeToSchemaType(fieldType: string): string { return 'number' case 'boolean': return 'boolean' - case 'date': - case 'text': default: return 'string' } diff --git a/apps/sim/tools/spotify/add_playlist_cover.ts b/apps/sim/tools/spotify/add_playlist_cover.ts index 551958378e..c0b597e243 100644 --- a/apps/sim/tools/spotify/add_playlist_cover.ts +++ b/apps/sim/tools/spotify/add_playlist_cover.ts @@ -29,11 +29,13 @@ export const spotifyAddPlaylistCoverTool: ToolConfig< playlistId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The Spotify playlist ID', }, imageBase64: { type: 'string', required: true, + visibility: 'user-only', description: 'Base64-encoded JPEG image (max 256KB)', }, }, diff --git a/apps/sim/tools/spotify/check_following.ts b/apps/sim/tools/spotify/check_following.ts index c1bcca245c..36194608c0 100644 --- a/apps/sim/tools/spotify/check_following.ts +++ b/apps/sim/tools/spotify/check_following.ts @@ -29,11 +29,13 @@ export const spotifyCheckFollowingTool: ToolConfig< type: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Type to check: "artist" or "user"', }, ids: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated artist or user IDs (max 50)', }, }, diff --git a/apps/sim/tools/spotify/check_playlist_followers.ts b/apps/sim/tools/spotify/check_playlist_followers.ts index 973647563b..393e7b676e 100644 --- a/apps/sim/tools/spotify/check_playlist_followers.ts +++ b/apps/sim/tools/spotify/check_playlist_followers.ts @@ -29,11 +29,13 @@ export const spotifyCheckPlaylistFollowersTool: ToolConfig< playlistId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The Spotify playlist ID', }, userIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated user IDs to check (max 5)', }, }, diff --git a/apps/sim/tools/spotify/check_saved_albums.ts b/apps/sim/tools/spotify/check_saved_albums.ts index 751d6260c1..44a369a542 100644 --- a/apps/sim/tools/spotify/check_saved_albums.ts +++ b/apps/sim/tools/spotify/check_saved_albums.ts @@ -28,6 +28,7 @@ export const spotifyCheckSavedAlbumsTool: ToolConfig< albumIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated album IDs (max 20)', }, }, diff --git a/apps/sim/tools/spotify/check_saved_audiobooks.ts b/apps/sim/tools/spotify/check_saved_audiobooks.ts index 2c14ebe01a..b64a65a745 100644 --- a/apps/sim/tools/spotify/check_saved_audiobooks.ts +++ b/apps/sim/tools/spotify/check_saved_audiobooks.ts @@ -28,6 +28,7 @@ export const spotifyCheckSavedAudiobooksTool: ToolConfig< audiobookIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated audiobook IDs (max 50)', }, }, diff --git a/apps/sim/tools/spotify/check_saved_episodes.ts b/apps/sim/tools/spotify/check_saved_episodes.ts index 45027e1dd3..22116ce302 100644 --- a/apps/sim/tools/spotify/check_saved_episodes.ts +++ b/apps/sim/tools/spotify/check_saved_episodes.ts @@ -28,6 +28,7 @@ export const spotifyCheckSavedEpisodesTool: ToolConfig< episodeIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated episode IDs (max 50)', }, }, diff --git a/apps/sim/tools/spotify/check_saved_shows.ts b/apps/sim/tools/spotify/check_saved_shows.ts index 22efd1e2fe..41e7a62fb8 100644 --- a/apps/sim/tools/spotify/check_saved_shows.ts +++ b/apps/sim/tools/spotify/check_saved_shows.ts @@ -28,6 +28,7 @@ export const spotifyCheckSavedShowsTool: ToolConfig< showIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated show IDs (max 50)', }, }, diff --git a/apps/sim/tools/spotify/follow_playlist.ts b/apps/sim/tools/spotify/follow_playlist.ts index 62f2c6ea4d..51c35af257 100644 --- a/apps/sim/tools/spotify/follow_playlist.ts +++ b/apps/sim/tools/spotify/follow_playlist.ts @@ -29,11 +29,13 @@ export const spotifyFollowPlaylistTool: ToolConfig< playlistId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The Spotify playlist ID', }, public: { type: 'boolean', required: false, + visibility: 'user-only', default: true, description: 'Whether the playlist will be in public playlists', }, diff --git a/apps/sim/tools/spotify/get_audiobook.ts b/apps/sim/tools/spotify/get_audiobook.ts index 5268432088..9d373251e9 100644 --- a/apps/sim/tools/spotify/get_audiobook.ts +++ b/apps/sim/tools/spotify/get_audiobook.ts @@ -40,11 +40,13 @@ export const spotifyGetAudiobookTool: ToolConfig< audiobookId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The Spotify audiobook ID', }, market: { type: 'string', required: false, + visibility: 'user-only', description: 'ISO country code for market', }, }, diff --git a/apps/sim/tools/spotify/get_audiobook_chapters.ts b/apps/sim/tools/spotify/get_audiobook_chapters.ts index 451a082ce1..9f50e2acb4 100644 --- a/apps/sim/tools/spotify/get_audiobook_chapters.ts +++ b/apps/sim/tools/spotify/get_audiobook_chapters.ts @@ -42,23 +42,27 @@ export const spotifyGetAudiobookChaptersTool: ToolConfig< audiobookId: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'The Spotify audiobook ID', }, limit: { type: 'number', required: false, + visibility: 'user-only', default: 20, description: 'Number of chapters to return (1-50)', }, offset: { type: 'number', required: false, + visibility: 'user-only', default: 0, description: 'Index of first chapter to return', }, market: { type: 'string', required: false, + visibility: 'user-only', description: 'ISO country code for market', }, }, diff --git a/apps/sim/tools/spotify/get_audiobooks.ts b/apps/sim/tools/spotify/get_audiobooks.ts index 3e5519b36d..fc93c69a40 100644 --- a/apps/sim/tools/spotify/get_audiobooks.ts +++ b/apps/sim/tools/spotify/get_audiobooks.ts @@ -38,11 +38,13 @@ export const spotifyGetAudiobooksTool: ToolConfig< audiobookIds: { type: 'string', required: true, + visibility: 'user-or-llm', description: 'Comma-separated audiobook IDs (max 50)', }, market: { type: 'string', required: false, + visibility: 'user-only', description: 'ISO country code for market', }, }, diff --git a/apps/sim/tools/spotify/get_episode.ts b/apps/sim/tools/spotify/get_episode.ts index c6a0e373c1..48772fb3d5 100644 --- a/apps/sim/tools/spotify/get_episode.ts +++ b/apps/sim/tools/spotify/get_episode.ts @@ -37,11 +37,13 @@ export const spotifyGetEpisodeTool: ToolConfig Date: Sat, 17 Jan 2026 20:33:48 -0800 Subject: [PATCH 2/4] fixed the name for google forms --- .../content/docs/en/tools/google_forms.mdx | 141 +++++++++++------- .../{google_form.ts => google_forms.ts} | 0 apps/sim/tools/google_form/index.ts | 21 --- .../batch_update.ts | 4 +- .../create_form.ts | 4 +- .../create_watch.ts | 4 +- .../delete_watch.ts | 4 +- .../{google_form => google_forms}/get_form.ts | 4 +- .../get_responses.ts | 4 +- apps/sim/tools/google_forms/index.ts | 21 +++ .../list_watches.ts | 4 +- .../renew_watch.ts | 4 +- .../set_publish_settings.ts | 4 +- .../{google_form => google_forms}/types.ts | 0 .../{google_form => google_forms}/utils.ts | 0 apps/sim/tools/registry.ts | 2 +- 16 files changed, 125 insertions(+), 96 deletions(-) rename apps/sim/blocks/blocks/{google_form.ts => google_forms.ts} (100%) delete mode 100644 apps/sim/tools/google_form/index.ts rename apps/sim/tools/{google_form => google_forms}/batch_update.ts (96%) rename apps/sim/tools/{google_form => google_forms}/create_form.ts (96%) rename apps/sim/tools/{google_form => google_forms}/create_watch.ts (97%) rename apps/sim/tools/{google_form => google_forms}/delete_watch.ts (94%) rename apps/sim/tools/{google_form => google_forms}/get_form.ts (97%) rename apps/sim/tools/{google_form => google_forms}/get_responses.ts (98%) create mode 100644 apps/sim/tools/google_forms/index.ts rename apps/sim/tools/{google_form => google_forms}/list_watches.ts (96%) rename apps/sim/tools/{google_form => google_forms}/renew_watch.ts (95%) rename apps/sim/tools/{google_form => google_forms}/set_publish_settings.ts (96%) rename apps/sim/tools/{google_form => google_forms}/types.ts (100%) rename apps/sim/tools/{google_form => google_forms}/utils.ts (100%) diff --git a/apps/docs/content/docs/en/tools/google_forms.mdx b/apps/docs/content/docs/en/tools/google_forms.mdx index 8e05a62dd5..3a43f590da 100644 --- a/apps/docs/content/docs/en/tools/google_forms.mdx +++ b/apps/docs/content/docs/en/tools/google_forms.mdx @@ -37,10 +37,15 @@ Integrate Google Forms into your workflow. Read form structure, get responses, c ### `google_forms_get_responses` +Retrieve a single response or list responses from a Google Form + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form | +| `responseId` | string | No | If provided, returns this specific response | +| `pageSize` | number | No | Maximum number of responses to return \(service may return fewer\). Defaults to 5000. | #### Output @@ -56,154 +61,178 @@ Integrate Google Forms into your workflow. Read form structure, get responses, c ### `google_forms_get_form` +Retrieve a form structure including its items, settings, and metadata + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form to retrieve | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `formId` | string | The form ID | +| `title` | string | The form title visible to responders | +| `description` | string | The form description | +| `documentTitle` | string | The document title visible in Drive | +| `responderUri` | string | The URI to share with responders | +| `linkedSheetId` | string | The ID of the linked Google Sheet | +| `revisionId` | string | The revision ID of the form | +| `items` | array | The form items \(questions, sections, etc.\) | +| ↳ `itemId` | string | Item ID | +| ↳ `title` | string | Item title | +| ↳ `description` | string | Item description | +| `settings` | json | Form settings | +| `publishSettings` | json | Form publish settings | ### `google_forms_create_form` +Create a new Google Form with a title + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `title` | string | Yes | The title of the form visible to responders | +| `documentTitle` | string | No | The document title visible in Drive \(defaults to form title\) | +| `unpublished` | boolean | No | If true, create an unpublished form that does not accept responses | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `formId` | string | The ID of the created form | +| `title` | string | The form title | +| `documentTitle` | string | The document title in Drive | +| `responderUri` | string | The URI to share with responders | +| `revisionId` | string | The revision ID of the form | ### `google_forms_batch_update` +Apply multiple updates to a form (add items, update info, change settings, etc.) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form to update | +| `requests` | json | Yes | Array of update requests \(updateFormInfo, updateSettings, createItem, updateItem, moveItem, deleteItem\) | +| `includeFormInResponse` | boolean | No | Whether to return the updated form in the response | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `replies` | array | The replies from each update request | +| `writeControl` | json | Write control information with revision IDs | +| `form` | json | The updated form \(if includeFormInResponse was true\) | ### `google_forms_set_publish_settings` +Update the publish settings of a form (publish/unpublish, accept responses) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form | +| `isPublished` | boolean | Yes | Whether the form is published and visible to others | +| `isAcceptingResponses` | boolean | No | Whether the form accepts responses \(forced to false if isPublished is false\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `formId` | string | The form ID | +| `publishSettings` | json | The updated publish settings | +| ↳ `publishState` | object | The publish state | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses | ### `google_forms_create_watch` +Create a notification watch for form changes (schema changes or new responses) + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form to watch | +| `eventType` | string | Yes | Event type to watch: SCHEMA \(form changes\) or RESPONSES \(new submissions\) | +| `topicName` | string | Yes | The Cloud Pub/Sub topic name \(format: projects/\{project\}/topics/\{topic\}\) | +| `watchId` | string | No | Custom watch ID \(4-63 chars, lowercase letters, numbers, hyphens\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `id` | string | The watch ID | +| `eventType` | string | The event type being watched | +| `topicName` | string | The Cloud Pub/Sub topic | +| `createTime` | string | When the watch was created | +| `expireTime` | string | When the watch expires \(7 days after creation\) | +| `state` | string | The watch state \(ACTIVE, SUSPENDED\) | ### `google_forms_list_watches` +List all notification watches for a form + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `watches` | array | List of watches for the form | +| ↳ `id` | string | Watch ID | +| ↳ `eventType` | string | Event type \(SCHEMA or RESPONSES\) | +| ↳ `createTime` | string | When the watch was created | +| ↳ `expireTime` | string | When the watch expires | +| ↳ `state` | string | Watch state | ### `google_forms_delete_watch` +Delete a notification watch from a form + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form | +| `watchId` | string | Yes | The ID of the watch to delete | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `deleted` | boolean | Whether the watch was successfully deleted | ### `google_forms_renew_watch` +Renew a notification watch for another 7 days + #### Input | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `formId` | string | Yes | The ID of the Google Form | +| `watchId` | string | Yes | The ID of the watch to renew | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `response` | json | Operation response data | -| `formId` | string | Form ID | -| `title` | string | Form title | -| `responderUri` | string | Form responder URL | -| `items` | json | Form items | -| `responses` | json | Form responses | -| `watches` | json | Form watches | +| `id` | string | The watch ID | +| `eventType` | string | The event type being watched | +| `expireTime` | string | The new expiration time | +| `state` | string | The watch state | diff --git a/apps/sim/blocks/blocks/google_form.ts b/apps/sim/blocks/blocks/google_forms.ts similarity index 100% rename from apps/sim/blocks/blocks/google_form.ts rename to apps/sim/blocks/blocks/google_forms.ts diff --git a/apps/sim/tools/google_form/index.ts b/apps/sim/tools/google_form/index.ts deleted file mode 100644 index 2c5eaa40cc..0000000000 --- a/apps/sim/tools/google_form/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { batchUpdateTool } from '@/tools/google_form/batch_update' -import { createFormTool } from '@/tools/google_form/create_form' -import { createWatchTool } from '@/tools/google_form/create_watch' -import { deleteWatchTool } from '@/tools/google_form/delete_watch' -import { getFormTool } from '@/tools/google_form/get_form' -import { getResponsesTool } from '@/tools/google_form/get_responses' -import { listWatchesTool } from '@/tools/google_form/list_watches' -import { renewWatchTool } from '@/tools/google_form/renew_watch' -import { setPublishSettingsTool } from '@/tools/google_form/set_publish_settings' - -export const googleFormsGetResponsesTool = getResponsesTool -export const googleFormsGetFormTool = getFormTool -export const googleFormsCreateFormTool = createFormTool -export const googleFormsBatchUpdateTool = batchUpdateTool -export const googleFormsSetPublishSettingsTool = setPublishSettingsTool -export const googleFormsCreateWatchTool = createWatchTool -export const googleFormsListWatchesTool = listWatchesTool -export const googleFormsDeleteWatchTool = deleteWatchTool -export const googleFormsRenewWatchTool = renewWatchTool - -export * from './types' diff --git a/apps/sim/tools/google_form/batch_update.ts b/apps/sim/tools/google_forms/batch_update.ts similarity index 96% rename from apps/sim/tools/google_form/batch_update.ts rename to apps/sim/tools/google_forms/batch_update.ts index 12c60d7199..0cbea9f9ae 100644 --- a/apps/sim/tools/google_form/batch_update.ts +++ b/apps/sim/tools/google_forms/batch_update.ts @@ -2,8 +2,8 @@ import type { GoogleForm, GoogleFormsBatchUpdateParams, GoogleFormsBatchUpdateResponse, -} from '@/tools/google_form/types' -import { buildBatchUpdateUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildBatchUpdateUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' interface BatchUpdateApiResponse { diff --git a/apps/sim/tools/google_form/create_form.ts b/apps/sim/tools/google_forms/create_form.ts similarity index 96% rename from apps/sim/tools/google_form/create_form.ts rename to apps/sim/tools/google_forms/create_form.ts index 8b1abcdf52..bd31a9bd72 100644 --- a/apps/sim/tools/google_form/create_form.ts +++ b/apps/sim/tools/google_forms/create_form.ts @@ -2,8 +2,8 @@ import type { GoogleForm, GoogleFormsCreateFormParams, GoogleFormsCreateFormResponse, -} from '@/tools/google_form/types' -import { buildCreateFormUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildCreateFormUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const createFormTool: ToolConfig< diff --git a/apps/sim/tools/google_form/create_watch.ts b/apps/sim/tools/google_forms/create_watch.ts similarity index 97% rename from apps/sim/tools/google_form/create_watch.ts rename to apps/sim/tools/google_forms/create_watch.ts index fb145548e5..4f7874d577 100644 --- a/apps/sim/tools/google_form/create_watch.ts +++ b/apps/sim/tools/google_forms/create_watch.ts @@ -2,8 +2,8 @@ import type { GoogleFormsCreateWatchParams, GoogleFormsCreateWatchResponse, GoogleFormsWatch, -} from '@/tools/google_form/types' -import { buildCreateWatchUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildCreateWatchUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const createWatchTool: ToolConfig< diff --git a/apps/sim/tools/google_form/delete_watch.ts b/apps/sim/tools/google_forms/delete_watch.ts similarity index 94% rename from apps/sim/tools/google_form/delete_watch.ts rename to apps/sim/tools/google_forms/delete_watch.ts index c6e92b60fa..41eed7bd6c 100644 --- a/apps/sim/tools/google_form/delete_watch.ts +++ b/apps/sim/tools/google_forms/delete_watch.ts @@ -1,8 +1,8 @@ import type { GoogleFormsDeleteWatchParams, GoogleFormsDeleteWatchResponse, -} from '@/tools/google_form/types' -import { buildDeleteWatchUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildDeleteWatchUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const deleteWatchTool: ToolConfig< diff --git a/apps/sim/tools/google_form/get_form.ts b/apps/sim/tools/google_forms/get_form.ts similarity index 97% rename from apps/sim/tools/google_form/get_form.ts rename to apps/sim/tools/google_forms/get_form.ts index b99541872f..0aab231b09 100644 --- a/apps/sim/tools/google_form/get_form.ts +++ b/apps/sim/tools/google_forms/get_form.ts @@ -2,8 +2,8 @@ import type { GoogleForm, GoogleFormsGetFormParams, GoogleFormsGetFormResponse, -} from '@/tools/google_form/types' -import { buildGetFormUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildGetFormUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const getFormTool: ToolConfig = { diff --git a/apps/sim/tools/google_form/get_responses.ts b/apps/sim/tools/google_forms/get_responses.ts similarity index 98% rename from apps/sim/tools/google_form/get_responses.ts rename to apps/sim/tools/google_forms/get_responses.ts index 9cdc754c21..1778a9fb50 100644 --- a/apps/sim/tools/google_form/get_responses.ts +++ b/apps/sim/tools/google_forms/get_responses.ts @@ -2,8 +2,8 @@ import type { GoogleFormsGetResponsesParams, GoogleFormsResponse, GoogleFormsResponseList, -} from '@/tools/google_form/types' -import { buildGetResponseUrl, buildListResponsesUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildGetResponseUrl, buildListResponsesUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const getResponsesTool: ToolConfig = { diff --git a/apps/sim/tools/google_forms/index.ts b/apps/sim/tools/google_forms/index.ts new file mode 100644 index 0000000000..919daff2c4 --- /dev/null +++ b/apps/sim/tools/google_forms/index.ts @@ -0,0 +1,21 @@ +import { batchUpdateTool } from '@/tools/google_forms/batch_update' +import { createFormTool } from '@/tools/google_forms/create_form' +import { createWatchTool } from '@/tools/google_forms/create_watch' +import { deleteWatchTool } from '@/tools/google_forms/delete_watch' +import { getFormTool } from '@/tools/google_forms/get_form' +import { getResponsesTool } from '@/tools/google_forms/get_responses' +import { listWatchesTool } from '@/tools/google_forms/list_watches' +import { renewWatchTool } from '@/tools/google_forms/renew_watch' +import { setPublishSettingsTool } from '@/tools/google_forms/set_publish_settings' + +export const googleFormsGetResponsesTool = getResponsesTool +export const googleFormsGetFormTool = getFormTool +export const googleFormsCreateFormTool = createFormTool +export const googleFormsBatchUpdateTool = batchUpdateTool +export const googleFormsSetPublishSettingsTool = setPublishSettingsTool +export const googleFormsCreateWatchTool = createWatchTool +export const googleFormsListWatchesTool = listWatchesTool +export const googleFormsDeleteWatchTool = deleteWatchTool +export const googleFormsRenewWatchTool = renewWatchTool + +export * from './types' diff --git a/apps/sim/tools/google_form/list_watches.ts b/apps/sim/tools/google_forms/list_watches.ts similarity index 96% rename from apps/sim/tools/google_form/list_watches.ts rename to apps/sim/tools/google_forms/list_watches.ts index 843f52e915..3442c0afaa 100644 --- a/apps/sim/tools/google_form/list_watches.ts +++ b/apps/sim/tools/google_forms/list_watches.ts @@ -2,8 +2,8 @@ import type { GoogleFormsListWatchesParams, GoogleFormsListWatchesResponse, GoogleFormsWatch, -} from '@/tools/google_form/types' -import { buildListWatchesUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildListWatchesUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' interface ListWatchesApiResponse { diff --git a/apps/sim/tools/google_form/renew_watch.ts b/apps/sim/tools/google_forms/renew_watch.ts similarity index 95% rename from apps/sim/tools/google_form/renew_watch.ts rename to apps/sim/tools/google_forms/renew_watch.ts index eb51e16a46..21a6a9a93e 100644 --- a/apps/sim/tools/google_form/renew_watch.ts +++ b/apps/sim/tools/google_forms/renew_watch.ts @@ -2,8 +2,8 @@ import type { GoogleFormsRenewWatchParams, GoogleFormsRenewWatchResponse, GoogleFormsWatch, -} from '@/tools/google_form/types' -import { buildRenewWatchUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildRenewWatchUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' export const renewWatchTool: ToolConfig< diff --git a/apps/sim/tools/google_form/set_publish_settings.ts b/apps/sim/tools/google_forms/set_publish_settings.ts similarity index 96% rename from apps/sim/tools/google_form/set_publish_settings.ts rename to apps/sim/tools/google_forms/set_publish_settings.ts index b0e826e935..fa3a3b5ad2 100644 --- a/apps/sim/tools/google_form/set_publish_settings.ts +++ b/apps/sim/tools/google_forms/set_publish_settings.ts @@ -2,8 +2,8 @@ import type { GoogleFormsPublishSettings, GoogleFormsSetPublishSettingsParams, GoogleFormsSetPublishSettingsResponse, -} from '@/tools/google_form/types' -import { buildSetPublishSettingsUrl } from '@/tools/google_form/utils' +} from '@/tools/google_forms/types' +import { buildSetPublishSettingsUrl } from '@/tools/google_forms/utils' import type { ToolConfig } from '@/tools/types' interface SetPublishSettingsApiResponse { diff --git a/apps/sim/tools/google_form/types.ts b/apps/sim/tools/google_forms/types.ts similarity index 100% rename from apps/sim/tools/google_form/types.ts rename to apps/sim/tools/google_forms/types.ts diff --git a/apps/sim/tools/google_form/utils.ts b/apps/sim/tools/google_forms/utils.ts similarity index 100% rename from apps/sim/tools/google_form/utils.ts rename to apps/sim/tools/google_forms/utils.ts diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 875d7683fc..2a8088477e 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -492,7 +492,7 @@ import { googleFormsListWatchesTool, googleFormsRenewWatchTool, googleFormsSetPublishSettingsTool, -} from '@/tools/google_form' +} from '@/tools/google_forms' import { googleGroupsAddAliasTool, googleGroupsAddMemberTool, From dbfa68f192f53c076760047f285e67402dd2afad Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 17 Jan 2026 20:36:13 -0800 Subject: [PATCH 3/4] revert schema enrichers change --- apps/sim/tools/schema-enrichers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/tools/schema-enrichers.ts b/apps/sim/tools/schema-enrichers.ts index d8e60fe598..f5b5532195 100644 --- a/apps/sim/tools/schema-enrichers.ts +++ b/apps/sim/tools/schema-enrichers.ts @@ -18,6 +18,8 @@ function mapFieldTypeToSchemaType(fieldType: string): string { return 'number' case 'boolean': return 'boolean' + case 'date': + case 'text': default: return 'string' } From 1ce36dfb9c5cf2cc5f69c820de79534bb47d640d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 17 Jan 2026 20:42:14 -0800 Subject: [PATCH 4/4] fixed block ordering --- apps/sim/blocks/registry.ts | 40 ++++++++++++++---------------- apps/sim/tools/schema-enrichers.ts | 2 -- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 33ff415f72..544c294322 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -8,7 +8,6 @@ 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' @@ -38,7 +37,7 @@ import { GoogleSearchBlock } from '@/blocks/blocks/google' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' import { GoogleDocsBlock } from '@/blocks/blocks/google_docs' import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' -import { GoogleFormsBlock } from '@/blocks/blocks/google_form' +import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups' import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets' import { GoogleSlidesBlock } from '@/blocks/blocks/google_slides' @@ -114,6 +113,7 @@ import { ShopifyBlock } from '@/blocks/blocks/shopify' import { SlackBlock } from '@/blocks/blocks/slack' import { SmtpBlock } from '@/blocks/blocks/smtp' import { SpotifyBlock } from '@/blocks/blocks/spotify' +import { SQSBlock } from '@/blocks/blocks/sqs' import { SSHBlock } from '@/blocks/blocks/ssh' import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' @@ -149,7 +149,6 @@ import { ZendeskBlock } from '@/blocks/blocks/zendesk' import { ZepBlock } from '@/blocks/blocks/zep' import { ZoomBlock } from '@/blocks/blocks/zoom' import type { BlockConfig } from '@/blocks/types' -import { SQSBlock } from './blocks/sqs' // Registry of all available blocks, alphabetically sorted export const registry: Record = { @@ -163,7 +162,6 @@ export const registry: Record = { apollo: ApolloBlock, arxiv: ArxivBlock, asana: AsanaBlock, - // box: BoxBlock, // TODO: Box OAuth integration browser_use: BrowserUseBlock, calendly: CalendlyBlock, chat_trigger: ChatTriggerBlock, @@ -177,8 +175,9 @@ export const registry: Record = { discord: DiscordBlock, dropbox: DropboxBlock, duckduckgo: DuckDuckGoBlock, - elevenlabs: ElevenLabsBlock, + dynamodb: DynamoDBBlock, elasticsearch: ElasticsearchBlock, + elevenlabs: ElevenLabsBlock, evaluator: EvaluatorBlock, exa: ExaBlock, file: FileBlock, @@ -191,21 +190,21 @@ export const registry: Record = { gitlab: GitLabBlock, gmail: GmailBlock, gmail_v2: GmailV2Block, - grain: GrainBlock, - grafana: GrafanaBlock, - greptile: GreptileBlock, - guardrails: GuardrailsBlock, google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, google_forms: GoogleFormsBlock, + google_groups: GoogleGroupsBlock, google_search: GoogleSearchBlock, google_sheets: GoogleSheetsBlock, google_sheets_v2: GoogleSheetsV2Block, google_slides: GoogleSlidesBlock, google_vault: GoogleVaultBlock, - google_groups: GoogleGroupsBlock, + grafana: GrafanaBlock, + grain: GrainBlock, + greptile: GreptileBlock, + guardrails: GuardrailsBlock, hubspot: HubSpotBlock, huggingface: HuggingFaceBlock, human_in_the_loop: HumanInTheLoopBlock, @@ -237,7 +236,6 @@ export const registry: Record = { microsoft_planner: MicrosoftPlannerBlock, microsoft_teams: MicrosoftTeamsBlock, mistral_parse: MistralParseBlock, - reducto: ReductoBlock, mongodb: MongoDBBlock, mysql: MySQLBlock, neo4j: Neo4jBlock, @@ -257,35 +255,34 @@ export const registry: Record = { pulse: PulseBlock, qdrant: QdrantBlock, rds: RDSBlock, - sqs: SQSBlock, - dynamodb: DynamoDBBlock, reddit: RedditBlock, + reducto: ReductoBlock, resend: ResendBlock, response: ResponseBlock, - rss: RssBlock, router: RouterBlock, router_v2: RouterV2Block, + rss: RssBlock, s3: S3Block, salesforce: SalesforceBlock, schedule: ScheduleBlock, search: SearchBlock, sendgrid: SendGridBlock, sentry: SentryBlock, - servicenow: ServiceNowBlock, serper: SerperBlock, + servicenow: ServiceNowBlock, + sftp: SftpBlock, sharepoint: SharepointBlock, shopify: ShopifyBlock, slack: SlackBlock, - spotify: SpotifyBlock, smtp: SmtpBlock, - sftp: SftpBlock, + spotify: SpotifyBlock, + sqs: SQSBlock, ssh: SSHBlock, stagehand: StagehandBlock, - starter: StarterBlock, start_trigger: StartTriggerBlock, - stt: SttBlock, - tts: TtsBlock, + starter: StarterBlock, stripe: StripeBlock, + stt: SttBlock, supabase: SupabaseBlock, tavily: TavilyBlock, telegram: TelegramBlock, @@ -293,6 +290,7 @@ export const registry: Record = { tinybird: TinybirdBlock, translate: TranslateBlock, trello: TrelloBlock, + tts: TtsBlock, twilio_sms: TwilioSMSBlock, twilio_voice: TwilioVoiceBlock, typeform: TypeformBlock, @@ -310,8 +308,8 @@ export const registry: Record = { workflow_input: WorkflowInputBlock, x: XBlock, youtube: YouTubeBlock, - zep: ZepBlock, zendesk: ZendeskBlock, + zep: ZepBlock, zoom: ZoomBlock, } diff --git a/apps/sim/tools/schema-enrichers.ts b/apps/sim/tools/schema-enrichers.ts index f5b5532195..d8e60fe598 100644 --- a/apps/sim/tools/schema-enrichers.ts +++ b/apps/sim/tools/schema-enrichers.ts @@ -18,8 +18,6 @@ function mapFieldTypeToSchemaType(fieldType: string): string { return 'number' case 'boolean': return 'boolean' - case 'date': - case 'text': default: return 'string' }