Compare commits

...

22 Commits

Author SHA1 Message Date
Waleed
4fd0989264 v0.5.101: circular dependency mitigation, confluence enhancements, google tasks and bigquery integrations, workflow lock 2026-02-26 15:04:53 -08:00
Waleed
345a95f48d fix(confluence): prevent content erasure on page/blogpost update and fix space update (#3356)
- Add body-format=storage to GET-before-PUT for page and blogpost updates
  (without this, Confluence v2 API does not return body content, causing
  the fallback to erase content when only updating the title)
- Fetch current space name when updating only description (Confluence API
  requires name on PUT, so we preserve the existing name automatically)
2026-02-26 14:52:57 -08:00
Waleed
e07963f88c chore(db): drop 8 redundant indexes and add partial index for stale execution cleanup (#3354) 2026-02-26 13:17:39 -08:00
Waleed
25c59e3e2e feat(devin): add devin integration for autonomous coding sessions (#3352)
* feat(devin): add devin integration for autonomous coding sessions

* lint

* improvement(devin): update tool names and add manual docs description

* improvement(devin): rename tool files to snake_case and regenerate docs

* regen docs

* fix(devin): remove redundant Number() conversions in tool request bodies
2026-02-26 11:57:50 -08:00
Waleed
dde098e8e5 fix: prevent raw workflowInput from overwriting coerced start block values (#3347)
buildUnifiedStartOutput and buildIntegrationTriggerOutput first populate
output with schema-coerced structuredInput values (via coerceValue), then
iterate workflowInput and unconditionally overwrite those keys with raw
strings. This causes typed values (arrays, objects, numbers, booleans)
passed to child workflows to arrive as stringified versions.

Add a structuredKeys guard so the workflowInput loop skips keys already
set by the coerced structuredInput, letting coerceValue's type-aware
parsing (JSON.parse for objects/arrays, Number() for numbers, etc.)
take effect.

Fixes #3105

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:13:19 -08:00
Waleed
5ae0115444 feat(sidebar): add lock/unlock to workflow registry context menu (#3350)
* feat(sidebar): add lock/unlock to workflow registry context menu

* docs(tools): add manual descriptions to google_books and table

* docs(tools): add manual descriptions to google_bigquery and google_tasks

* fix(sidebar): avoid unnecessary store subscriptions and fix mixed lock state toggle

* fix(sidebar): use getWorkflowLockToggleIds utility for lock toggle

Replaces manual pivot-sorting logic with the existing utility function,
which handles block ordering and no-op guards consistently.

* lint
2026-02-25 23:40:30 -08:00
Waleed
fbafe204e5 fix(confluence): add input validation for SSRF-flagged parameters (#3351) 2026-02-25 23:35:45 -08:00
Waleed
ba7d6ff298 fix(credential-selector): remove reserved icon space when no credential selected (#3348) 2026-02-25 22:29:35 -08:00
Waleed
40016e79a1 feat(google-tasks): add Google Tasks integration (#3342)
* feat(google-tasks): add Google Tasks integration

* fix(google-tasks): return actual taskId in delete response

* fix(google-tasks): use absolute imports and fix registry order

* fix(google-tasks): rename list-task-lists to list_task_lists for doc generator

* improvement(google-tasks): destructure task and taskList outputs with typed schemas

* ran lint

* improvement(google-tasks): add wandConfig for due date timestamp generation
2026-02-25 21:52:34 -08:00
Waleed
e4fb8b2fdd feat(bigquery): add Google BigQuery integration (#3341)
* feat(bigquery): add Google BigQuery integration

* fix(bigquery): add auth provider, fix docsLink and insertedRows count

* fix(bigquery): set pageToken visibility to user-or-llm for pagination

* fix(bigquery): use prefixed export names to avoid aliased imports

* lint

* improvement(bigquery): destructure tool outputs with structured array/object types

* lint
2026-02-25 19:31:06 -08:00
Waleed
d98545d554 fix(terminal): thread executionOrder through child workflow SSE events for loop support (#3346)
* fix(terminal): thread executionOrder through child workflow SSE events for loop support

* ran lint

* fix(terminal): render iteration children through EntryNodeRow for workflow block expansion

IterationNodeRow was rendering all children as flat BlockRow components,
ignoring nodeType. Workflow blocks inside loop iterations were never
rendered as WorkflowNodeRow, so they had no expand chevron or child tree.

* fix(terminal): add childWorkflowBlockId to matchesEntryForUpdate

Sub-executors reset executionOrderCounter, so child blocks across loop
iterations share the same blockId + executionOrder. Without checking
childWorkflowBlockId, updateConsole for iteration N overwrites entries
from iterations 0..N-1, causing all child blocks to be grouped under
the last iteration's workflow instance.
2026-02-25 19:02:44 -08:00
Waleed
fadbad4085 feat(confluence): add get user by account ID tool (#3345)
* feat(confluence): add get user by account ID tool

* feat(confluence): add missing tools for tasks, blog posts, spaces, descendants, permissions, and properties

Add 16 new Confluence operations: list/get/update tasks, update/delete blog posts,
create/update/delete spaces, get page descendants, list space permissions,
list/create/delete space properties. Includes API routes, tool definitions,
block config wiring, OAuth scopes, and generated docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): add missing OAuth scopes to auth.ts provider config

The OAuth authorization flow uses scopes from auth.ts, not oauth.ts.
The 9 new scopes were only added to oauth.ts and the block config but
not to the actual provider config in auth.ts, causing re-auth to still
return tokens without the new scopes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(confluence): fix truncated get_user tool description in docs

Remove apostrophe from description that caused MDX generation to
truncate at the escape character.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): address PR review feedback

- Move get_user from GET to POST to avoid exposing access token in URL
- Add 400 validation for missing params in space-properties create/delete
- Add null check for blog post version before update to prevent TypeError

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(confluence): add missing response fields for descendants and tasks

- Add type and depth fields to page descendants (from Confluence API)
- Add body field (storage format) to task list/get/update responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* lint

* fix(confluence): use validatePathSegment for Atlassian account IDs

validateAlphanumericId rejects valid Atlassian account IDs that contain
colons (e.g. 557058:6b9c9931-4693-49c1-8b3a-931f1af98134). Use
validatePathSegment with a custom pattern allowing colons instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ran lint

* update mock

* upgrade turborepo

* fix(confluence): reject empty update body for space PUT

Return 400 when neither name nor description is provided for space
update, instead of sending an empty body to the Confluence API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): remove spaceId requirement for create_space and fix list_tasks pagination

- Remove create_space from spaceId condition array since creating a space
  doesn't require a space ID input
- Remove list_tasks from generic supportsCursor array so it uses its
  dedicated handler that correctly passes assignedTo and status filters
  during pagination

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ran lint

* fixed type errors

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:16:53 -08:00
Waleed
67f8a687f6 v0.5.100: multiple credentials, 40% speedup, gong, attio, audit log improvements 2026-02-25 00:28:25 -08:00
Waleed
af592349d3 v0.5.99: local dev improvements, live workflow logs in terminal 2026-02-23 00:24:49 -08:00
Waleed
0d86ea01f0 v0.5.98: change detection improvements, rate limit and code execution fixes, removed retired models, hex integration 2026-02-21 18:07:40 -08:00
Waleed
115f04e989 v0.5.97: oidc discovery for copilot mcp 2026-02-21 02:06:25 -08:00
Waleed
34d92fae89 v0.5.96: sim oauth provider, slack ephemeral message tool and blockkit support 2026-02-20 18:22:20 -08:00
Waleed
67aa4bb332 v0.5.95: gemini 3.1 pro, cloudflare, dataverse, revenuecat, redis, upstash, algolia tools; isolated-vm robustness improvements, tables backend (#3271)
* feat(tools): advanced fields for youtube, vercel; added cloudflare and dataverse tools (#3257)

* refactor(vercel): mark optional fields as advanced mode

Move optional/power-user fields behind the advanced toggle:
- List Deployments: project filter, target, state
- Create Deployment: project ID override, redeploy from, target
- List Projects: search
- Create/Update Project: framework, build/output/install commands
- Env Vars: variable type
- Webhooks: project IDs filter
- Checks: path, details URL
- Team Members: role filter
- All operations: team ID scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(youtube): mark optional params as advanced mode

Hide pagination, sort order, and filter fields behind the advanced
toggle for a cleaner default UX across all YouTube operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* added advanced fields for vercel and youtube, added cloudflare and dataverse block

* addded desc for dataverse

* add more tools

* ack comment

* more

* ops

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat(tables): added tables (#2867)

* updates

* required

* trashy table viewer

* updates

* updates

* filtering ui

* updates

* updates

* updates

* one input mode

* format

* fix lints

* improved errors

* updates

* updates

* chages

* doc strings

* breaking down file

* update comments with ai

* updates

* comments

* changes

* revert

* updates

* dedupe

* updates

* updates

* updates

* refactoring

* renames & refactors

* refactoring

* updates

* undo

* update db

* wand

* updates

* fix comments

* fixes

* simplify comments

* u[dates

* renames

* better comments

* validation

* updates

* updates

* updates

* fix sorting

* fix appearnce

* updating prompt to make it user sort

* rm

* updates

* rename

* comments

* clean comments

* simplicifcaiton

* updates

* updates

* refactor

* reduced type confusion

* undo

* rename

* undo changes

* undo

* simplify

* updates

* updates

* revert

* updates

* db updates

* type fix

* fix

* fix error handling

* updates

* docs

* docs

* updates

* rename

* dedupe

* revert

* uncook

* updates

* fix

* fix

* fix

* fix

* prepare merge

* readd migrations

* add back missed code

* migrate enrichment logic to general abstraction

* address bugbot concerns

* adhere to size limits for tables

* remove conflicting migration

* add back migrations

* fix tables auth

* fix permissive auth

* fix lint

* reran migrations

* migrate to use tanstack query for all server state

* update table-selector

* update names

* added tables to permission groups, updated subblock types

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: waleed <walif6@gmail.com>

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running (#3259)

* fix(snapshot): changed insert to upsert when concurrent identical child workflows are running

* fixed ci tests failing

* fix(workflows): disallow duplicate workflow names at the same folder level (#3260)

* feat(tools): added redis, upstash, algolia, and revenuecat (#3261)

* feat(tools): added redis, upstash, algolia, and revenuecat

* ack comment

* feat(models): add gemini-3.1-pro-preview and update gemini-3-pro thinking levels (#3263)

* fix(audit-log): lazily resolve actor name/email when missing (#3262)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params (#3264)

* fix(blocks): move type coercions from tools.config.tool to tools.config.params

Number() coercions in tools.config.tool ran at serialization time before
variable resolution, destroying dynamic references like <block.result.count>
by converting them to NaN/null. Moved all coercions to tools.config.params
which runs at execution time after variables are resolved.

Fixed in 15 blocks: exa, arxiv, sentry, incidentio, wikipedia, ahrefs,
posthog, elasticsearch, dropbox, hunter, lemlist, spotify, youtube, grafana,
parallel. Also added mode: 'advanced' to optional exa fields.

Closes #3258

* fix(blocks): address PR review — move remaining param mutations from tool() to params()

- Moved field mappings from tool() to params() in grafana, posthog,
  lemlist, spotify, dropbox (same dynamic reference bug)
- Fixed parallel.ts excerpts/full_content boolean logic
- Fixed parallel.ts search_queries empty case (must set undefined)
- Fixed elasticsearch.ts timeout not included when already ends with 's'
- Restored dropbox.ts tool() switch for proper default fallback

* fix(blocks): restore field renames to tool() for serialization-time validation

Field renames (e.g. personalApiKey→apiKey) must be in tool() because
validateRequiredFieldsBeforeExecution calls selectToolId()→tool() then
checks renamed field names on params. Only type coercions (Number(),
boolean) stay in params() to avoid destroying dynamic variable references.

* improvement(resolver): resovled empty sentinel to not pass through unexecuted valid refs to text inputs (#3266)

* fix(blocks): add required constraint for serviceDeskId in JSM block (#3268)

* fix(blocks): add required constraint for serviceDeskId in JSM block

* fix(blocks): rename custom field values to request field values in JSM create request

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* fix(tables): hide tables from sidebar and block registry (#3270)

* fix(tables): hide tables from sidebar and block registry

* fix(trigger): add isolated-vm support to trigger.dev container builds (#3269)

Scheduled workflow executions running in trigger.dev containers were
failing to spawn isolated-vm workers because the native module wasn't
available in the container. This caused loop condition evaluation to
silently fail and exit after one iteration.

- Add isolated-vm to build.external and additionalPackages in trigger config
- Include isolated-vm-worker.cjs via additionalFiles for child process spawning
- Add fallback path resolution for worker file in trigger.dev environment

* lint

* fix(trigger): update node version to align with main app (#3272)

* fix(build): fix corrupted sticky disk cache on blacksmith (#3273)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
2026-02-20 13:43:07 -08:00
Waleed
15ace5e63f v0.5.94: vercel integration, folder insertion, migrated tracking redirects to rewrites 2026-02-18 16:53:34 -08:00
Waleed
fdca73679d v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs 2026-02-18 12:10:05 -08:00
Waleed
da46a387c9 v0.5.92: shortlinks, copilot scrolling stickiness, pagination 2026-02-17 15:13:21 -08:00
Waleed
b7e377ec4b v0.5.91: docs i18n, turborepo upgrade 2026-02-16 00:36:05 -08:00
91 changed files with 20271 additions and 45 deletions

View File

@@ -939,6 +939,25 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function DevinIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 500 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M59.29,209.39l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.95,0,3.92-0.52,5.67-1.51l48.87-28.21c0,0,0.14-0.11,0.2-0.16c0.74-0.45,1.44-0.99,2.07-1.6c0.09-0.09,0.18-0.2,0.27-0.29c0.54-0.58,1.03-1.21,1.44-1.89c0.06-0.11,0.16-0.2,0.2-0.32c0.43-0.74,0.74-1.53,0.99-2.37c0.05-0.18,0.09-0.36,0.14-0.54c0.2-0.86,0.36-1.74,0.36-2.66v-28.21c0-10.89,5.87-21.03,15.3-26.48c9.42-5.45,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.37,0.11,0.54,0.16c0.83,0.2,1.69,0.32,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.05,0.26-0.05c0.79,0,1.58-0.11,2.34-0.32c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.64-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.23-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81V64.52c0-4.05-2.16-7.78-5.67-9.81l-48.91-28.19c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.27,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.31c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.42-14.1c-0.79-0.45-1.63-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.84-0.2-1.69-0.31-2.55-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.31c-0.14,0.02-0.25,0.05-0.38,0.09c-0.82,0.23-1.63,0.57-2.4,1c-0.06,0.05-0.16,0.05-0.23,0.09l-48.84,28.24c-3.51,2.03-5.67,5.76-5.67,9.81v56.42c0,4.05,2.16,7.78,5.67,9.81C59.29,209.41,59.29,209.39,59.29,209.39z'
fill='#2A6DCE'
/>
<path
d='M325.46,223.49c9.42-5.44,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.36,0.11,0.54,0.16c0.83,0.2,1.69,0.31,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.03,0.26-0.05c0.79,0,1.58-0.11,2.34-0.31c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.62-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.25-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.84-28.22c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.26,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.32c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.43-14.11c-0.79-0.45-1.62-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.83-0.2-1.69-0.32-2.54-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.32c-0.14,0.03-0.25,0.05-0.38,0.09c-0.83,0.23-1.64,0.57-2.41,0.99c-0.06,0.05-0.16,0.05-0.23,0.09l-48.87,28.21c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.05,0.23,0.09c0.77,0.43,1.58,0.77,2.41,0.99c0.14,0.05,0.27,0.05,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.27,0.05c0.05,0,0.09,0,0.11,0c0.86,0,1.69-0.14,2.54-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.56,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.31c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.26,0.29c0.61,0.6,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.72,1.51,5.67,1.51s3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.58-0.77-2.41-0.99c-0.14-0.05-0.25-0.05-0.38-0.09c-0.79-0.18-1.57-0.29-2.38-0.32c-0.11,0-0.25,0-0.36,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5c0-10.91,5.87-21.03,15.3-26.48C325.55,223.49,325.46,223.49,325.46,223.49z'
fill='#1DC19C'
/>
<path
d='M304.5,369.22l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.57-0.77-2.41-0.99c-0.14-0.05-0.27-0.05-0.4-0.09c-0.79-0.18-1.57-0.29-2.37-0.32c-0.14,0-0.25,0-0.38,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5v-28.22c0-0.92-0.14-1.8-0.36-2.66c-0.05-0.18-0.09-0.36-0.14-0.54c-0.25-0.83-0.57-1.62-0.99-2.37c-0.06-0.11-0.14-0.2-0.2-0.32c-0.4-0.68-0.9-1.31-1.44-1.89c-0.09-0.09-0.18-0.2-0.27-0.29c-0.6-0.6-1.31-1.12-2.07-1.6c-0.06-0.05-0.11-0.11-0.2-0.16l-48.87-28.21c-3.51-2.03-7.81-2.03-11.32,0L59.28,290.6c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.06,0.23,0.09c0.77,0.43,1.55,0.77,2.38,0.99c0.14,0.05,0.27,0.06,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.29,0.05c0.05,0,0.09,0,0.14,0c0.86,0,1.69-0.14,2.52-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.57,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.32c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.27,0.29c0.61,0.61,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.96,0,3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81L304.5,369.22z'
fill='#1796E2'
/>
</svg>
)
}
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -1302,6 +1321,21 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleTasksIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 527.1 500' xmlns='http://www.w3.org/2000/svg'>
<polygon
fill='#0066DA'
points='410.4,58.3 368.8,81.2 348.2,120.6 368.8,168.8 407.8,211 450,187.5 475.9,142.8 450,87.5'
/>
<path
fill='#2684FC'
d='M249.3,219.4l98.9-98.9c29.1,22.1,50.5,53.8,59.6,90.4L272.1,346.7c-12.2,12.2-32,12.2-44.2,0l-91.5-91.5 c-9.8-9.8-9.8-25.6,0-35.3l39-39c9.8-9.8,25.6-9.8,35.3,0L249.3,219.4z M519.8,63.6l-39.7-39.7c-9.7-9.7-25.6-9.7-35.3,0 l-34.4,34.4c27.5,23,49.9,51.8,65.5,84.5l43.9-43.9C529.6,89.2,529.6,73.3,519.8,63.6z M412.5,250c0,89.8-72.8,162.5-162.5,162.5 S87.5,339.8,87.5,250S160.2,87.5,250,87.5c36.9,0,70.9,12.3,98.2,33.1l62.2-62.2C367,21.9,311.1,0,250,0C111.9,0,0,111.9,0,250 s111.9,250,250,250s250-111.9,250-250c0-38.3-8.7-74.7-24.1-107.2L407.8,211C410.8,223.5,412.5,236.6,412.5,250z'
/>
</svg>
)
}
export function SupabaseIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const gradient0 = `supabase_paint0_${id}`
@@ -3430,6 +3464,23 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
)
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<path
d='M14.48 58.196L.558 34.082c-.744-1.288-.744-2.876 0-4.164L14.48 5.805c.743-1.287 2.115-2.08 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113c.744 1.288.744 2.876 0 4.164L49.52 58.196c-.743 1.287-2.115 2.08-3.6 2.082H18.07c-1.483-.005-2.85-.798-3.593-2.082z'
fill='#4386fa'
/>
<path
d='M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984c1.486-.002 2.858-.796 3.6-2.082L58.75 42.23z'
opacity='.1'
/>
<path
d='M45.267 43.23L41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616c.557.983 1.363 1.803 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z'
fill='#fff'
/>
</svg>
)
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
<path

View File

@@ -25,6 +25,7 @@ import {
ConfluenceIcon,
CursorIcon,
DatadogIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
DropboxIcon,
@@ -42,6 +43,7 @@ import {
GitLabIcon,
GmailIcon,
GongIcon,
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
GoogleDocsIcon,
@@ -52,6 +54,7 @@ import {
GoogleMapsIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleTasksIcon,
GoogleTranslateIcon,
GoogleVaultIcon,
GrafanaIcon,
@@ -172,6 +175,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
confluence_v2: ConfluenceIcon,
cursor_v2: CursorIcon,
datadog: DatadogIcon,
devin: DevinIcon,
discord: DiscordIcon,
dropbox: DropboxIcon,
dspy: DsPyIcon,
@@ -188,6 +192,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
gong: GongIcon,
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar_v2: GoogleCalendarIcon,
google_docs: GoogleDocsIcon,
@@ -198,6 +203,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_search: GoogleIcon,
google_sheets_v2: GoogleSheetsIcon,
google_slides_v2: GoogleSlidesIcon,
google_tasks: GoogleTasksIcon,
google_translate: GoogleTranslateIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,

View File

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

View File

@@ -0,0 +1,157 @@
---
title: Devin
description: Autonomous AI software engineer
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="devin"
color="#12141A"
/>
{/* MANUAL-CONTENT-START:intro */}
[Devin](https://devin.ai/) is an autonomous AI software engineer by Cognition that can independently write, run, debug, and deploy code.
With Devin, you can:
- **Automate coding tasks**: Assign software engineering tasks and let Devin autonomously write, test, and iterate on code
- **Manage sessions**: Create, monitor, and interact with Devin sessions to track progress on assigned tasks
- **Guide active work**: Send messages to running sessions to provide additional context, redirect efforts, or answer questions
- **Retrieve structured output**: Poll completed sessions for pull requests, structured results, and detailed status
- **Control costs**: Set ACU (Autonomous Compute Unit) limits to cap spending on long-running tasks
- **Standardize workflows**: Use playbook IDs to apply repeatable task patterns across sessions
In Sim, the Devin integration enables your agents to programmatically manage Devin sessions as part of their workflows:
- **Create sessions**: Kick off new Devin sessions with a prompt describing the task, optional playbook, ACU limits, and tags
- **Get session details**: Retrieve the full state of a session including status, pull requests, structured output, and resource consumption
- **List sessions**: Query all sessions in your organization with optional pagination
- **Send messages**: Communicate with active or suspended sessions to provide guidance, and automatically resume suspended sessions
This allows for powerful automation scenarios such as triggering code generation from upstream events, polling for completion before consuming results, orchestrating multi-step development pipelines, and integrating Devin's output into broader agent workflows.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code.
## Tools
### `devin_create_session`
Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
| `prompt` | string | Yes | The task prompt for Devin to work on |
| `playbookId` | string | No | Optional playbook ID to guide the session |
| `maxAcuLimit` | number | No | Maximum ACU limit for the session |
| `tags` | string | No | Comma-separated tags for the session |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessionId` | string | Unique identifier for the session |
| `url` | string | URL to view the session in the Devin UI |
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
| `title` | string | Session title |
| `createdAt` | number | Unix timestamp when the session was created |
| `updatedAt` | number | Unix timestamp when the session was last updated |
| `acusConsumed` | number | ACUs consumed by the session |
| `tags` | json | Tags associated with the session |
| `pullRequests` | json | Pull requests created during the session |
| `structuredOutput` | json | Structured output from the session |
| `playbookId` | string | Associated playbook ID |
### `devin_get_session`
Retrieve details of an existing Devin session including status, tags, pull requests, and structured output.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
| `sessionId` | string | Yes | The session ID to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessionId` | string | Unique identifier for the session |
| `url` | string | URL to view the session in the Devin UI |
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
| `title` | string | Session title |
| `createdAt` | number | Unix timestamp when the session was created |
| `updatedAt` | number | Unix timestamp when the session was last updated |
| `acusConsumed` | number | ACUs consumed by the session |
| `tags` | json | Tags associated with the session |
| `pullRequests` | json | Pull requests created during the session |
| `structuredOutput` | json | Structured output from the session |
| `playbookId` | string | Associated playbook ID |
### `devin_list_sessions`
List Devin sessions in the organization. Returns up to 100 sessions by default.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
| `limit` | number | No | Maximum number of sessions to return \(1-200, default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessions` | array | List of Devin sessions |
| ↳ `sessionId` | string | Unique identifier for the session |
| ↳ `url` | string | URL to view the session |
| ↳ `status` | string | Session status |
| ↳ `statusDetail` | string | Detailed status |
| ↳ `title` | string | Session title |
| ↳ `createdAt` | number | Creation timestamp \(Unix\) |
| ↳ `updatedAt` | number | Last updated timestamp \(Unix\) |
| ↳ `tags` | json | Session tags |
### `devin_send_message`
Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Devin API key \(service user credential starting with cog_\) |
| `sessionId` | string | Yes | The session ID to send the message to |
| `message` | string | Yes | The message to send to Devin |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sessionId` | string | Unique identifier for the session |
| `url` | string | URL to view the session in the Devin UI |
| `status` | string | Session status \(new, claimed, running, exit, error, suspended, resuming\) |
| `statusDetail` | string | Detailed status \(working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.\) |
| `title` | string | Session title |
| `createdAt` | number | Unix timestamp when the session was created |
| `updatedAt` | number | Unix timestamp when the session was last updated |
| `acusConsumed` | number | ACUs consumed by the session |
| `tags` | json | Tags associated with the session |
| `pullRequests` | json | Pull requests created during the session |
| `structuredOutput` | json | Structured output from the session |
| `playbookId` | string | Associated playbook ID |

View File

@@ -0,0 +1,168 @@
---
title: Google BigQuery
description: Query, list, and insert data in Google BigQuery
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_bigquery"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google BigQuery](https://cloud.google.com/bigquery) is Google Cloud's fully managed, serverless data warehouse designed for large-scale data analytics. BigQuery lets you run fast SQL queries on massive datasets, making it ideal for business intelligence, data exploration, and machine learning pipelines. It supports standard SQL, streaming inserts, and integrates with the broader Google Cloud ecosystem.
In Sim, the Google BigQuery integration allows your agents to query datasets, list tables, inspect schemas, and insert rows as part of automated workflows. This enables use cases such as automated reporting, data pipeline orchestration, real-time data ingestion, and analytics-driven decision making. By connecting Sim with BigQuery, your agents can pull insights from petabytes of data, write results back to tables, and keep your analytics workflows running without manual intervention.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows.
## Tools
### `google_bigquery_query`
Run a SQL query against Google BigQuery and return the results
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Google Cloud project ID |
| `query` | string | Yes | SQL query to execute |
| `useLegacySql` | boolean | No | Whether to use legacy SQL syntax \(default: false\) |
| `maxResults` | number | No | Maximum number of rows to return |
| `defaultDatasetId` | string | No | Default dataset for unqualified table names |
| `location` | string | No | Processing location \(e.g., "US", "EU"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `columns` | array | Array of column names from the query result |
| `rows` | array | Array of row objects keyed by column name |
| `totalRows` | string | Total number of rows in the complete result set |
| `jobComplete` | boolean | Whether the query completed within the timeout |
| `totalBytesProcessed` | string | Total bytes processed by the query |
| `cacheHit` | boolean | Whether the query result was served from cache |
| `jobReference` | object | Job reference \(useful when jobComplete is false\) |
| ↳ `projectId` | string | Project ID containing the job |
| ↳ `jobId` | string | Unique job identifier |
| ↳ `location` | string | Geographic location of the job |
| `pageToken` | string | Token for fetching additional result pages |
### `google_bigquery_list_datasets`
List all datasets in a Google BigQuery project
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Google Cloud project ID |
| `maxResults` | number | No | Maximum number of datasets to return |
| `pageToken` | string | No | Token for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `datasets` | array | Array of dataset objects |
| ↳ `datasetId` | string | Unique dataset identifier |
| ↳ `projectId` | string | Project ID containing this dataset |
| ↳ `friendlyName` | string | Descriptive name for the dataset |
| ↳ `location` | string | Geographic location where the data resides |
| `nextPageToken` | string | Token for fetching next page of results |
### `google_bigquery_list_tables`
List all tables in a Google BigQuery dataset
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Google Cloud project ID |
| `datasetId` | string | Yes | BigQuery dataset ID |
| `maxResults` | number | No | Maximum number of tables to return |
| `pageToken` | string | No | Token for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tables` | array | Array of table objects |
| ↳ `tableId` | string | Table identifier |
| ↳ `datasetId` | string | Dataset ID containing this table |
| ↳ `projectId` | string | Project ID containing this table |
| ↳ `type` | string | Table type \(TABLE, VIEW, EXTERNAL, etc.\) |
| ↳ `friendlyName` | string | User-friendly name for the table |
| ↳ `creationTime` | string | Time when created, in milliseconds since epoch |
| `totalItems` | number | Total number of tables in the dataset |
| `nextPageToken` | string | Token for fetching next page of results |
### `google_bigquery_get_table`
Get metadata and schema for a Google BigQuery table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Google Cloud project ID |
| `datasetId` | string | Yes | BigQuery dataset ID |
| `tableId` | string | Yes | BigQuery table ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tableId` | string | Table ID |
| `datasetId` | string | Dataset ID |
| `projectId` | string | Project ID |
| `type` | string | Table type \(TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL\) |
| `description` | string | Table description |
| `numRows` | string | Total number of rows |
| `numBytes` | string | Total size in bytes, excluding data in streaming buffer |
| `schema` | array | Array of column definitions |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Data type \(STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.\) |
| ↳ `mode` | string | Column mode \(NULLABLE, REQUIRED, or REPEATED\) |
| ↳ `description` | string | Column description |
| `creationTime` | string | Table creation time \(milliseconds since epoch\) |
| `lastModifiedTime` | string | Last modification time \(milliseconds since epoch\) |
| `location` | string | Geographic location where the table resides |
### `google_bigquery_insert_rows`
Insert rows into a Google BigQuery table using streaming insert
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Google Cloud project ID |
| `datasetId` | string | Yes | BigQuery dataset ID |
| `tableId` | string | Yes | BigQuery table ID |
| `rows` | string | Yes | JSON array of row objects to insert |
| `skipInvalidRows` | boolean | No | Whether to insert valid rows even if some are invalid |
| `ignoreUnknownValues` | boolean | No | Whether to ignore columns not in the table schema |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `insertedRows` | number | Number of rows successfully inserted |
| `errors` | array | Array of per-row insertion errors \(empty if all succeeded\) |
| ↳ `index` | number | Zero-based index of the row that failed |
| ↳ `errors` | array | Error details for this row |
| ↳ `reason` | string | Short error code summarizing the error |
| ↳ `location` | string | Where the error occurred |
| ↳ `message` | string | Human-readable error description |

View File

@@ -10,6 +10,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Books](https://books.google.com) is Google's comprehensive book discovery and metadata service, providing access to millions of books from publishers, libraries, and digitized collections worldwide. The Google Books API enables programmatic search and retrieval of detailed book information including titles, authors, descriptions, ratings, and publication details.
In Sim, the Google Books integration allows your agents to search for books and retrieve volume details as part of automated workflows. This enables use cases such as content research, reading list curation, bibliographic data enrichment, ISBN lookups, and knowledge gathering from published works. By connecting Sim with Google Books, your agents can discover and analyze book metadata, filter by availability or format, and incorporate literary references into their outputs—all without manual research.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Search for books using the Google Books API. Find volumes by title, author, ISBN, or keywords, and retrieve detailed information about specific books including descriptions, ratings, and publication details.

View File

@@ -0,0 +1,205 @@
---
title: Google Tasks
description: Manage Google Tasks
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_tasks"
color="#E0E0E0"
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Tasks](https://support.google.com/tasks) is Google's lightweight task management service, integrated into Gmail, Google Calendar, and the standalone Google Tasks app. It provides a simple way to create, organize, and track to-do items with support for due dates, subtasks, and task lists. As part of Google Workspace, Google Tasks keeps your action items synchronized across all your devices.
In Sim, the Google Tasks integration allows your agents to create, read, update, delete, and list tasks and task lists as part of automated workflows. This enables use cases such as automated task creation from incoming data, to-do list management based on workflow triggers, task status tracking, and deadline monitoring. By connecting Sim with Google Tasks, your agents can manage action items programmatically, keep teams organized, and ensure nothing falls through the cracks.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists.
## Tools
### `google_tasks_create`
Create a new task in a Google Tasks list
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
| `title` | string | Yes | Title of the task \(max 1024 characters\) |
| `notes` | string | No | Notes/description for the task \(max 8192 characters\) |
| `due` | string | No | Due date in RFC 3339 format \(e.g., 2025-06-03T00:00:00.000Z\) |
| `status` | string | No | Task status: "needsAction" or "completed" |
| `parent` | string | No | Parent task ID to create this task as a subtask. Omit for top-level tasks. |
| `previous` | string | No | Previous sibling task ID to position after. Omit to place first among siblings. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Task ID |
| `title` | string | Task title |
| `notes` | string | Task notes |
| `status` | string | Task status \(needsAction or completed\) |
| `due` | string | Due date |
| `updated` | string | Last modification time |
| `selfLink` | string | URL for the task |
| `webViewLink` | string | Link to task in Google Tasks UI |
| `parent` | string | Parent task ID |
| `position` | string | Position among sibling tasks |
| `completed` | string | Completion date |
| `deleted` | boolean | Whether the task is deleted |
### `google_tasks_list`
List all tasks in a Google Tasks list
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
| `maxResults` | number | No | Maximum number of tasks to return \(default 20, max 100\) |
| `pageToken` | string | No | Token for pagination |
| `showCompleted` | boolean | No | Whether to show completed tasks \(default true\) |
| `showDeleted` | boolean | No | Whether to show deleted tasks \(default false\) |
| `showHidden` | boolean | No | Whether to show hidden tasks \(default false\) |
| `dueMin` | string | No | Lower bound for due date filter \(RFC 3339 timestamp\) |
| `dueMax` | string | No | Upper bound for due date filter \(RFC 3339 timestamp\) |
| `completedMin` | string | No | Lower bound for task completion date \(RFC 3339 timestamp\) |
| `completedMax` | string | No | Upper bound for task completion date \(RFC 3339 timestamp\) |
| `updatedMin` | string | No | Lower bound for last modification time \(RFC 3339 timestamp\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tasks` | array | List of tasks |
| ↳ `id` | string | Task identifier |
| ↳ `title` | string | Title of the task |
| ↳ `notes` | string | Notes/description for the task |
| ↳ `status` | string | Task status: "needsAction" or "completed" |
| ↳ `due` | string | Due date \(RFC 3339 timestamp\) |
| ↳ `completed` | string | Completion date \(RFC 3339 timestamp\) |
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
| ↳ `selfLink` | string | URL pointing to this task |
| ↳ `webViewLink` | string | Link to task in Google Tasks UI |
| ↳ `parent` | string | Parent task identifier |
| ↳ `position` | string | Position among sibling tasks \(string-based ordering\) |
| ↳ `hidden` | boolean | Whether the task is hidden |
| ↳ `deleted` | boolean | Whether the task is deleted |
| ↳ `links` | array | Collection of links associated with the task |
| ↳ `type` | string | Link type \(e.g., "email", "generic", "chat_message"\) |
| ↳ `description` | string | Link description |
| ↳ `link` | string | The URL |
| `nextPageToken` | string | Token for retrieving the next page of results |
### `google_tasks_get`
Retrieve a specific task by ID from a Google Tasks list
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
| `taskId` | string | Yes | The ID of the task to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Task ID |
| `title` | string | Task title |
| `notes` | string | Task notes |
| `status` | string | Task status \(needsAction or completed\) |
| `due` | string | Due date |
| `updated` | string | Last modification time |
| `selfLink` | string | URL for the task |
| `webViewLink` | string | Link to task in Google Tasks UI |
| `parent` | string | Parent task ID |
| `position` | string | Position among sibling tasks |
| `completed` | string | Completion date |
| `deleted` | boolean | Whether the task is deleted |
### `google_tasks_update`
Update an existing task in a Google Tasks list
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
| `taskId` | string | Yes | The ID of the task to update |
| `title` | string | No | New title for the task |
| `notes` | string | No | New notes for the task |
| `due` | string | No | New due date in RFC 3339 format |
| `status` | string | No | New status: "needsAction" or "completed" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Task ID |
| `title` | string | Task title |
| `notes` | string | Task notes |
| `status` | string | Task status \(needsAction or completed\) |
| `due` | string | Due date |
| `updated` | string | Last modification time |
| `selfLink` | string | URL for the task |
| `webViewLink` | string | Link to task in Google Tasks UI |
| `parent` | string | Parent task ID |
| `position` | string | Position among sibling tasks |
| `completed` | string | Completion date |
| `deleted` | boolean | Whether the task is deleted |
### `google_tasks_delete`
Delete a task from a Google Tasks list
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskListId` | string | No | Task list ID \(defaults to primary task list "@default"\) |
| `taskId` | string | Yes | The ID of the task to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `taskId` | string | Deleted task ID |
| `deleted` | boolean | Whether deletion was successful |
### `google_tasks_list_task_lists`
Retrieve all task lists for the authenticated user
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `maxResults` | number | No | Maximum number of task lists to return \(default 20, max 100\) |
| `pageToken` | string | No | Token for pagination |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `taskLists` | array | List of task lists |
| ↳ `id` | string | Task list identifier |
| ↳ `title` | string | Title of the task list |
| ↳ `updated` | string | Last modification time \(RFC 3339 timestamp\) |
| ↳ `selfLink` | string | URL pointing to this task list |
| `nextPageToken` | string | Token for retrieving the next page of results |

View File

@@ -21,6 +21,7 @@
"confluence",
"cursor",
"datadog",
"devin",
"discord",
"dropbox",
"dspy",
@@ -37,6 +38,7 @@
"gitlab",
"gmail",
"gong",
"google_bigquery",
"google_books",
"google_calendar",
"google_docs",
@@ -47,6 +49,7 @@
"google_search",
"google_sheets",
"google_slides",
"google_tasks",
"google_translate",
"google_vault",
"grafana",

View File

@@ -5,11 +5,12 @@ description: User-defined data tables for storing and querying structured data
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
<BlockInfoCard
type="table"
color="#10B981"
/>
{/* MANUAL-CONTENT-START:intro */}
Tables allow you to create and manage custom data tables directly within Sim. Store, query, and manipulate structured data within your workflows without needing external database integrations.
**Why Use Tables?**
@@ -26,6 +27,7 @@ Tables allow you to create and manage custom data tables directly within Sim. St
- Batch operations for bulk inserts
- Bulk updates and deletes by filter
- Up to 10,000 rows per table, 100 tables per workspace
{/* MANUAL-CONTENT-END */}
## Creating Tables

View File

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

View File

@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluencePageDescendantsAPI')
export const dynamic = 'force-dynamic'
/**
* Get all descendants of a Confluence page recursively.
* Uses GET /wiki/api/v2/pages/{id}/descendants
*/
export async function POST(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!pageId) {
return NextResponse.json({ error: 'Page ID is required' }, { status: 400 })
}
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
if (!pageIdValidation.isValid) {
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/descendants?${queryParams.toString()}`
logger.info(`Fetching descendants for page ${pageId}`)
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to get page descendants (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const descendants = (data.results || []).map((page: any) => ({
id: page.id,
title: page.title,
type: page.type ?? null,
status: page.status ?? null,
spaceId: page.spaceId ?? null,
parentId: page.parentId ?? null,
childPosition: page.childPosition ?? null,
depth: page.depth ?? null,
}))
return NextResponse.json({
descendants,
pageId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error getting page descendants:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -1,7 +1,12 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validateNumericId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { cleanHtmlContent, getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluencePageVersionsAPI')
@@ -57,8 +62,14 @@ export async function POST(request: NextRequest) {
// If versionNumber is provided, get specific version with page content
if (versionNumber !== undefined && versionNumber !== null) {
const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}`
const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${versionNumber}&body-format=storage`
const versionValidation = validateNumericId(versionNumber, 'versionNumber', { min: 1 })
if (!versionValidation.isValid) {
return NextResponse.json({ error: versionValidation.error }, { status: 400 })
}
const safeVersion = versionValidation.sanitized
const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${safeVersion}`
const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${safeVersion}&body-format=storage`
logger.info(`Fetching version ${versionNumber} for page ${pageId}`)
@@ -135,6 +146,10 @@ export async function POST(request: NextRequest) {
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}

View File

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

View File

@@ -0,0 +1,114 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceSpacePermissionsAPI')
export const dynamic = 'force-dynamic'
/**
* List permissions for a Confluence space.
* Uses GET /wiki/api/v2/spaces/{id}/permissions
*/
export async function POST(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { domain, accessToken, spaceId, cloudId: providedCloudId, limit = 50, cursor } = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!spaceId) {
return NextResponse.json({ error: 'Space ID is required' }, { status: 400 })
}
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
if (!spaceIdValidation.isValid) {
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/permissions?${queryParams.toString()}`
logger.info(`Fetching permissions for space ${spaceId}`)
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to list space permissions (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const permissions = (data.results || []).map((perm: any) => ({
id: perm.id,
principalType: perm.principal?.type ?? null,
principalId: perm.principal?.id ?? null,
operationKey: perm.operation?.key ?? null,
operationTargetType: perm.operation?.targetType ?? null,
anonymousAccess: perm.anonymousAccess ?? false,
unlicensedAccess: perm.unlicensedAccess ?? false,
}))
return NextResponse.json({
permissions,
spaceId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing space permissions:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,209 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceSpacePropertiesAPI')
export const dynamic = 'force-dynamic'
/**
* List, create, or delete space properties.
* Uses GET/POST /wiki/api/v2/spaces/{id}/properties
* and DELETE /wiki/api/v2/spaces/{id}/properties/{propertyId}
*/
export async function POST(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
domain,
accessToken,
spaceId,
cloudId: providedCloudId,
action,
key,
value,
propertyId,
limit = 50,
cursor,
} = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!spaceId) {
return NextResponse.json({ error: 'Space ID is required' }, { status: 400 })
}
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
if (!spaceIdValidation.isValid) {
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/properties`
// Validate required params for specific actions
if (action === 'delete' && !propertyId) {
return NextResponse.json(
{ error: 'Property ID is required for delete action' },
{ status: 400 }
)
}
if (action === 'create' && !key) {
return NextResponse.json(
{ error: 'Property key is required for create action' },
{ status: 400 }
)
}
// Delete a property
if (action === 'delete' && propertyId) {
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
if (!propertyIdValidation.isValid) {
return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 })
}
const url = `${baseUrl}/${encodeURIComponent(propertyId)}`
logger.info(`Deleting space property ${propertyId} from space ${spaceId}`)
const response = await fetch(url, {
method: 'DELETE',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to delete space property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
return NextResponse.json({ spaceId, propertyId, deleted: true })
}
// Create a property
if (action === 'create' && key) {
logger.info(`Creating space property '${key}' on space ${spaceId}`)
const response = await fetch(baseUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ key, value: value ?? {} }),
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to create space property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
propertyId: data.id,
key: data.key,
value: data.value ?? null,
spaceId,
})
}
// List properties
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}
const url = `${baseUrl}?${queryParams.toString()}`
logger.info(`Fetching properties for space ${spaceId}`)
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to list space properties (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const properties = (data.results || []).map((prop: any) => ({
id: prop.id,
key: prop.key,
value: prop.value ?? null,
}))
return NextResponse.json({
properties,
spaceId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error with space properties:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

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

View File

@@ -0,0 +1,278 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
validatePathSegment,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceTasksAPI')
export const dynamic = 'force-dynamic'
/**
* List, get, or update Confluence inline tasks.
* Uses GET /wiki/api/v2/tasks, GET /wiki/api/v2/tasks/{id}, PUT /wiki/api/v2/tasks/{id}
*/
export async function POST(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const {
domain,
accessToken,
cloudId: providedCloudId,
action,
taskId,
status: taskStatus,
pageId,
spaceId,
assignedTo,
limit = 50,
cursor,
} = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
// Update a task
if (action === 'update' && taskId) {
const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255)
if (!taskIdValidation.isValid) {
return NextResponse.json({ error: taskIdValidation.error }, { status: 400 })
}
// First fetch the current task to get required fields
const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
const getResponse = await fetch(getUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!getResponse.ok) {
const errorData = await getResponse.json().catch(() => null)
const errorMessage = errorData?.message || `Failed to fetch task (${getResponse.status})`
return NextResponse.json({ error: errorMessage }, { status: getResponse.status })
}
const currentTask = await getResponse.json()
const updateBody: Record<string, unknown> = {
id: taskId,
status: taskStatus || currentTask.status,
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
logger.info(`Updating task ${taskId}`)
const response = await fetch(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(updateBody),
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage = errorData?.message || `Failed to update task (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
task: {
id: data.id,
localId: data.localId ?? null,
spaceId: data.spaceId ?? null,
pageId: data.pageId ?? null,
blogPostId: data.blogPostId ?? null,
status: data.status,
body: data.body?.storage?.value ?? null,
createdBy: data.createdBy ?? null,
assignedTo: data.assignedTo ?? null,
completedBy: data.completedBy ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
dueAt: data.dueAt ?? null,
completedAt: data.completedAt ?? null,
},
})
}
// Get a specific task
if (taskId) {
const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255)
if (!taskIdValidation.isValid) {
return NextResponse.json({ error: taskIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}`
logger.info(`Fetching task ${taskId}`)
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage = errorData?.message || `Failed to get task (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
task: {
id: data.id,
localId: data.localId ?? null,
spaceId: data.spaceId ?? null,
pageId: data.pageId ?? null,
blogPostId: data.blogPostId ?? null,
status: data.status,
body: data.body?.storage?.value ?? null,
createdBy: data.createdBy ?? null,
assignedTo: data.assignedTo ?? null,
completedBy: data.completedBy ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
dueAt: data.dueAt ?? null,
completedAt: data.completedAt ?? null,
},
})
}
// List tasks
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(limit, 250)))
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}
if (taskStatus) queryParams.append('status', taskStatus)
if (pageId) {
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
if (!pageIdValidation.isValid) {
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
}
queryParams.append('page-id', pageId)
}
if (spaceId) {
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
if (!spaceIdValidation.isValid) {
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
}
queryParams.append('space-id', spaceId)
}
if (assignedTo) {
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
const assignedToValidation = validatePathSegment(assignedTo, {
paramName: 'assignedTo',
maxLength: 128,
customPattern: /^[a-zA-Z0-9_|:-]+$/,
})
if (!assignedToValidation.isValid) {
return NextResponse.json({ error: assignedToValidation.error }, { status: 400 })
}
queryParams.append('assigned-to', assignedTo)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks?${queryParams.toString()}`
logger.info('Fetching tasks')
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage = errorData?.message || `Failed to list tasks (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const tasks = (data.results || []).map((task: any) => ({
id: task.id,
localId: task.localId ?? null,
spaceId: task.spaceId ?? null,
pageId: task.pageId ?? null,
blogPostId: task.blogPostId ?? null,
status: task.status,
body: task.body?.storage?.value ?? null,
createdBy: task.createdBy ?? null,
assignedTo: task.assignedTo ?? null,
completedBy: task.completedBy ?? null,
createdAt: task.createdAt ?? null,
updatedAt: task.updatedAt ?? null,
dueAt: task.dueAt ?? null,
completedAt: task.completedAt ?? null,
}))
return NextResponse.json({
tasks,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error with tasks:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,85 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validatePathSegment } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceUserAPI')
export const dynamic = 'force-dynamic'
/**
* Get a Confluence user by account ID.
* Uses GET /wiki/rest/api/user?accountId={accountId}
*/
export async function POST(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { domain, accessToken, accountId, cloudId: providedCloudId } = body
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
}
if (!accessToken) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!accountId) {
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
}
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
const accountIdValidation = validatePathSegment(accountId, {
paramName: 'accountId',
maxLength: 128,
customPattern: /^[a-zA-Z0-9_|:-]+$/,
})
if (!accountIdValidation.isValid) {
return NextResponse.json({ error: accountIdValidation.error }, { status: 400 })
}
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => null)
logger.error('Confluence API error response:', {
status: response.status,
statusText: response.statusText,
error: JSON.stringify(errorData, null, 2),
})
const errorMessage =
errorData?.message || `Failed to get Confluence user (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
logger.error('Error getting Confluence user:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -987,7 +987,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const onChildWorkflowInstanceReady = (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
iterationContext?: IterationContext,
executionOrder?: number
) => {
sendEvent({
type: 'block:childWorkflowStarted',
@@ -1001,6 +1002,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
iterationCurrent: iterationContext.iterationCurrent,
iterationContainerId: iterationContext.iterationContainerId,
}),
...(executionOrder !== undefined && { executionOrder }),
},
})
}

View File

@@ -40,10 +40,12 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files',
'https://www.googleapis.com/auth/drive': 'Access all Google Drive files',
'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
'https://www.googleapis.com/auth/tasks': 'Create, read, update, and delete Google Tasks',
'https://www.googleapis.com/auth/userinfo.email': 'View email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms',
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery',
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups',
@@ -81,6 +83,15 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'write:content.property:confluence': 'Create and manage content properties',
'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)',
'read:content.metadata:confluence': 'View content metadata (required for ancestors)',
'read:user:confluence': 'View Confluence user profiles',
'read:task:confluence': 'View Confluence inline tasks',
'write:task:confluence': 'Update Confluence inline tasks',
'delete:blogpost:confluence': 'Delete Confluence blog posts',
'write:space:confluence': 'Create and update Confluence spaces',
'delete:space:confluence': 'Delete Confluence spaces',
'read:space.property:confluence': 'View Confluence space properties',
'write:space.property:confluence': 'Create and manage space properties',
'read:space.permission:confluence': 'View Confluence space permissions',
'read:me': 'Read profile information',
'database.read': 'Read database',
'database.write': 'Write to database',

View File

@@ -379,7 +379,7 @@ export function CredentialSelector({
filterOptions={true}
isLoading={credentialsLoading}
overlayContent={overlayContent}
className={selectedId || isCredentialSetSelected ? 'pl-[28px]' : ''}
className={overlayContent ? 'pl-[28px]' : ''}
/>
{needsUpdate && (

View File

@@ -41,6 +41,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables'
import { useAutoLayout } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks'
@@ -203,6 +204,7 @@ export const Panel = memo(function Panel() {
)
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
const { isSnapshotView } = useCurrentWorkflow()
/**
* Mark hydration as complete on mount
@@ -421,7 +423,7 @@ export const Panel = memo(function Panel() {
<Layout className='h-3 w-3' animate={isAutoLayouting} variant='clockwise' />
<span>Auto layout</span>
</PopoverItem>
{userPermissions.canAdmin && !currentWorkflow?.isSnapshotView && (
{userPermissions.canAdmin && !isSnapshotView && (
<PopoverItem onClick={handleToggleWorkflowLock} disabled={!hasBlocks}>
{allBlocksLocked ? (
<Unlock className='h-3 w-3' />

View File

@@ -160,12 +160,16 @@ const IterationNodeRow = memo(function IterationNodeRow({
onSelectEntry,
isExpanded,
onToggle,
expandedNodes,
onToggleNode,
}: {
node: EntryNode
selectedEntryId: string | null
onSelectEntry: (entry: ConsoleEntry) => void
isExpanded: boolean
onToggle: () => void
expandedNodes: Set<string>
onToggleNode: (nodeId: string) => void
}) {
const { entry, children, iterationInfo } = node
const hasError = Boolean(entry.error) || children.some((c) => c.entry.error)
@@ -226,11 +230,13 @@ const IterationNodeRow = memo(function IterationNodeRow({
{isExpanded && hasChildren && (
<div className={ROW_STYLES.nested}>
{children.map((child) => (
<BlockRow
<EntryNodeRow
key={child.entry.id}
entry={child.entry}
isSelected={selectedEntryId === child.entry.id}
onSelect={onSelectEntry}
node={child}
selectedEntryId={selectedEntryId}
onSelectEntry={onSelectEntry}
expandedNodes={expandedNodes}
onToggleNode={onToggleNode}
/>
))}
</div>
@@ -346,6 +352,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
onSelectEntry={onSelectEntry}
isExpanded={expandedNodes.has(iterNode.entry.id)}
onToggle={() => onToggleNode(iterNode.entry.id)}
expandedNodes={expandedNodes}
onToggleNode={onToggleNode}
/>
))}
</div>
@@ -520,6 +528,8 @@ const EntryNodeRow = memo(function EntryNodeRow({
onSelectEntry={onSelectEntry}
isExpanded={expandedNodes.has(node.entry.id)}
onToggle={() => onToggleNode(node.entry.id)}
expandedNodes={expandedNodes}
onToggleNode={onToggleNode}
/>
)
}

View File

@@ -554,6 +554,7 @@ export function useWorkflowExecution() {
childWorkflowInstanceId: string
iterationCurrent?: number
iterationContainerId?: string
executionOrder?: number
}) => {
if (isStaleExecution()) return
updateConsole(
@@ -564,6 +565,7 @@ export function useWorkflowExecution() {
...(data.iterationContainerId !== undefined && {
iterationContainerId: data.iterationContainerId,
}),
...(data.executionOrder !== undefined && { executionOrder: data.executionOrder }),
},
executionIdRef.current
)

View File

@@ -2534,6 +2534,16 @@ const WorkflowContent = React.memo(() => {
window.removeEventListener('remove-from-subflow', handleRemoveFromSubflow as EventListener)
}, [blocks, edgesForDisplay, getNodeAbsolutePosition, collaborativeBatchUpdateParent])
useEffect(() => {
const handleToggleWorkflowLock = (e: CustomEvent<{ blockIds: string[] }>) => {
collaborativeBatchToggleLocked(e.detail.blockIds)
}
window.addEventListener('toggle-workflow-lock', handleToggleWorkflowLock as EventListener)
return () =>
window.removeEventListener('toggle-workflow-lock', handleToggleWorkflowLock as EventListener)
}, [collaborativeBatchToggleLocked])
/**
* Updates container dimensions in displayNodes during drag or keyboard movement.
*/

View File

@@ -281,6 +281,24 @@ interface ContextMenuProps {
* Set to true when user cannot leave (e.g., last admin)
*/
disableLeave?: boolean
/**
* Callback when lock/unlock is clicked
*/
onToggleLock?: () => void
/**
* Whether to show the lock option (default: false)
* Set to true for workflows that support locking
*/
showLock?: boolean
/**
* Whether the lock option is disabled (default: false)
* Set to true when user lacks permissions
*/
disableLock?: boolean
/**
* Whether the workflow is currently locked (all blocks locked)
*/
isLocked?: boolean
}
/**
@@ -321,6 +339,10 @@ export function ContextMenu({
onLeave,
showLeave = false,
disableLeave = false,
onToggleLock,
showLock = false,
disableLock = false,
isLocked = false,
}: ContextMenuProps) {
const [hexInput, setHexInput] = useState(currentColor || '#ffffff')
@@ -372,7 +394,8 @@ export function ContextMenu({
(showRename && onRename) ||
(showCreate && onCreate) ||
(showCreateFolder && onCreateFolder) ||
(showColorChange && onColorChange)
(showColorChange && onColorChange) ||
(showLock && onToggleLock)
const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport)
return (
@@ -495,6 +518,19 @@ export function ContextMenu({
</PopoverFolder>
)}
{showLock && onToggleLock && (
<PopoverItem
rootOnly
disabled={disableLock}
onClick={() => {
onToggleLock()
onClose()
}}
>
{isLocked ? 'Unlock' : 'Lock'}
</PopoverItem>
)}
{/* Copy and export actions */}
{hasEditSection && hasCopySection && <PopoverDivider rootOnly />}
{showDuplicate && onDuplicate && (

View File

@@ -6,6 +6,7 @@ import { MoreHorizontal } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
import { Avatars } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars'
@@ -27,6 +28,7 @@ import {
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
interface WorkflowItemProps {
workflow: WorkflowMetadata
@@ -169,6 +171,29 @@ export function WorkflowItem({
[workflow.id, updateWorkflow]
)
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
const isActiveWorkflow = workflow.id === activeWorkflowId
const isWorkflowLocked = useWorkflowStore(
useCallback(
(state) => {
if (!isActiveWorkflow) return false
const blockValues = Object.values(state.blocks)
if (blockValues.length === 0) return false
return blockValues.every((block) => block.locked)
},
[isActiveWorkflow]
)
)
const handleToggleLock = useCallback(() => {
if (!isActiveWorkflow) return
const blocks = useWorkflowStore.getState().blocks
const blockIds = getWorkflowLockToggleIds(blocks, !isWorkflowLocked)
if (blockIds.length === 0) return
window.dispatchEvent(new CustomEvent('toggle-workflow-lock', { detail: { blockIds } }))
}, [isActiveWorkflow, isWorkflowLocked])
const isEditingRef = useRef(false)
const {
@@ -461,6 +486,10 @@ export function WorkflowItem({
disableExport={!userPermissions.canEdit}
disableColorChange={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit || !canDeleteSelection}
onToggleLock={handleToggleLock}
showLock={isActiveWorkflow && !isMixedSelection && selectedWorkflows.size <= 1}
disableLock={!userPermissions.canAdmin}
isLocked={isWorkflowLocked}
/>
<DeleteModal

View File

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

View File

@@ -0,0 +1,187 @@
import { DevinIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const DevinBlock: BlockConfig = {
type: 'devin',
name: 'Devin',
description: 'Autonomous AI software engineer',
longDescription:
'Integrate Devin into your workflow. Create sessions to assign coding tasks, send messages to guide active sessions, and retrieve session status and results. Devin autonomously writes, runs, and tests code.',
bestPractices: `
- Write clear, specific prompts describing the task, expected outcome, and any constraints.
- Use playbook IDs to standardize recurring task patterns across sessions.
- Set ACU limits to control cost for long-running tasks.
- Use Get Session to poll for completion status before consuming structured output.
- Send Message auto-resumes suspended sessions — no need to resume separately.
`,
docsLink: 'https://docs.sim.ai/tools/devin',
category: 'tools',
bgColor: '#12141A',
icon: DevinIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create Session', id: 'create_session' },
{ label: 'Get Session', id: 'get_session' },
{ label: 'List Sessions', id: 'list_sessions' },
{ label: 'Send Message', id: 'send_message' },
],
value: () => 'create_session',
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Devin API key (cog_...)',
password: true,
required: true,
},
{
id: 'prompt',
title: 'Prompt',
type: 'long-input',
placeholder: 'Describe the task for Devin...',
required: { field: 'operation', value: 'create_session' },
condition: { field: 'operation', value: 'create_session' },
wandConfig: {
enabled: true,
prompt: `You are an expert at writing clear, actionable prompts for Devin, an autonomous AI software engineer. Generate or refine a task prompt based on the user's request.
Current prompt: {context}
RULES:
1. Be specific about the expected outcome and deliverables
2. Include relevant technical context (languages, frameworks, repos)
3. Specify any constraints (don't modify certain files, follow certain patterns)
4. Break complex tasks into clear steps when helpful
5. Return ONLY the prompt text, no markdown formatting or explanations`,
placeholder: 'Describe what you want Devin to do...',
},
},
{
id: 'playbookId',
title: 'Playbook ID',
type: 'short-input',
placeholder: 'Optional playbook ID to guide the session',
condition: { field: 'operation', value: 'create_session' },
mode: 'advanced',
},
{
id: 'maxAcuLimit',
title: 'Max ACU Limit',
type: 'short-input',
placeholder: 'Maximum ACU budget for this session',
condition: { field: 'operation', value: 'create_session' },
mode: 'advanced',
},
{
id: 'tags',
title: 'Tags',
type: 'short-input',
placeholder: 'Comma-separated tags',
condition: { field: 'operation', value: 'create_session' },
mode: 'advanced',
},
{
id: 'sessionId',
title: 'Session ID',
type: 'short-input',
placeholder: 'Enter session ID',
required: { field: 'operation', value: ['get_session', 'send_message'] },
condition: { field: 'operation', value: ['get_session', 'send_message'] },
},
{
id: 'message',
title: 'Message',
type: 'long-input',
placeholder: 'Enter message to send to Devin...',
required: { field: 'operation', value: 'send_message' },
condition: { field: 'operation', value: 'send_message' },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of sessions (1-200, default: 100)',
condition: { field: 'operation', value: 'list_sessions' },
mode: 'advanced',
},
],
tools: {
access: [
'devin_create_session',
'devin_get_session',
'devin_list_sessions',
'devin_send_message',
],
config: {
tool: (params) => `devin_${params.operation}`,
params: (params) => {
if (params.maxAcuLimit != null && params.maxAcuLimit !== '') {
params.maxAcuLimit = Number(params.maxAcuLimit)
}
if (params.limit != null && params.limit !== '') {
params.limit = Number(params.limit)
}
return params
},
},
},
inputs: {
prompt: { type: 'string', description: 'Task prompt for Devin' },
sessionId: { type: 'string', description: 'Session ID' },
message: { type: 'string', description: 'Message to send to the session' },
apiKey: { type: 'string', description: 'Devin API key' },
playbookId: { type: 'string', description: 'Playbook ID to guide the session' },
maxAcuLimit: { type: 'number', description: 'Maximum ACU limit' },
tags: { type: 'string', description: 'Comma-separated tags' },
limit: { type: 'number', description: 'Number of sessions to return' },
},
outputs: {
sessionId: { type: 'string', description: 'Session identifier' },
url: { type: 'string', description: 'URL to view the session in Devin UI' },
status: {
type: 'string',
description: 'Session status (new, claimed, running, exit, error, suspended, resuming)',
},
statusDetail: {
type: 'string',
description: 'Detailed status (working, waiting_for_user, finished, etc.)',
condition: { field: 'operation', value: 'list_sessions', not: true },
},
title: { type: 'string', description: 'Session title' },
createdAt: { type: 'number', description: 'Creation timestamp (Unix)' },
updatedAt: { type: 'number', description: 'Last updated timestamp (Unix)' },
acusConsumed: {
type: 'number',
description: 'ACUs consumed',
condition: { field: 'operation', value: 'list_sessions', not: true },
},
tags: { type: 'json', description: 'Session tags' },
pullRequests: {
type: 'json',
description: 'Pull requests created during the session',
condition: { field: 'operation', value: 'list_sessions', not: true },
},
structuredOutput: {
type: 'json',
description: 'Structured output from the session',
condition: { field: 'operation', value: 'list_sessions', not: true },
},
playbookId: {
type: 'string',
description: 'Associated playbook ID',
condition: { field: 'operation', value: 'list_sessions', not: true },
},
sessions: {
type: 'json',
description: 'List of sessions',
condition: { field: 'operation', value: 'list_sessions' },
},
},
}

View File

@@ -0,0 +1,256 @@
import { GoogleBigQueryIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const GoogleBigQueryBlock: BlockConfig = {
type: 'google_bigquery',
name: 'Google BigQuery',
description: 'Query, list, and insert data in Google BigQuery',
longDescription:
'Connect to Google BigQuery to run SQL queries, list datasets and tables, get table metadata, and insert rows.',
docsLink: 'https://docs.sim.ai/tools/google_bigquery',
category: 'tools',
bgColor: '#E0E0E0',
icon: GoogleBigQueryIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Run Query', id: 'query' },
{ label: 'List Datasets', id: 'list_datasets' },
{ label: 'List Tables', id: 'list_tables' },
{ label: 'Get Table', id: 'get_table' },
{ label: 'Insert Rows', id: 'insert_rows' },
],
value: () => 'query',
},
{
id: 'credential',
title: 'Google Account',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
required: true,
serviceId: 'google-bigquery',
requiredScopes: ['https://www.googleapis.com/auth/bigquery'],
placeholder: 'Select Google account',
},
{
id: 'manualCredential',
title: 'Google Account',
type: 'short-input',
canonicalParamId: 'oauthCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
},
{
id: 'projectId',
title: 'Project ID',
type: 'short-input',
placeholder: 'Enter Google Cloud project ID',
required: true,
},
{
id: 'query',
title: 'SQL Query',
type: 'long-input',
placeholder: 'SELECT * FROM `project.dataset.table` LIMIT 100',
condition: { field: 'operation', value: 'query' },
required: { field: 'operation', value: 'query' },
wandConfig: {
enabled: true,
prompt: `Generate a BigQuery Standard SQL query based on the user's description.
The query should:
- Use Standard SQL syntax (not Legacy SQL)
- Be well-formatted and efficient
- Include appropriate LIMIT clauses when applicable
Examples:
- "get all users" -> SELECT * FROM \`project.dataset.users\` LIMIT 1000
- "count orders by status" -> SELECT status, COUNT(*) as count FROM \`project.dataset.orders\` GROUP BY status
- "recent events" -> SELECT * FROM \`project.dataset.events\` ORDER BY created_at DESC LIMIT 100
Return ONLY the SQL query - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the query you want to run...',
},
},
{
id: 'useLegacySql',
title: 'Use Legacy SQL',
type: 'switch',
condition: { field: 'operation', value: 'query' },
},
{
id: 'maxResults',
title: 'Max Results',
type: 'short-input',
placeholder: 'Maximum rows to return',
condition: { field: 'operation', value: ['query', 'list_datasets', 'list_tables'] },
},
{
id: 'defaultDatasetId',
title: 'Default Dataset',
type: 'short-input',
placeholder: 'Default dataset for unqualified table names',
condition: { field: 'operation', value: 'query' },
},
{
id: 'location',
title: 'Location',
type: 'short-input',
placeholder: 'Processing location (e.g., US, EU)',
condition: { field: 'operation', value: 'query' },
},
{
id: 'datasetId',
title: 'Dataset ID',
type: 'short-input',
placeholder: 'Enter BigQuery dataset ID',
condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] },
},
{
id: 'tableId',
title: 'Table ID',
type: 'short-input',
placeholder: 'Enter BigQuery table ID',
condition: { field: 'operation', value: ['get_table', 'insert_rows'] },
required: { field: 'operation', value: ['get_table', 'insert_rows'] },
},
{
id: 'rows',
title: 'Rows',
type: 'long-input',
placeholder: '[{"column1": "value1", "column2": 42}]',
condition: { field: 'operation', value: 'insert_rows' },
required: { field: 'operation', value: 'insert_rows' },
wandConfig: {
enabled: true,
prompt: `Generate a JSON array of row objects for BigQuery insertion based on the user's description.
Each row should be a JSON object where keys are column names and values match the expected types.
Examples:
- "3 users" -> [{"name": "Alice", "email": "alice@example.com"}, {"name": "Bob", "email": "bob@example.com"}, {"name": "Charlie", "email": "charlie@example.com"}]
- "order record" -> [{"order_id": "ORD-001", "amount": 99.99, "status": "pending"}]
Return ONLY the JSON array - no explanations, no wrapping, no extra text.`,
placeholder: 'Describe the rows to insert...',
generationType: 'json-object',
},
},
{
id: 'skipInvalidRows',
title: 'Skip Invalid Rows',
type: 'switch',
condition: { field: 'operation', value: 'insert_rows' },
},
{
id: 'ignoreUnknownValues',
title: 'Ignore Unknown Values',
type: 'switch',
condition: { field: 'operation', value: 'insert_rows' },
},
{
id: 'pageToken',
title: 'Page Token',
type: 'short-input',
placeholder: 'Pagination token',
condition: { field: 'operation', value: ['list_datasets', 'list_tables'] },
},
],
tools: {
access: [
'google_bigquery_query',
'google_bigquery_list_datasets',
'google_bigquery_list_tables',
'google_bigquery_get_table',
'google_bigquery_insert_rows',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'query':
return 'google_bigquery_query'
case 'list_datasets':
return 'google_bigquery_list_datasets'
case 'list_tables':
return 'google_bigquery_list_tables'
case 'get_table':
return 'google_bigquery_get_table'
case 'insert_rows':
return 'google_bigquery_insert_rows'
default:
throw new Error(`Invalid Google BigQuery operation: ${params.operation}`)
}
},
params: (params) => {
const { oauthCredential, rows, maxResults, ...rest } = params
return {
...rest,
oauthCredential,
...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }),
...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }),
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
oauthCredential: { type: 'string', description: 'Google BigQuery OAuth credential' },
projectId: { type: 'string', description: 'Google Cloud project ID' },
query: { type: 'string', description: 'SQL query to execute' },
useLegacySql: { type: 'boolean', description: 'Whether to use legacy SQL syntax' },
maxResults: { type: 'number', description: 'Maximum number of results to return' },
defaultDatasetId: {
type: 'string',
description: 'Default dataset for unqualified table names',
},
location: { type: 'string', description: 'Processing location' },
datasetId: { type: 'string', description: 'BigQuery dataset ID' },
tableId: { type: 'string', description: 'BigQuery table ID' },
rows: { type: 'string', description: 'JSON array of row objects to insert' },
skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' },
ignoreUnknownValues: {
type: 'boolean',
description: 'Whether to ignore unknown column values',
},
pageToken: { type: 'string', description: 'Pagination token' },
},
outputs: {
columns: { type: 'json', description: 'Array of column names (query)' },
rows: { type: 'json', description: 'Array of row objects (query)' },
totalRows: { type: 'string', description: 'Total number of rows (query)' },
jobComplete: { type: 'boolean', description: 'Whether the query completed (query)' },
totalBytesProcessed: { type: 'string', description: 'Bytes processed (query)' },
cacheHit: { type: 'boolean', description: 'Whether result was cached (query)' },
jobReference: { type: 'json', description: 'Job reference for incomplete queries (query)' },
pageToken: { type: 'string', description: 'Token for additional result pages (query)' },
datasets: { type: 'json', description: 'Array of dataset objects (list_datasets)' },
tables: { type: 'json', description: 'Array of table objects (list_tables)' },
totalItems: { type: 'number', description: 'Total items count (list_tables)' },
tableId: { type: 'string', description: 'Table ID (get_table)' },
datasetId: { type: 'string', description: 'Dataset ID (get_table)' },
type: { type: 'string', description: 'Table type (get_table)' },
description: { type: 'string', description: 'Table description (get_table)' },
numRows: { type: 'string', description: 'Row count (get_table)' },
numBytes: { type: 'string', description: 'Size in bytes (get_table)' },
schema: { type: 'json', description: 'Column definitions (get_table)' },
creationTime: { type: 'string', description: 'Creation time (get_table)' },
lastModifiedTime: { type: 'string', description: 'Last modified time (get_table)' },
location: { type: 'string', description: 'Data location (get_table)' },
insertedRows: { type: 'number', description: 'Rows inserted (insert_rows)' },
errors: { type: 'json', description: 'Insert errors (insert_rows)' },
nextPageToken: { type: 'string', description: 'Token for next page of results' },
},
}

View File

@@ -0,0 +1,262 @@
import { GoogleTasksIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { GoogleTasksResponse } from '@/tools/google_tasks/types'
export const GoogleTasksBlock: BlockConfig<GoogleTasksResponse> = {
type: 'google_tasks',
name: 'Google Tasks',
description: 'Manage Google Tasks',
longDescription:
'Integrate Google Tasks into your workflow. Create, read, update, delete, and list tasks and task lists.',
docsLink: 'https://docs.sim.ai/tools/google_tasks',
category: 'tools',
bgColor: '#E0E0E0',
icon: GoogleTasksIcon,
authMode: AuthMode.OAuth,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create Task', id: 'create' },
{ label: 'List Tasks', id: 'list' },
{ label: 'Get Task', id: 'get' },
{ label: 'Update Task', id: 'update' },
{ label: 'Delete Task', id: 'delete' },
{ label: 'List Task Lists', id: 'list_task_lists' },
],
value: () => 'create',
},
{
id: 'credential',
title: 'Google Tasks Account',
type: 'oauth-input',
canonicalParamId: 'oauthCredential',
mode: 'basic',
required: true,
serviceId: 'google-tasks',
requiredScopes: ['https://www.googleapis.com/auth/tasks'],
placeholder: 'Select Google Tasks account',
},
{
id: 'manualCredential',
title: 'Google Tasks Account',
type: 'short-input',
canonicalParamId: 'oauthCredential',
mode: 'advanced',
placeholder: 'Enter credential ID',
required: true,
},
// Task List ID - shown for all task operations (not list_task_lists)
{
id: 'taskListId',
title: 'Task List ID',
type: 'short-input',
placeholder: 'Task list ID (leave empty for default list)',
condition: { field: 'operation', value: 'list_task_lists', not: true },
},
// Create Task Fields
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Buy groceries',
condition: { field: 'operation', value: 'create' },
required: { field: 'operation', value: 'create' },
},
{
id: 'notes',
title: 'Notes',
type: 'long-input',
placeholder: 'Task notes or description',
condition: { field: 'operation', value: 'create' },
},
{
id: 'due',
title: 'Due Date',
type: 'short-input',
placeholder: '2025-06-03T00:00:00.000Z',
condition: { field: 'operation', value: 'create' },
wandConfig: {
enabled: true,
prompt: `Generate an RFC 3339 timestamp in UTC based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS.000Z (UTC timezone).
Examples:
- "tomorrow" -> Calculate tomorrow's date at 00:00:00.000Z
- "next Friday" -> Calculate the next Friday's date at 00:00:00.000Z
- "June 15" -> 2025-06-15T00:00:00.000Z
Return ONLY the timestamp - no explanations, no extra text.`,
},
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
condition: { field: 'operation', value: 'create' },
options: [
{ label: 'Needs Action', id: 'needsAction' },
{ label: 'Completed', id: 'completed' },
],
},
// Get/Update/Delete Task Fields - Task ID
{
id: 'taskId',
title: 'Task ID',
type: 'short-input',
placeholder: 'Task ID',
condition: { field: 'operation', value: ['get', 'update', 'delete'] },
required: { field: 'operation', value: ['get', 'update', 'delete'] },
},
// Update Task Fields
{
id: 'title',
title: 'New Title',
type: 'short-input',
placeholder: 'Updated task title',
condition: { field: 'operation', value: 'update' },
},
{
id: 'notes',
title: 'New Notes',
type: 'long-input',
placeholder: 'Updated task notes',
condition: { field: 'operation', value: 'update' },
},
{
id: 'due',
title: 'New Due Date',
type: 'short-input',
placeholder: '2025-06-03T00:00:00.000Z',
condition: { field: 'operation', value: 'update' },
wandConfig: {
enabled: true,
prompt: `Generate an RFC 3339 timestamp in UTC based on the user's description.
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS.000Z (UTC timezone).
Examples:
- "tomorrow" -> Calculate tomorrow's date at 00:00:00.000Z
- "next Friday" -> Calculate the next Friday's date at 00:00:00.000Z
- "June 15" -> 2025-06-15T00:00:00.000Z
Return ONLY the timestamp - no explanations, no extra text.`,
},
},
{
id: 'status',
title: 'New Status',
type: 'dropdown',
condition: { field: 'operation', value: 'update' },
options: [
{ label: 'Needs Action', id: 'needsAction' },
{ label: 'Completed', id: 'completed' },
],
},
// List Tasks Fields
{
id: 'maxResults',
title: 'Max Results',
type: 'short-input',
placeholder: '20',
condition: { field: 'operation', value: ['list', 'list_task_lists'] },
},
{
id: 'showCompleted',
title: 'Show Completed',
type: 'dropdown',
condition: { field: 'operation', value: 'list' },
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
},
],
tools: {
access: [
'google_tasks_create',
'google_tasks_list',
'google_tasks_get',
'google_tasks_update',
'google_tasks_delete',
'google_tasks_list_task_lists',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'create':
return 'google_tasks_create'
case 'list':
return 'google_tasks_list'
case 'get':
return 'google_tasks_get'
case 'update':
return 'google_tasks_update'
case 'delete':
return 'google_tasks_delete'
case 'list_task_lists':
return 'google_tasks_list_task_lists'
default:
throw new Error(`Invalid Google Tasks operation: ${params.operation}`)
}
},
params: (params) => {
const { oauthCredential, operation, showCompleted, maxResults, ...rest } = params
const processedParams: Record<string, unknown> = { ...rest }
if (maxResults && typeof maxResults === 'string') {
processedParams.maxResults = Number.parseInt(maxResults, 10)
}
if (showCompleted !== undefined) {
processedParams.showCompleted = showCompleted === 'true'
}
return {
oauthCredential,
...processedParams,
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
oauthCredential: { type: 'string', description: 'Google Tasks access token' },
taskListId: { type: 'string', description: 'Task list identifier' },
title: { type: 'string', description: 'Task title' },
notes: { type: 'string', description: 'Task notes' },
due: { type: 'string', description: 'Task due date' },
status: { type: 'string', description: 'Task status' },
taskId: { type: 'string', description: 'Task identifier' },
maxResults: { type: 'string', description: 'Maximum number of results' },
showCompleted: { type: 'string', description: 'Whether to show completed tasks' },
},
outputs: {
id: { type: 'string', description: 'Task ID' },
title: { type: 'string', description: 'Task title' },
notes: { type: 'string', description: 'Task notes' },
status: { type: 'string', description: 'Task status' },
due: { type: 'string', description: 'Due date' },
updated: { type: 'string', description: 'Last modification time' },
selfLink: { type: 'string', description: 'URL for the task' },
webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI' },
parent: { type: 'string', description: 'Parent task ID' },
position: { type: 'string', description: 'Position among sibling tasks' },
completed: { type: 'string', description: 'Completion date' },
deleted: { type: 'boolean', description: 'Whether the task is deleted' },
tasks: { type: 'json', description: 'Array of tasks (list operation)' },
taskLists: { type: 'json', description: 'Array of task lists (list_task_lists operation)' },
taskId: { type: 'string', description: 'Deleted task ID (delete operation)' },
nextPageToken: { type: 'string', description: 'Token for next page of results' },
},
}

View File

@@ -135,7 +135,7 @@ const SUPPORTED_LANGUAGES = [
{ label: 'Yiddish', id: 'yi' },
{ label: 'Yoruba', id: 'yo' },
{ label: 'Zulu', id: 'zu' },
] as const
] satisfies { label: string; id: string }[]
export const GoogleTranslateBlock: BlockConfig = {
type: 'google_translate',

View File

@@ -23,6 +23,7 @@ import { ConditionBlock } from '@/blocks/blocks/condition'
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
import { DatadogBlock } from '@/blocks/blocks/datadog'
import { DevinBlock } from '@/blocks/blocks/devin'
import { DiscordBlock } from '@/blocks/blocks/discord'
import { DropboxBlock } from '@/blocks/blocks/dropbox'
import { DSPyBlock } from '@/blocks/blocks/dspy'
@@ -43,6 +44,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab'
import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail'
import { GongBlock } from '@/blocks/blocks/gong'
import { GoogleSearchBlock } from '@/blocks/blocks/google'
import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery'
import { GoogleBooksBlock } from '@/blocks/blocks/google_books'
import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar'
import { GoogleDocsBlock } from '@/blocks/blocks/google_docs'
@@ -52,6 +54,7 @@ import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups'
import { GoogleMapsBlock } from '@/blocks/blocks/google_maps'
import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets'
import { GoogleSlidesBlock, GoogleSlidesV2Block } from '@/blocks/blocks/google_slides'
import { GoogleTasksBlock } from '@/blocks/blocks/google_tasks'
import { GoogleTranslateBlock } from '@/blocks/blocks/google_translate'
import { GoogleVaultBlock } from '@/blocks/blocks/google_vault'
import { GrafanaBlock } from '@/blocks/blocks/grafana'
@@ -204,6 +207,7 @@ export const registry: Record<string, BlockConfig> = {
cursor: CursorBlock,
cursor_v2: CursorV2Block,
datadog: DatadogBlock,
devin: DevinBlock,
discord: DiscordBlock,
dropbox: DropboxBlock,
dspy: DSPyBlock,
@@ -235,6 +239,7 @@ export const registry: Record<string, BlockConfig> = {
google_forms: GoogleFormsBlock,
google_groups: GoogleGroupsBlock,
google_maps: GoogleMapsBlock,
google_tasks: GoogleTasksBlock,
google_translate: GoogleTranslateBlock,
gong: GongBlock,
google_search: GoogleSearchBlock,
@@ -242,6 +247,7 @@ export const registry: Record<string, BlockConfig> = {
google_sheets_v2: GoogleSheetsV2Block,
google_slides: GoogleSlidesBlock,
google_slides_v2: GoogleSlidesV2Block,
google_bigquery: GoogleBigQueryBlock,
google_vault: GoogleVaultBlock,
grafana: GrafanaBlock,
grain: GrainBlock,

View File

@@ -939,6 +939,25 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function DevinIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 500 500' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M59.29,209.39l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.95,0,3.92-0.52,5.67-1.51l48.87-28.21c0,0,0.14-0.11,0.2-0.16c0.74-0.45,1.44-0.99,2.07-1.6c0.09-0.09,0.18-0.2,0.27-0.29c0.54-0.58,1.03-1.21,1.44-1.89c0.06-0.11,0.16-0.2,0.2-0.32c0.43-0.74,0.74-1.53,0.99-2.37c0.05-0.18,0.09-0.36,0.14-0.54c0.2-0.86,0.36-1.74,0.36-2.66v-28.21c0-10.89,5.87-21.03,15.3-26.48c9.42-5.45,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.37,0.11,0.54,0.16c0.83,0.2,1.69,0.32,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.05,0.26-0.05c0.79,0,1.58-0.11,2.34-0.32c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.64-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.23-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81V64.52c0-4.05-2.16-7.78-5.67-9.81l-48.91-28.19c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.27,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.31c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.42-14.1c-0.79-0.45-1.63-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.84-0.2-1.69-0.31-2.55-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.31c-0.14,0.02-0.25,0.05-0.38,0.09c-0.82,0.23-1.63,0.57-2.4,1c-0.06,0.05-0.16,0.05-0.23,0.09l-48.84,28.24c-3.51,2.03-5.67,5.76-5.67,9.81v56.42c0,4.05,2.16,7.78,5.67,9.81C59.29,209.41,59.29,209.39,59.29,209.39z'
fill='#2A6DCE'
/>
<path
d='M325.46,223.49c9.42-5.44,21.15-5.44,30.59,0l24.43,14.11c0.79,0.45,1.62,0.77,2.47,1.01c0.18,0.05,0.36,0.11,0.54,0.16c0.83,0.2,1.69,0.31,2.54,0.34c0.05,0,0.09,0,0.11,0c0.09,0,0.18-0.03,0.26-0.05c0.79,0,1.58-0.11,2.34-0.31c0.14-0.03,0.27-0.05,0.4-0.09c0.83-0.23,1.62-0.57,2.41-0.99c0.06-0.05,0.16-0.05,0.25-0.09l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.84-28.22c-3.51-2.03-7.81-2.03-11.32,0l-48.87,28.21c0,0-0.14,0.11-0.2,0.16c-0.74,0.45-1.44,0.99-2.07,1.6c-0.09,0.09-0.18,0.2-0.26,0.29c-0.54,0.58-1.03,1.21-1.44,1.89c-0.06,0.11-0.16,0.2-0.2,0.32c-0.43,0.74-0.74,1.53-0.99,2.37c-0.05,0.18-0.09,0.36-0.14,0.54c-0.2,0.86-0.36,1.74-0.36,2.66v28.21c0,10.89-5.87,21.03-15.3,26.5c-9.42,5.44-21.15,5.44-30.59,0l-24.43-14.11c-0.79-0.45-1.62-0.77-2.47-1.01c-0.18-0.05-0.36-0.11-0.54-0.16c-0.83-0.2-1.69-0.32-2.54-0.34c-0.14,0-0.25,0-0.38,0c-0.81,0-1.6,0.11-2.37,0.32c-0.14,0.03-0.25,0.05-0.38,0.09c-0.83,0.23-1.64,0.57-2.41,0.99c-0.06,0.05-0.16,0.05-0.23,0.09l-48.87,28.21c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.05,0.23,0.09c0.77,0.43,1.58,0.77,2.41,0.99c0.14,0.05,0.27,0.05,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.27,0.05c0.05,0,0.09,0,0.11,0c0.86,0,1.69-0.14,2.54-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.56,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.31c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.26,0.29c0.61,0.6,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.72,1.51,5.67,1.51s3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.58-0.77-2.41-0.99c-0.14-0.05-0.25-0.05-0.38-0.09c-0.79-0.18-1.57-0.29-2.38-0.32c-0.11,0-0.25,0-0.36,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5c0-10.91,5.87-21.03,15.3-26.48C325.55,223.49,325.46,223.49,325.46,223.49z'
fill='#1DC19C'
/>
<path
d='M304.5,369.22l-48.87-28.21c0,0-0.16-0.05-0.23-0.09c-0.77-0.43-1.57-0.77-2.41-0.99c-0.14-0.05-0.27-0.05-0.4-0.09c-0.79-0.18-1.57-0.29-2.37-0.32c-0.14,0-0.25,0-0.38,0c-0.86,0-1.71,0.14-2.54,0.34c-0.18,0.05-0.34,0.09-0.52,0.16c-0.86,0.25-1.69,0.57-2.47,1.01l-24.43,14.11c-9.42,5.44-21.15,5.44-30.58,0c-9.42-5.44-15.3-15.59-15.3-26.5v-28.22c0-0.92-0.14-1.8-0.36-2.66c-0.05-0.18-0.09-0.36-0.14-0.54c-0.25-0.83-0.57-1.62-0.99-2.37c-0.06-0.11-0.14-0.2-0.2-0.32c-0.4-0.68-0.9-1.31-1.44-1.89c-0.09-0.09-0.18-0.2-0.27-0.29c-0.6-0.6-1.31-1.12-2.07-1.6c-0.06-0.05-0.11-0.11-0.2-0.16l-48.87-28.21c-3.51-2.03-7.81-2.03-11.32,0L59.28,290.6c-3.51,2.03-5.67,5.76-5.67,9.81v56.43c0,4.05,2.16,7.78,5.67,9.81l48.87,28.21c0,0,0.16,0.06,0.23,0.09c0.77,0.43,1.55,0.77,2.38,0.99c0.14,0.05,0.27,0.06,0.4,0.09c0.77,0.18,1.55,0.29,2.34,0.32c0.09,0,0.18,0.05,0.29,0.05c0.05,0,0.09,0,0.14,0c0.86,0,1.69-0.14,2.52-0.34c0.18-0.05,0.36-0.09,0.54-0.16c0.86-0.25,1.69-0.57,2.47-1.01l24.43-14.11c9.42-5.44,21.15-5.44,30.59,0c9.42,5.44,15.3,15.59,15.3,26.48v28.21c0,0.92,0.14,1.8,0.36,2.66c0.05,0.18,0.09,0.36,0.14,0.54c0.25,0.83,0.57,1.62,0.99,2.37c0.06,0.11,0.14,0.2,0.2,0.32c0.4,0.68,0.9,1.31,1.44,1.89c0.09,0.09,0.18,0.2,0.27,0.29c0.61,0.61,1.31,1.12,2.07,1.6c0.06,0.05,0.11,0.11,0.2,0.16l48.87,28.21c1.75,1.01,3.71,1.51,5.67,1.51c1.96,0,3.92-0.52,5.67-1.51l48.87-28.21c3.51-2.03,5.67-5.76,5.67-9.81v-56.43c0-4.05-2.16-7.78-5.67-9.81L304.5,369.22z'
fill='#1796E2'
/>
</svg>
)
}
export function DiscordIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -1302,6 +1321,21 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function GoogleTasksIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 527.1 500' xmlns='http://www.w3.org/2000/svg'>
<polygon
fill='#0066DA'
points='410.4,58.3 368.8,81.2 348.2,120.6 368.8,168.8 407.8,211 450,187.5 475.9,142.8 450,87.5'
/>
<path
fill='#2684FC'
d='M249.3,219.4l98.9-98.9c29.1,22.1,50.5,53.8,59.6,90.4L272.1,346.7c-12.2,12.2-32,12.2-44.2,0l-91.5-91.5 c-9.8-9.8-9.8-25.6,0-35.3l39-39c9.8-9.8,25.6-9.8,35.3,0L249.3,219.4z M519.8,63.6l-39.7-39.7c-9.7-9.7-25.6-9.7-35.3,0 l-34.4,34.4c27.5,23,49.9,51.8,65.5,84.5l43.9-43.9C529.6,89.2,529.6,73.3,519.8,63.6z M412.5,250c0,89.8-72.8,162.5-162.5,162.5 S87.5,339.8,87.5,250S160.2,87.5,250,87.5c36.9,0,70.9,12.3,98.2,33.1l62.2-62.2C367,21.9,311.1,0,250,0C111.9,0,0,111.9,0,250 s111.9,250,250,250s250-111.9,250-250c0-38.3-8.7-74.7-24.1-107.2L407.8,211C410.8,223.5,412.5,236.6,412.5,250z'
/>
</svg>
)
}
export function SupabaseIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const gradient0 = `supabase_paint0_${id}`
@@ -3430,6 +3464,23 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
</svg>
)
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
<path
d='M14.48 58.196L.558 34.082c-.744-1.288-.744-2.876 0-4.164L14.48 5.805c.743-1.287 2.115-2.08 3.6-2.082h27.857c1.48.007 2.845.8 3.585 2.082l13.92 24.113c.744 1.288.744 2.876 0 4.164L49.52 58.196c-.743 1.287-2.115 2.08-3.6 2.082H18.07c-1.483-.005-2.85-.798-3.593-2.082z'
fill='#4386fa'
/>
<path
d='M40.697 24.235s3.87 9.283-1.406 14.545-14.883 1.894-14.883 1.894L43.95 60.27h1.984c1.486-.002 2.858-.796 3.6-2.082L58.75 42.23z'
opacity='.1'
/>
<path
d='M45.267 43.23L41 38.953a.67.67 0 0 0-.158-.12 11.63 11.63 0 1 0-2.032 2.037.67.67 0 0 0 .113.15l4.277 4.277a.67.67 0 0 0 .947 0l1.12-1.12a.67.67 0 0 0 0-.947zM31.64 40.464a8.75 8.75 0 1 1 8.749-8.749 8.75 8.75 0 0 1-8.749 8.749zm-5.593-9.216v3.616c.557.983 1.363 1.803 2.338 2.375v-6.013zm4.375-2.998v9.772a6.45 6.45 0 0 0 2.338 0V28.25zm6.764 6.606v-2.142H34.85v4.5a6.43 6.43 0 0 0 2.338-2.368z'
fill='#fff'
/>
</svg>
)
export const GoogleVaultIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 82 82'>
<path

View File

@@ -80,7 +80,10 @@ export class BlockExecutor {
const startTime = performance.now()
let resolvedInputs: Record<string, any> = {}
const nodeMetadata = this.buildNodeMetadata(node)
const nodeMetadata = {
...this.buildNodeMetadata(node),
executionOrder: blockLog?.executionOrder,
}
let cleanupSelfReference: (() => void) | undefined
if (block.metadata?.id === BlockType.HUMAN_IN_THE_LOOP) {

View File

@@ -89,7 +89,8 @@ export interface ExecutionCallbacks {
onChildWorkflowInstanceReady?: (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
iterationContext?: IterationContext,
executionOrder?: number
) => void
}
@@ -155,7 +156,8 @@ export interface ContextExtensions {
onChildWorkflowInstanceReady?: (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
iterationContext?: IterationContext,
executionOrder?: number
) => void
/**

View File

@@ -62,6 +62,7 @@ export class WorkflowBlockHandler implements BlockHandler {
branchTotal?: number
originalBlockId?: string
isLoopNode?: boolean
executionOrder?: number
}
): Promise<BlockOutput | StreamingExecution> {
return this._executeCore(ctx, block, inputs, nodeMetadata)
@@ -79,6 +80,7 @@ export class WorkflowBlockHandler implements BlockHandler {
branchTotal?: number
originalBlockId?: string
isLoopNode?: boolean
executionOrder?: number
}
): Promise<BlockOutput | StreamingExecution> {
logger.info(`Executing workflow block: ${block.id}`)
@@ -169,7 +171,12 @@ export class WorkflowBlockHandler implements BlockHandler {
const iterationContext = nodeMetadata
? this.getIterationContext(ctx, nodeMetadata)
: undefined
ctx.onChildWorkflowInstanceReady?.(effectiveBlockId, instanceId, iterationContext)
ctx.onChildWorkflowInstanceReady?.(
effectiveBlockId,
instanceId,
iterationContext,
nodeMetadata?.executionOrder
)
}
const subExecutor = new Executor({

View File

@@ -264,7 +264,8 @@ export interface ExecutionContext {
onChildWorkflowInstanceReady?: (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
iterationContext?: IterationContext,
executionOrder?: number
) => void
/**
@@ -377,6 +378,7 @@ export interface BlockHandler {
branchTotal?: number
originalBlockId?: string
isLoopNode?: boolean
executionOrder?: number
}
) => Promise<BlockOutput | StreamingExecution>
}

View File

@@ -215,5 +215,115 @@ describe('start-block utilities', () => {
expect(output.customField).toBe('defaultValue')
})
it.concurrent('preserves coerced types for unified start payload', () => {
const block = createBlock('start_trigger', 'start', {
subBlocks: {
inputFormat: {
value: [
{ name: 'conversation_id', type: 'number' },
{ name: 'sender', type: 'object' },
{ name: 'is_active', type: 'boolean' },
],
},
},
})
const resolution = {
blockId: 'start',
block,
path: StartBlockPath.UNIFIED,
} as const
const output = buildStartBlockOutput({
resolution,
workflowInput: {
conversation_id: '149',
sender: '{"id":10,"email":"user@example.com"}',
is_active: 'true',
},
})
expect(output.conversation_id).toBe(149)
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
expect(output.is_active).toBe(true)
})
it.concurrent(
'prefers coerced inputFormat values over duplicated top-level workflowInput keys',
() => {
const block = createBlock('start_trigger', 'start', {
subBlocks: {
inputFormat: {
value: [
{ name: 'conversation_id', type: 'number' },
{ name: 'sender', type: 'object' },
{ name: 'is_active', type: 'boolean' },
],
},
},
})
const resolution = {
blockId: 'start',
block,
path: StartBlockPath.UNIFIED,
} as const
const output = buildStartBlockOutput({
resolution,
workflowInput: {
input: {
conversation_id: '149',
sender: '{"id":10,"email":"user@example.com"}',
is_active: 'false',
},
conversation_id: '150',
sender: '{"id":99,"email":"wrong@example.com"}',
is_active: 'true',
extra: 'keep-me',
},
})
expect(output.conversation_id).toBe(149)
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
expect(output.is_active).toBe(false)
expect(output.extra).toBe('keep-me')
}
)
})
describe('EXTERNAL_TRIGGER path', () => {
it.concurrent('preserves coerced types for integration trigger payload', () => {
const block = createBlock('webhook', 'start', {
subBlocks: {
inputFormat: {
value: [
{ name: 'count', type: 'number' },
{ name: 'payload', type: 'object' },
],
},
},
})
const resolution = {
blockId: 'start',
block,
path: StartBlockPath.EXTERNAL_TRIGGER,
} as const
const output = buildStartBlockOutput({
resolution,
workflowInput: {
count: '5',
payload: '{"event":"push"}',
extra: 'untouched',
},
})
expect(output.count).toBe(5)
expect(output.payload).toEqual({ event: 'push' })
expect(output.extra).toBe('untouched')
})
})
})

View File

@@ -262,6 +262,7 @@ function buildUnifiedStartOutput(
hasStructured: boolean
): NormalizedBlockOutput {
const output: NormalizedBlockOutput = {}
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
if (hasStructured) {
for (const [key, value] of Object.entries(structuredInput)) {
@@ -272,6 +273,9 @@ function buildUnifiedStartOutput(
if (isPlainObject(workflowInput)) {
for (const [key, value] of Object.entries(workflowInput)) {
if (key === 'onUploadError') continue
// Skip keys already set by schema-coerced structuredInput to
// prevent raw workflowInput strings from overwriting typed values.
if (structuredKeys?.has(key)) continue
// Runtime values override defaults (except undefined/null which mean "not provided")
if (value !== undefined && value !== null) {
output[key] = value
@@ -384,6 +388,7 @@ function buildIntegrationTriggerOutput(
hasStructured: boolean
): NormalizedBlockOutput {
const output: NormalizedBlockOutput = {}
const structuredKeys = hasStructured ? new Set(Object.keys(structuredInput)) : null
if (hasStructured) {
for (const [key, value] of Object.entries(structuredInput)) {
@@ -393,6 +398,7 @@ function buildIntegrationTriggerOutput(
if (isPlainObject(workflowInput)) {
for (const [key, value] of Object.entries(workflowInput)) {
if (structuredKeys?.has(key)) continue
if (value !== undefined && value !== null) {
output[key] = value
} else if (!Object.hasOwn(output, key)) {

View File

@@ -484,8 +484,10 @@ export const auth = betterAuth({
'google-docs',
'google-sheets',
'google-forms',
'google-bigquery',
'google-vault',
'google-groups',
'google-tasks',
'vertex-ai',
'github-repo',
'microsoft-dataverse',
@@ -1068,6 +1070,46 @@ export const auth = betterAuth({
}
},
},
{
providerId: 'google-bigquery',
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
accessType: 'offline',
scopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/bigquery',
],
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-bigquery`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
})
if (!response.ok) {
logger.error('Failed to fetch Google user info', { status: response.status })
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
}
const profile = await response.json()
const now = new Date()
return {
id: `${profile.sub}-${crypto.randomUUID()}`,
name: profile.name || 'Google User',
email: profile.email,
image: profile.picture || undefined,
emailVerified: profile.email_verified || false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Google getUserInfo', { error })
throw error
}
},
},
{
providerId: 'google-vault',
clientId: env.GOOGLE_CLIENT_ID as string,
@@ -1150,6 +1192,46 @@ export const auth = betterAuth({
},
},
{
providerId: 'google-tasks',
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
accessType: 'offline',
scopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/tasks',
],
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-tasks`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
})
if (!response.ok) {
logger.error('Failed to fetch Google user info', { status: response.status })
throw new Error(`Failed to fetch Google user info: ${response.statusText}`)
}
const profile = await response.json()
const now = new Date()
return {
id: `${profile.sub}-${crypto.randomUUID()}`,
name: profile.name || 'Google User',
email: profile.email,
image: profile.picture || undefined,
emailVerified: profile.email_verified || false,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Google getUserInfo', { error })
throw error
}
},
},
{
providerId: 'vertex-ai',
clientId: env.GOOGLE_CLIENT_ID as string,
@@ -1846,6 +1928,15 @@ export const auth = betterAuth({
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
'read:task:confluence',
'write:task:confluence',
'delete:blogpost:confluence',
'write:space:confluence',
'delete:space:confluence',
'read:space.property:confluence',
'write:space.property:confluence',
'read:space.permission:confluence',
],
responseType: 'code',
pkce: true,

View File

@@ -1039,3 +1039,74 @@ export function validateGoogleCalendarId(
return { isValid: true, sanitized: value }
}
/**
* Validates a pagination cursor token
*
* Pagination cursors are opaque tokens returned by APIs (e.g., Confluence, Jira)
* and passed back to get the next page. They are typically base64-encoded or
* URL-safe strings. This validator ensures the cursor cannot contain characters
* that could alter URL structure.
*
* @param value - The cursor token to validate
* @param paramName - Name of the parameter for error messages
* @param maxLength - Maximum length (default: 1024)
* @returns ValidationResult
*
* @example
* ```typescript
* if (cursor) {
* const result = validatePaginationCursor(cursor, 'cursor')
* if (!result.isValid) {
* return NextResponse.json({ error: result.error }, { status: 400 })
* }
* }
* ```
*/
export function validatePaginationCursor(
value: string | null | undefined,
paramName = 'cursor',
maxLength = 1024
): ValidationResult {
if (value === null || value === undefined || value === '') {
return {
isValid: false,
error: `${paramName} is required`,
}
}
if (value.length > maxLength) {
logger.warn('Pagination cursor exceeds maximum length', {
paramName,
length: value.length,
maxLength,
})
return {
isValid: false,
error: `${paramName} exceeds maximum length of ${maxLength} characters`,
}
}
if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) {
logger.warn('Pagination cursor contains control characters', { paramName })
return {
isValid: false,
error: `${paramName} contains invalid characters`,
}
}
// Allow alphanumeric, base64 chars (+, /, =), and URL-safe chars (-, _, ., ~, %)
const cursorPattern = /^[A-Za-z0-9+/=\-_.~%]+$/
if (!cursorPattern.test(value)) {
logger.warn('Pagination cursor contains disallowed characters', {
paramName,
value: value.substring(0, 100),
})
return {
isValid: false,
error: `${paramName} contains invalid characters`,
}
}
return { isValid: true, sanitized: value }
}

View File

@@ -8,6 +8,7 @@ import {
DropboxIcon,
GithubIcon,
GmailIcon,
GoogleBigQueryIcon,
GoogleCalendarIcon,
GoogleDocsIcon,
GoogleDriveIcon,
@@ -15,6 +16,7 @@ import {
GoogleGroupsIcon,
GoogleIcon,
GoogleSheetsIcon,
GoogleTasksIcon,
HubspotIcon,
JiraIcon,
LinearIcon,
@@ -119,6 +121,22 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
baseProviderIcon: GoogleIcon,
scopes: ['https://www.googleapis.com/auth/calendar'],
},
'google-bigquery': {
name: 'Google BigQuery',
description: 'Query, list, and insert data in Google BigQuery.',
providerId: 'google-bigquery',
icon: GoogleBigQueryIcon,
baseProviderIcon: GoogleIcon,
scopes: ['https://www.googleapis.com/auth/bigquery'],
},
'google-tasks': {
name: 'Google Tasks',
description: 'Create, manage, and organize tasks with Google Tasks.',
providerId: 'google-tasks',
icon: GoogleTasksIcon,
baseProviderIcon: GoogleIcon,
scopes: ['https://www.googleapis.com/auth/tasks'],
},
'google-vault': {
name: 'Google Vault',
description: 'Search, export, and manage matters/holds via Google Vault.',
@@ -330,6 +348,21 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'search:confluence',
'read:me',
'offline_access',
'read:blogpost:confluence',
'write:blogpost:confluence',
'delete:blogpost:confluence',
'read:content.property:confluence',
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
'read:user:confluence',
'read:task:confluence',
'write:task:confluence',
'write:space:confluence',
'delete:space:confluence',
'read:space.property:confluence',
'write:space.property:confluence',
'read:space.permission:confluence',
],
},
},

View File

@@ -7,6 +7,8 @@ export type OAuthProvider =
| 'google-docs'
| 'google-sheets'
| 'google-calendar'
| 'google-bigquery'
| 'google-tasks'
| 'google-vault'
| 'google-forms'
| 'google-groups'
@@ -52,6 +54,8 @@ export type OAuthService =
| 'google-docs'
| 'google-sheets'
| 'google-calendar'
| 'google-bigquery'
| 'google-tasks'
| 'google-vault'
| 'google-forms'
| 'google-groups'

View File

@@ -155,6 +155,7 @@ export interface BlockChildWorkflowStartedEvent extends BaseExecutionEvent {
childWorkflowInstanceId: string
iterationCurrent?: number
iterationContainerId?: string
executionOrder?: number
}
}
@@ -396,7 +397,8 @@ export function createSSECallbacks(options: SSECallbackOptions) {
const onChildWorkflowInstanceReady = (
blockId: string,
childWorkflowInstanceId: string,
iterationContext?: IterationContext
iterationContext?: IterationContext,
executionOrder?: number
) => {
sendEvent({
type: 'block:childWorkflowStarted',
@@ -410,6 +412,7 @@ export function createSSECallbacks(options: SSECallbackOptions) {
iterationCurrent: iterationContext.iterationCurrent,
iterationContainerId: iterationContext.iterationContainerId,
}),
...(executionOrder !== undefined && { executionOrder }),
},
})
}

View File

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

View File

@@ -91,6 +91,13 @@ const matchesEntryForUpdate = (
return false
}
if (
update.childWorkflowBlockId !== undefined &&
entry.childWorkflowBlockId !== update.childWorkflowBlockId
) {
return false
}
return true
}

View File

@@ -0,0 +1,134 @@
import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceCreateSpaceParams {
accessToken: string
domain: string
name: string
key: string
description?: string
cloudId?: string
}
export interface ConfluenceCreateSpaceResponse {
success: boolean
output: {
ts: string
spaceId: string
name: string
key: string
type: string
status: string
url: string
homepageId: string | null
description: { value: string; representation: string } | null
}
}
export const confluenceCreateSpaceTool: ToolConfig<
ConfluenceCreateSpaceParams,
ConfluenceCreateSpaceResponse
> = {
id: 'confluence_create_space',
name: 'Confluence Create Space',
description: 'Create a new Confluence space.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new space',
},
key: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Unique key for the space (uppercase, no spaces)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description for the new space',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space',
method: 'POST',
headers: (params: ConfluenceCreateSpaceParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceCreateSpaceParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
name: params.name,
key: params.key,
description: params.description,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.id ?? '',
name: data.name ?? '',
key: data.key ?? '',
type: data.type ?? '',
status: data.status ?? '',
url: data._links?.webui ?? '',
homepageId: data.homepageId ?? null,
description: data.description ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Created space ID' },
name: { type: 'string', description: 'Space name' },
key: { type: 'string', description: 'Space key' },
type: { type: 'string', description: 'Space type' },
status: { type: 'string', description: 'Space status' },
url: { type: 'string', description: 'URL to view the space' },
homepageId: { type: 'string', description: 'Homepage ID', optional: true },
description: {
type: 'object',
description: 'Space description',
properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
optional: true,
},
},
}

View File

@@ -0,0 +1,118 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceCreateSpacePropertyParams {
accessToken: string
domain: string
spaceId: string
key: string
value?: unknown
cloudId?: string
}
export interface ConfluenceCreateSpacePropertyResponse {
success: boolean
output: {
ts: string
propertyId: string
key: string
value: unknown
spaceId: string
}
}
export const confluenceCreateSpacePropertyTool: ToolConfig<
ConfluenceCreateSpacePropertyParams,
ConfluenceCreateSpacePropertyResponse
> = {
id: 'confluence_create_space_property',
name: 'Confluence Create Space Property',
description: 'Create a property on a Confluence space.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
spaceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Space ID to create the property on',
},
key: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Property key/name',
},
value: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Property value (JSON)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space-properties',
method: 'POST',
headers: (params: ConfluenceCreateSpacePropertyParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceCreateSpacePropertyParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
action: 'create',
key: params.key,
value: params.value,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
propertyId: data.propertyId ?? '',
key: data.key ?? '',
value: data.value ?? null,
spaceId: data.spaceId ?? '',
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
propertyId: { type: 'string', description: 'Created property ID' },
key: { type: 'string', description: 'Property key' },
value: { type: 'json', description: 'Property value' },
spaceId: { type: 'string', description: 'Space ID' },
},
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,147 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceGetPageDescendantsParams {
accessToken: string
domain: string
pageId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceGetPageDescendantsResponse {
success: boolean
output: {
ts: string
descendants: Array<{
id: string
title: string
type: string | null
status: string | null
spaceId: string | null
parentId: string | null
childPosition: number | null
depth: number | null
}>
pageId: string
nextCursor: string | null
}
}
export const confluenceGetPageDescendantsTool: ToolConfig<
ConfluenceGetPageDescendantsParams,
ConfluenceGetPageDescendantsResponse
> = {
id: 'confluence_get_page_descendants',
name: 'Confluence Get Page Descendants',
description: 'Get all descendants of a Confluence page recursively.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Page ID to get descendants for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of descendants to return (default: 50, max: 250)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from previous response',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/page-descendants',
method: 'POST',
headers: (params: ConfluenceGetPageDescendantsParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceGetPageDescendantsParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
pageId: params.pageId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
descendants: data.descendants || [],
pageId: data.pageId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
descendants: {
type: 'array',
description: 'Array of descendant pages',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Page ID' },
title: { type: 'string', description: 'Page title' },
type: {
type: 'string',
description: 'Content type (page, whiteboard, database, etc.)',
optional: true,
},
status: { type: 'string', description: 'Page status', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
parentId: { type: 'string', description: 'Parent page ID', optional: true },
childPosition: { type: 'number', description: 'Position among siblings', optional: true },
depth: { type: 'number', description: 'Depth in the hierarchy', optional: true },
},
},
},
pageId: { type: 'string', description: 'Parent page ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListSpacePermissionsParams {
accessToken: string
domain: string
spaceId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListSpacePermissionsResponse {
success: boolean
output: {
ts: string
permissions: Array<{
id: string
principalType: string | null
principalId: string | null
operationKey: string | null
operationTargetType: string | null
anonymousAccess: boolean
unlicensedAccess: boolean
}>
spaceId: string
nextCursor: string | null
}
}
export const confluenceListSpacePermissionsTool: ToolConfig<
ConfluenceListSpacePermissionsParams,
ConfluenceListSpacePermissionsResponse
> = {
id: 'confluence_list_space_permissions',
name: 'Confluence List Space Permissions',
description: 'List permissions for a Confluence space.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
spaceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Space ID to list permissions for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of permissions to return (default: 50, max: 250)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from previous response',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space-permissions',
method: 'POST',
headers: (params: ConfluenceListSpacePermissionsParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListSpacePermissionsParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
permissions: data.permissions || [],
spaceId: data.spaceId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
permissions: {
type: 'array',
description: 'Array of space permissions',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Permission ID' },
principalType: {
type: 'string',
description: 'Principal type (user, group, role)',
optional: true,
},
principalId: { type: 'string', description: 'Principal ID', optional: true },
operationKey: {
type: 'string',
description: 'Operation key (read, create, delete, etc.)',
optional: true,
},
operationTargetType: {
type: 'string',
description: 'Target type (page, blogpost, space, etc.)',
optional: true,
},
anonymousAccess: { type: 'boolean', description: 'Whether anonymous access is allowed' },
unlicensedAccess: {
type: 'boolean',
description: 'Whether unlicensed access is allowed',
},
},
},
},
spaceId: { type: 'string', description: 'Space ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,133 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListSpacePropertiesParams {
accessToken: string
domain: string
spaceId: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListSpacePropertiesResponse {
success: boolean
output: {
ts: string
properties: Array<{
id: string
key: string
value: unknown
}>
spaceId: string
nextCursor: string | null
}
}
export const confluenceListSpacePropertiesTool: ToolConfig<
ConfluenceListSpacePropertiesParams,
ConfluenceListSpacePropertiesResponse
> = {
id: 'confluence_list_space_properties',
name: 'Confluence List Space Properties',
description: 'List properties on a Confluence space.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
spaceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Space ID to list properties for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of properties to return (default: 50, max: 250)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from previous response',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space-properties',
method: 'POST',
headers: (params: ConfluenceListSpacePropertiesParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListSpacePropertiesParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
properties: data.properties || [],
spaceId: data.spaceId ?? '',
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
properties: {
type: 'array',
description: 'Array of space properties',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Property ID' },
key: { type: 'string', description: 'Property key' },
value: { type: 'json', description: 'Property value' },
},
},
},
spaceId: { type: 'string', description: 'Space ID' },
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,181 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceListTasksParams {
accessToken: string
domain: string
pageId?: string
spaceId?: string
assignedTo?: string
status?: string
limit?: number
cursor?: string
cloudId?: string
}
export interface ConfluenceListTasksResponse {
success: boolean
output: {
ts: string
tasks: Array<{
id: string
localId: string | null
spaceId: string | null
pageId: string | null
blogPostId: string | null
status: string
body: string | null
createdBy: string | null
assignedTo: string | null
completedBy: string | null
createdAt: string | null
updatedAt: string | null
dueAt: string | null
completedAt: string | null
}>
nextCursor: string | null
}
}
export const confluenceListTasksTool: ToolConfig<
ConfluenceListTasksParams,
ConfluenceListTasksResponse
> = {
id: 'confluence_list_tasks',
name: 'Confluence List Tasks',
description:
'List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
pageId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by page ID',
},
spaceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by space ID',
},
assignedTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by assignee account ID',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by status (complete or incomplete)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tasks to return (default: 50, max: 250)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor from previous response',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/tasks',
method: 'POST',
headers: (params: ConfluenceListTasksParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceListTasksParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
pageId: params.pageId,
spaceId: params.spaceId,
assignedTo: params.assignedTo,
status: params.status,
limit: params.limit,
cursor: params.cursor,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
tasks: data.tasks || [],
nextCursor: data.nextCursor ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
tasks: {
type: 'array',
description: 'Array of Confluence tasks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
localId: { type: 'string', description: 'Local task ID', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
pageId: { type: 'string', description: 'Page ID', optional: true },
blogPostId: { type: 'string', description: 'Blog post ID', optional: true },
status: { type: 'string', description: 'Task status (complete or incomplete)' },
body: {
type: 'string',
description: 'Task body content in storage format',
optional: true,
},
createdBy: { type: 'string', description: 'Creator account ID', optional: true },
assignedTo: { type: 'string', description: 'Assignee account ID', optional: true },
completedBy: { type: 'string', description: 'Completer account ID', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
dueAt: { type: 'string', description: 'Due date', optional: true },
completedAt: { type: 'string', description: 'Completion timestamp', optional: true },
},
},
},
nextCursor: {
type: 'string',
description: 'Cursor for fetching the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,123 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateBlogPostParams {
accessToken: string
domain: string
blogPostId: string
title?: string
content?: string
cloudId?: string
}
export interface ConfluenceUpdateBlogPostResponse {
success: boolean
output: {
ts: string
blogPostId: string
title: string
status: string | null
spaceId: string | null
version: Record<string, unknown> | null
url: string
}
}
export const confluenceUpdateBlogPostTool: ToolConfig<
ConfluenceUpdateBlogPostParams,
ConfluenceUpdateBlogPostResponse
> = {
id: 'confluence_update_blogpost',
name: 'Confluence Update Blog Post',
description: 'Update an existing Confluence blog post title and/or content.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
blogPostId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the blog post to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the blog post',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New content for the blog post in storage format',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/blogposts',
method: 'PUT',
headers: (params: ConfluenceUpdateBlogPostParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateBlogPostParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
blogPostId: params.blogPostId,
title: params.title,
content: params.content,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
blogPostId: data.id ?? '',
title: data.title ?? '',
status: data.status ?? null,
spaceId: data.spaceId ?? null,
version: data.version ?? null,
url: data._links?.webui ?? '',
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
blogPostId: { type: 'string', description: 'Updated blog post ID' },
title: { type: 'string', description: 'Blog post title' },
status: { type: 'string', description: 'Blog post status', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
version: { type: 'json', description: 'Version information', optional: true },
url: { type: 'string', description: 'URL to view the blog post' },
},
}

View File

@@ -0,0 +1,131 @@
import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateSpaceParams {
accessToken: string
domain: string
spaceId: string
name?: string
description?: string
cloudId?: string
}
export interface ConfluenceUpdateSpaceResponse {
success: boolean
output: {
ts: string
spaceId: string
name: string
key: string
type: string
status: string
url: string
description: { value: string; representation: string } | null
}
}
export const confluenceUpdateSpaceTool: ToolConfig<
ConfluenceUpdateSpaceParams,
ConfluenceUpdateSpaceResponse
> = {
id: 'confluence_update_space',
name: 'Confluence Update Space',
description: 'Update a Confluence space name or description.',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
spaceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the space to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New name for the space',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New description for the space',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/space',
method: 'PUT',
headers: (params: ConfluenceUpdateSpaceParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateSpaceParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
spaceId: params.spaceId,
name: params.name,
description: params.description,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
ts: new Date().toISOString(),
spaceId: data.id ?? '',
name: data.name ?? '',
key: data.key ?? '',
type: data.type ?? '',
status: data.status ?? '',
url: data._links?.webui ?? '',
description: data.description ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
spaceId: { type: 'string', description: 'Updated space ID' },
name: { type: 'string', description: 'Space name' },
key: { type: 'string', description: 'Space key' },
type: { type: 'string', description: 'Space type' },
status: { type: 'string', description: 'Space status' },
url: { type: 'string', description: 'URL to view the space' },
description: {
type: 'object',
description: 'Space description',
properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES,
optional: true,
},
},
}

View File

@@ -0,0 +1,141 @@
import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types'
import type { ToolConfig } from '@/tools/types'
export interface ConfluenceUpdateTaskParams {
accessToken: string
domain: string
taskId: string
status: string
cloudId?: string
}
export interface ConfluenceUpdateTaskResponse {
success: boolean
output: {
ts: string
id: string
localId: string | null
spaceId: string | null
pageId: string | null
blogPostId: string | null
status: string
body: string | null
createdBy: string | null
assignedTo: string | null
completedBy: string | null
createdAt: string | null
updatedAt: string | null
dueAt: string | null
completedAt: string | null
}
}
export const confluenceUpdateTaskTool: ToolConfig<
ConfluenceUpdateTaskParams,
ConfluenceUpdateTaskResponse
> = {
id: 'confluence_update_task',
name: 'Confluence Update Task',
description: 'Update the status of a Confluence inline task (complete or incomplete).',
version: '1.0.0',
oauth: {
required: true,
provider: 'confluence',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Confluence',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to update',
},
status: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New status for the task (complete or incomplete)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: () => '/api/tools/confluence/tasks',
method: 'POST',
headers: (params: ConfluenceUpdateTaskParams) => ({
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
body: (params: ConfluenceUpdateTaskParams) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
action: 'update',
taskId: params.taskId,
status: params.status,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const task = data.task || data
return {
success: true,
output: {
ts: new Date().toISOString(),
id: task.id ?? '',
localId: task.localId ?? null,
spaceId: task.spaceId ?? null,
pageId: task.pageId ?? null,
blogPostId: task.blogPostId ?? null,
status: task.status ?? '',
body: task.body ?? null,
createdBy: task.createdBy ?? null,
assignedTo: task.assignedTo ?? null,
completedBy: task.completedBy ?? null,
createdAt: task.createdAt ?? null,
updatedAt: task.updatedAt ?? null,
dueAt: task.dueAt ?? null,
completedAt: task.completedAt ?? null,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
id: { type: 'string', description: 'Task ID' },
localId: { type: 'string', description: 'Local task ID', optional: true },
spaceId: { type: 'string', description: 'Space ID', optional: true },
pageId: { type: 'string', description: 'Page ID', optional: true },
blogPostId: { type: 'string', description: 'Blog post ID', optional: true },
status: { type: 'string', description: 'Updated task status' },
body: { type: 'string', description: 'Task body content in storage format', optional: true },
createdBy: { type: 'string', description: 'Creator account ID', optional: true },
assignedTo: { type: 'string', description: 'Assignee account ID', optional: true },
completedBy: { type: 'string', description: 'Completer account ID', optional: true },
createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
dueAt: { type: 'string', description: 'Due date', optional: true },
completedAt: { type: 'string', description: 'Completion timestamp', optional: true },
},
}

View File

@@ -0,0 +1,105 @@
import type { ToolConfig } from '@/tools/types'
import type { DevinCreateSessionParams, DevinCreateSessionResponse } from './types'
import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types'
export const devinCreateSessionTool: ToolConfig<
DevinCreateSessionParams,
DevinCreateSessionResponse
> = {
id: 'devin_create_session',
name: 'create_session',
description:
'Create a new Devin session with a prompt. Devin will autonomously work on the task described in the prompt.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Devin API key (service user credential starting with cog_)',
},
prompt: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The task prompt for Devin to work on',
},
playbookId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional playbook ID to guide the session',
},
maxAcuLimit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum ACU limit for the session',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated tags for the session',
},
},
request: {
url: 'https://api.devin.ai/v3/organizations/sessions',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
prompt: params.prompt,
}
if (params.playbookId) body.playbook_id = params.playbookId
if (params.maxAcuLimit != null) {
body.max_acu_limit = params.maxAcuLimit
}
if (params.tags) {
body.tags = params.tags.split(',').map((t: string) => t.trim())
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
sessionId: data.session_id ?? null,
url: data.url ?? null,
status: data.status ?? null,
statusDetail: data.status_detail ?? null,
title: data.title ?? null,
createdAt: data.created_at ?? null,
updatedAt: data.updated_at ?? null,
acusConsumed: data.acus_consumed ?? null,
tags: data.tags ?? null,
pullRequests: data.pull_requests ?? null,
structuredOutput: data.structured_output ?? null,
playbookId: data.playbook_id ?? null,
},
}
},
outputs: {
sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId,
url: DEVIN_SESSION_OUTPUT_PROPERTIES.url,
status: DEVIN_SESSION_OUTPUT_PROPERTIES.status,
statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail,
title: DEVIN_SESSION_OUTPUT_PROPERTIES.title,
createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt,
updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt,
acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed,
tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags,
pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests,
structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput,
playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId,
},
}

View File

@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { DevinGetSessionParams, DevinGetSessionResponse } from './types'
import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types'
export const devinGetSessionTool: ToolConfig<DevinGetSessionParams, DevinGetSessionResponse> = {
id: 'devin_get_session',
name: 'get_session',
description:
'Retrieve details of an existing Devin session including status, tags, pull requests, and structured output.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Devin API key (service user credential starting with cog_)',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The session ID to retrieve',
},
},
request: {
url: (params) => `https://api.devin.ai/v3/organizations/sessions/${params.sessionId}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
sessionId: data.session_id ?? null,
url: data.url ?? null,
status: data.status ?? null,
statusDetail: data.status_detail ?? null,
title: data.title ?? null,
createdAt: data.created_at ?? null,
updatedAt: data.updated_at ?? null,
acusConsumed: data.acus_consumed ?? null,
tags: data.tags ?? null,
pullRequests: data.pull_requests ?? null,
structuredOutput: data.structured_output ?? null,
playbookId: data.playbook_id ?? null,
},
}
},
outputs: {
sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId,
url: DEVIN_SESSION_OUTPUT_PROPERTIES.url,
status: DEVIN_SESSION_OUTPUT_PROPERTIES.status,
statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail,
title: DEVIN_SESSION_OUTPUT_PROPERTIES.title,
createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt,
updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt,
acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed,
tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags,
pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests,
structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput,
playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId,
},
}

View File

@@ -0,0 +1,4 @@
export { devinCreateSessionTool } from './create_session'
export { devinGetSessionTool } from './get_session'
export { devinListSessionsTool } from './list_sessions'
export { devinSendMessageTool } from './send_message'

View File

@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { DevinListSessionsParams, DevinListSessionsResponse } from './types'
import { DEVIN_SESSION_LIST_ITEM_PROPERTIES } from './types'
export const devinListSessionsTool: ToolConfig<DevinListSessionsParams, DevinListSessionsResponse> =
{
id: 'devin_list_sessions',
name: 'list_sessions',
description: 'List Devin sessions in the organization. Returns up to 100 sessions by default.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Devin API key (service user credential starting with cog_)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of sessions to return (1-200, default: 100)',
},
},
request: {
url: (params) => {
const searchParams = new URLSearchParams()
if (params.limit) searchParams.set('first', String(params.limit))
const qs = searchParams.toString()
return `https://api.devin.ai/v3/organizations/sessions${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const items = data.items ?? []
return {
success: true,
output: {
sessions: items.map((item: Record<string, unknown>) => ({
sessionId: item.session_id ?? null,
url: item.url ?? null,
status: item.status ?? null,
statusDetail: item.status_detail ?? null,
title: item.title ?? null,
createdAt: item.created_at ?? null,
updatedAt: item.updated_at ?? null,
tags: item.tags ?? null,
})),
},
}
},
outputs: {
sessions: {
type: 'array',
description: 'List of Devin sessions',
items: {
type: 'object',
properties: DEVIN_SESSION_LIST_ITEM_PROPERTIES,
},
},
},
}

View File

@@ -0,0 +1,80 @@
import type { ToolConfig } from '@/tools/types'
import type { DevinSendMessageParams, DevinSendMessageResponse } from './types'
import { DEVIN_SESSION_OUTPUT_PROPERTIES } from './types'
export const devinSendMessageTool: ToolConfig<DevinSendMessageParams, DevinSendMessageResponse> = {
id: 'devin_send_message',
name: 'send_message',
description:
'Send a message to a Devin session. If the session is suspended, it will be automatically resumed. Returns the updated session state.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Devin API key (service user credential starting with cog_)',
},
sessionId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The session ID to send the message to',
},
message: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The message to send to Devin',
},
},
request: {
url: (params) => `https://api.devin.ai/v3/organizations/sessions/${params.sessionId}/messages`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
message: params.message,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
sessionId: data.session_id ?? null,
url: data.url ?? null,
status: data.status ?? null,
statusDetail: data.status_detail ?? null,
title: data.title ?? null,
createdAt: data.created_at ?? null,
updatedAt: data.updated_at ?? null,
acusConsumed: data.acus_consumed ?? null,
tags: data.tags ?? null,
pullRequests: data.pull_requests ?? null,
structuredOutput: data.structured_output ?? null,
playbookId: data.playbook_id ?? null,
},
}
},
outputs: {
sessionId: DEVIN_SESSION_OUTPUT_PROPERTIES.sessionId,
url: DEVIN_SESSION_OUTPUT_PROPERTIES.url,
status: DEVIN_SESSION_OUTPUT_PROPERTIES.status,
statusDetail: DEVIN_SESSION_OUTPUT_PROPERTIES.statusDetail,
title: DEVIN_SESSION_OUTPUT_PROPERTIES.title,
createdAt: DEVIN_SESSION_OUTPUT_PROPERTIES.createdAt,
updatedAt: DEVIN_SESSION_OUTPUT_PROPERTIES.updatedAt,
acusConsumed: DEVIN_SESSION_OUTPUT_PROPERTIES.acusConsumed,
tags: DEVIN_SESSION_OUTPUT_PROPERTIES.tags,
pullRequests: DEVIN_SESSION_OUTPUT_PROPERTIES.pullRequests,
structuredOutput: DEVIN_SESSION_OUTPUT_PROPERTIES.structuredOutput,
playbookId: DEVIN_SESSION_OUTPUT_PROPERTIES.playbookId,
},
}

View File

@@ -0,0 +1,168 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
export interface DevinCreateSessionParams {
apiKey: string
prompt: string
playbookId?: string
maxAcuLimit?: number
tags?: string
}
export interface DevinGetSessionParams {
apiKey: string
sessionId: string
}
export interface DevinListSessionsParams {
apiKey: string
limit?: number
}
export interface DevinSendMessageParams {
apiKey: string
sessionId: string
message: string
}
export const DEVIN_SESSION_OUTPUT_PROPERTIES = {
sessionId: {
type: 'string',
description: 'Unique identifier for the session',
},
url: {
type: 'string',
description: 'URL to view the session in the Devin UI',
},
status: {
type: 'string',
description: 'Session status (new, claimed, running, exit, error, suspended, resuming)',
},
statusDetail: {
type: 'string',
description:
'Detailed status (working, waiting_for_user, waiting_for_approval, finished, inactivity, etc.)',
optional: true,
},
title: {
type: 'string',
description: 'Session title',
optional: true,
},
createdAt: {
type: 'number',
description: 'Unix timestamp when the session was created',
optional: true,
},
updatedAt: {
type: 'number',
description: 'Unix timestamp when the session was last updated',
optional: true,
},
acusConsumed: {
type: 'number',
description: 'ACUs consumed by the session',
optional: true,
},
tags: {
type: 'json',
description: 'Tags associated with the session',
optional: true,
},
pullRequests: {
type: 'json',
description: 'Pull requests created during the session',
optional: true,
},
structuredOutput: {
type: 'json',
description: 'Structured output from the session',
optional: true,
},
playbookId: {
type: 'string',
description: 'Associated playbook ID',
optional: true,
},
} as const satisfies Record<string, OutputProperty>
export const DEVIN_SESSION_LIST_ITEM_PROPERTIES = {
sessionId: {
type: 'string',
description: 'Unique identifier for the session',
},
url: {
type: 'string',
description: 'URL to view the session',
},
status: {
type: 'string',
description: 'Session status',
},
statusDetail: {
type: 'string',
description: 'Detailed status',
optional: true,
},
title: {
type: 'string',
description: 'Session title',
optional: true,
},
createdAt: {
type: 'number',
description: 'Creation timestamp (Unix)',
optional: true,
},
updatedAt: {
type: 'number',
description: 'Last updated timestamp (Unix)',
optional: true,
},
tags: {
type: 'json',
description: 'Session tags',
optional: true,
},
} as const satisfies Record<string, OutputProperty>
export interface DevinSessionOutput {
sessionId: string
url: string
status: string
statusDetail: string | null
title: string | null
createdAt: number | null
updatedAt: number | null
acusConsumed: number | null
tags: string[] | null
pullRequests: Array<{ pr_url: string; pr_state: string | null }> | null
structuredOutput: Record<string, unknown> | null
playbookId: string | null
}
export interface DevinCreateSessionResponse extends ToolResponse {
output: DevinSessionOutput
}
export interface DevinGetSessionResponse extends ToolResponse {
output: DevinSessionOutput
}
export interface DevinListSessionsResponse extends ToolResponse {
output: {
sessions: Array<{
sessionId: string
url: string
status: string
statusDetail: string | null
title: string | null
createdAt: number | null
updatedAt: number | null
tags: string[] | null
}>
}
}
export interface DevinSendMessageResponse extends ToolResponse {
output: DevinSessionOutput
}

View File

@@ -0,0 +1,132 @@
import type {
GoogleBigQueryGetTableParams,
GoogleBigQueryGetTableResponse,
} from '@/tools/google_bigquery/types'
import type { ToolConfig } from '@/tools/types'
export const googleBigQueryGetTableTool: ToolConfig<
GoogleBigQueryGetTableParams,
GoogleBigQueryGetTableResponse
> = {
id: 'google_bigquery_get_table',
name: 'BigQuery Get Table',
description: 'Get metadata and schema for a Google BigQuery table',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-bigquery',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Cloud project ID',
},
datasetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BigQuery dataset ID',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BigQuery table ID',
},
},
request: {
url: (params) =>
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables/${encodeURIComponent(params.tableId)}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || 'Failed to get BigQuery table'
throw new Error(errorMessage)
}
const schema = (data.schema?.fields ?? []).map(
(f: { name: string; type: string; mode?: string; description?: string }) => ({
name: f.name,
type: f.type,
mode: f.mode ?? null,
description: f.description ?? null,
})
)
return {
success: true,
output: {
tableId: data.tableReference?.tableId ?? null,
datasetId: data.tableReference?.datasetId ?? null,
projectId: data.tableReference?.projectId ?? null,
type: data.type ?? null,
description: data.description ?? null,
numRows: data.numRows ?? null,
numBytes: data.numBytes ?? null,
schema,
creationTime: data.creationTime ?? null,
lastModifiedTime: data.lastModifiedTime ?? null,
location: data.location ?? null,
},
}
},
outputs: {
tableId: { type: 'string', description: 'Table ID' },
datasetId: { type: 'string', description: 'Dataset ID' },
projectId: { type: 'string', description: 'Project ID' },
type: {
type: 'string',
description: 'Table type (TABLE, VIEW, SNAPSHOT, MATERIALIZED_VIEW, EXTERNAL)',
},
description: { type: 'string', description: 'Table description', optional: true },
numRows: { type: 'string', description: 'Total number of rows' },
numBytes: {
type: 'string',
description: 'Total size in bytes, excluding data in streaming buffer',
},
schema: {
type: 'array',
description: 'Array of column definitions',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Column name' },
type: {
type: 'string',
description: 'Data type (STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, RECORD, etc.)',
},
mode: {
type: 'string',
description: 'Column mode (NULLABLE, REQUIRED, or REPEATED)',
optional: true,
},
description: { type: 'string', description: 'Column description', optional: true },
},
},
},
creationTime: { type: 'string', description: 'Table creation time (milliseconds since epoch)' },
lastModifiedTime: {
type: 'string',
description: 'Last modification time (milliseconds since epoch)',
},
location: { type: 'string', description: 'Geographic location where the table resides' },
},
}

View File

@@ -0,0 +1,5 @@
export { googleBigQueryGetTableTool } from '@/tools/google_bigquery/get_table'
export { googleBigQueryInsertRowsTool } from '@/tools/google_bigquery/insert_rows'
export { googleBigQueryListDatasetsTool } from '@/tools/google_bigquery/list_datasets'
export { googleBigQueryListTablesTool } from '@/tools/google_bigquery/list_tables'
export { googleBigQueryQueryTool } from '@/tools/google_bigquery/query'

View File

@@ -0,0 +1,174 @@
import type {
GoogleBigQueryInsertRowsParams,
GoogleBigQueryInsertRowsResponse,
} from '@/tools/google_bigquery/types'
import type { ToolConfig } from '@/tools/types'
export const googleBigQueryInsertRowsTool: ToolConfig<
GoogleBigQueryInsertRowsParams,
GoogleBigQueryInsertRowsResponse
> = {
id: 'google_bigquery_insert_rows',
name: 'BigQuery Insert Rows',
description: 'Insert rows into a Google BigQuery table using streaming insert',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-bigquery',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Cloud project ID',
},
datasetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BigQuery dataset ID',
},
tableId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BigQuery table ID',
},
rows: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'JSON array of row objects to insert',
},
skipInvalidRows: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to insert valid rows even if some are invalid',
},
ignoreUnknownValues: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to ignore columns not in the table schema',
},
},
request: {
url: (params) =>
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables/${encodeURIComponent(params.tableId)}/insertAll`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const parsedRows = typeof params.rows === 'string' ? JSON.parse(params.rows) : params.rows
const rows = (parsedRows as Record<string, unknown>[]).map(
(row: Record<string, unknown>) => ({ json: row })
)
const body: Record<string, unknown> = { rows }
if (params.skipInvalidRows !== undefined) body.skipInvalidRows = params.skipInvalidRows
if (params.ignoreUnknownValues !== undefined)
body.ignoreUnknownValues = params.ignoreUnknownValues
return body
},
},
transformResponse: async (response: Response, params?: GoogleBigQueryInsertRowsParams) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || 'Failed to insert rows into BigQuery table'
throw new Error(errorMessage)
}
const insertErrors = data.insertErrors ?? []
const errors = insertErrors.map(
(err: {
index: number
errors: Array<{ reason?: string; location?: string; message?: string }>
}) => ({
index: err.index,
errors: err.errors.map((e) => ({
reason: e.reason ?? null,
location: e.location ?? null,
message: e.message ?? null,
})),
})
)
let totalRows = 0
if (params?.rows) {
const parsed = typeof params.rows === 'string' ? JSON.parse(params.rows) : params.rows
totalRows = Array.isArray(parsed) ? parsed.length : 0
}
// When insertErrors is empty, all rows succeeded.
// When insertErrors is present and skipInvalidRows is false (default),
// the entire batch is rejected — no rows are inserted.
let insertedRows = 0
if (insertErrors.length === 0) {
insertedRows = totalRows
} else if (params?.skipInvalidRows) {
const failedIndexes = new Set(insertErrors.map((e: { index: number }) => e.index))
insertedRows = totalRows - failedIndexes.size
}
return {
success: true,
output: {
insertedRows,
errors,
},
}
},
outputs: {
insertedRows: { type: 'number', description: 'Number of rows successfully inserted' },
errors: {
type: 'array',
description: 'Array of per-row insertion errors (empty if all succeeded)',
items: {
type: 'object',
properties: {
index: { type: 'number', description: 'Zero-based index of the row that failed' },
errors: {
type: 'array',
description: 'Error details for this row',
items: {
type: 'object',
properties: {
reason: {
type: 'string',
description: 'Short error code summarizing the error',
optional: true,
},
location: {
type: 'string',
description: 'Where the error occurred',
optional: true,
},
message: {
type: 'string',
description: 'Human-readable error description',
optional: true,
},
},
},
},
},
},
},
},
}

View File

@@ -0,0 +1,121 @@
import type {
GoogleBigQueryListDatasetsParams,
GoogleBigQueryListDatasetsResponse,
} from '@/tools/google_bigquery/types'
import type { ToolConfig } from '@/tools/types'
export const googleBigQueryListDatasetsTool: ToolConfig<
GoogleBigQueryListDatasetsParams,
GoogleBigQueryListDatasetsResponse
> = {
id: 'google_bigquery_list_datasets',
name: 'BigQuery List Datasets',
description: 'List all datasets in a Google BigQuery project',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-bigquery',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Cloud project ID',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of datasets to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Token for pagination',
},
},
request: {
url: (params) => {
const url = new URL(
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets`
)
if (params.maxResults !== undefined && params.maxResults !== null) {
const maxResults = Number(params.maxResults)
if (Number.isFinite(maxResults) && maxResults > 0) {
url.searchParams.set('maxResults', String(maxResults))
}
}
if (params.pageToken) url.searchParams.set('pageToken', params.pageToken)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || 'Failed to list BigQuery datasets'
throw new Error(errorMessage)
}
const datasets = (data.datasets ?? []).map(
(ds: {
datasetReference: { datasetId: string; projectId: string }
friendlyName?: string
location?: string
}) => ({
datasetId: ds.datasetReference.datasetId,
projectId: ds.datasetReference.projectId,
friendlyName: ds.friendlyName ?? null,
location: ds.location ?? null,
})
)
return {
success: true,
output: {
datasets,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
datasets: {
type: 'array',
description: 'Array of dataset objects',
items: {
type: 'object',
properties: {
datasetId: { type: 'string', description: 'Unique dataset identifier' },
projectId: { type: 'string', description: 'Project ID containing this dataset' },
friendlyName: {
type: 'string',
description: 'Descriptive name for the dataset',
optional: true,
},
location: { type: 'string', description: 'Geographic location where the data resides' },
},
},
},
nextPageToken: {
type: 'string',
description: 'Token for fetching next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,142 @@
import type {
GoogleBigQueryListTablesParams,
GoogleBigQueryListTablesResponse,
} from '@/tools/google_bigquery/types'
import type { ToolConfig } from '@/tools/types'
export const googleBigQueryListTablesTool: ToolConfig<
GoogleBigQueryListTablesParams,
GoogleBigQueryListTablesResponse
> = {
id: 'google_bigquery_list_tables',
name: 'BigQuery List Tables',
description: 'List all tables in a Google BigQuery dataset',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-bigquery',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Cloud project ID',
},
datasetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BigQuery dataset ID',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tables to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Token for pagination',
},
},
request: {
url: (params) => {
const url = new URL(
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId)}/tables`
)
if (params.maxResults !== undefined && params.maxResults !== null) {
const maxResults = Number(params.maxResults)
if (Number.isFinite(maxResults) && maxResults > 0) {
url.searchParams.set('maxResults', String(maxResults))
}
}
if (params.pageToken) url.searchParams.set('pageToken', params.pageToken)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || 'Failed to list BigQuery tables'
throw new Error(errorMessage)
}
const tables = (data.tables ?? []).map(
(t: {
tableReference: { tableId: string; datasetId: string; projectId: string }
type?: string
friendlyName?: string
creationTime?: string
}) => ({
tableId: t.tableReference.tableId,
datasetId: t.tableReference.datasetId,
projectId: t.tableReference.projectId,
type: t.type ?? null,
friendlyName: t.friendlyName ?? null,
creationTime: t.creationTime ?? null,
})
)
return {
success: true,
output: {
tables,
totalItems: data.totalItems ?? null,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
tables: {
type: 'array',
description: 'Array of table objects',
items: {
type: 'object',
properties: {
tableId: { type: 'string', description: 'Table identifier' },
datasetId: { type: 'string', description: 'Dataset ID containing this table' },
projectId: { type: 'string', description: 'Project ID containing this table' },
type: { type: 'string', description: 'Table type (TABLE, VIEW, EXTERNAL, etc.)' },
friendlyName: {
type: 'string',
description: 'User-friendly name for the table',
optional: true,
},
creationTime: {
type: 'string',
description: 'Time when created, in milliseconds since epoch',
optional: true,
},
},
},
},
totalItems: {
type: 'number',
description: 'Total number of tables in the dataset',
optional: true,
},
nextPageToken: {
type: 'string',
description: 'Token for fetching next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,164 @@
import type {
GoogleBigQueryQueryParams,
GoogleBigQueryQueryResponse,
} from '@/tools/google_bigquery/types'
import type { ToolConfig } from '@/tools/types'
export const googleBigQueryQueryTool: ToolConfig<
GoogleBigQueryQueryParams,
GoogleBigQueryQueryResponse
> = {
id: 'google_bigquery_query',
name: 'BigQuery Run Query',
description: 'Run a SQL query against Google BigQuery and return the results',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-bigquery',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Cloud project ID',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SQL query to execute',
},
useLegacySql: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to use legacy SQL syntax (default: false)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of rows to return',
},
defaultDatasetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Default dataset for unqualified table names',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Processing location (e.g., "US", "EU")',
},
},
request: {
url: (params) =>
`https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/queries`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
query: params.query,
useLegacySql: params.useLegacySql ?? false,
}
if (params.maxResults !== undefined) body.maxResults = Number(params.maxResults)
if (params.defaultDatasetId) {
body.defaultDataset = {
projectId: params.projectId,
datasetId: params.defaultDatasetId,
}
}
if (params.location) body.location = params.location
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
const errorMessage = data.error?.message || 'Failed to execute BigQuery query'
throw new Error(errorMessage)
}
const columns = (data.schema?.fields ?? []).map((f: { name: string }) => f.name)
const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => {
const obj: Record<string, unknown> = {}
row.f.forEach((field, index) => {
obj[columns[index]] = field.v ?? null
})
return obj
})
return {
success: true,
output: {
columns,
rows,
totalRows: data.totalRows ?? null,
jobComplete: data.jobComplete ?? false,
totalBytesProcessed: data.totalBytesProcessed ?? null,
cacheHit: data.cacheHit ?? null,
jobReference: data.jobReference ?? null,
pageToken: data.pageToken ?? null,
},
}
},
outputs: {
columns: {
type: 'array',
description: 'Array of column names from the query result',
items: { type: 'string', description: 'Column name' },
},
rows: {
type: 'array',
description: 'Array of row objects keyed by column name',
items: {
type: 'object',
description: 'Row with column name/value pairs',
},
},
totalRows: {
type: 'string',
description: 'Total number of rows in the complete result set',
optional: true,
},
jobComplete: { type: 'boolean', description: 'Whether the query completed within the timeout' },
totalBytesProcessed: { type: 'string', description: 'Total bytes processed by the query' },
cacheHit: {
type: 'boolean',
description: 'Whether the query result was served from cache',
optional: true,
},
jobReference: {
type: 'object',
description: 'Job reference (useful when jobComplete is false)',
optional: true,
properties: {
projectId: { type: 'string', description: 'Project ID containing the job' },
jobId: { type: 'string', description: 'Unique job identifier' },
location: { type: 'string', description: 'Geographic location of the job' },
},
},
pageToken: {
type: 'string',
description: 'Token for fetching additional result pages',
optional: true,
},
},
}

View File

@@ -0,0 +1,119 @@
import type { ToolResponse } from '@/tools/types'
export interface GoogleBigQueryBaseParams {
accessToken: string
projectId: string
}
export interface GoogleBigQueryQueryParams extends GoogleBigQueryBaseParams {
query: string
useLegacySql?: boolean
maxResults?: number
defaultDatasetId?: string
location?: string
}
export interface GoogleBigQueryListDatasetsParams extends GoogleBigQueryBaseParams {
maxResults?: number
pageToken?: string
}
export interface GoogleBigQueryListTablesParams extends GoogleBigQueryBaseParams {
datasetId: string
maxResults?: number
pageToken?: string
}
export interface GoogleBigQueryGetTableParams extends GoogleBigQueryBaseParams {
datasetId: string
tableId: string
}
export interface GoogleBigQueryInsertRowsParams extends GoogleBigQueryBaseParams {
datasetId: string
tableId: string
rows: string
skipInvalidRows?: boolean
ignoreUnknownValues?: boolean
}
export interface GoogleBigQueryJobReference {
projectId: string
jobId: string
location: string
}
export interface GoogleBigQueryQueryResponse extends ToolResponse {
output: {
columns: string[]
rows: Record<string, unknown>[]
totalRows: string | null
jobComplete: boolean
totalBytesProcessed: string | null
cacheHit: boolean | null
jobReference: GoogleBigQueryJobReference | null
pageToken: string | null
}
}
export interface GoogleBigQueryListDatasetsResponse extends ToolResponse {
output: {
datasets: Array<{
datasetId: string
projectId: string
friendlyName: string | null
location: string | null
}>
nextPageToken: string | null
}
}
export interface GoogleBigQueryListTablesResponse extends ToolResponse {
output: {
tables: Array<{
tableId: string
datasetId: string
projectId: string
type: string | null
friendlyName: string | null
creationTime: string | null
}>
totalItems: number | null
nextPageToken: string | null
}
}
export interface GoogleBigQueryGetTableResponse extends ToolResponse {
output: {
tableId: string
datasetId: string
projectId: string
type: string | null
description: string | null
numRows: string | null
numBytes: string | null
schema: Array<{
name: string
type: string
mode: string | null
description: string | null
}>
creationTime: string | null
lastModifiedTime: string | null
location: string | null
}
}
export interface GoogleBigQueryInsertRowsResponse extends ToolResponse {
output: {
insertedRows: number
errors: Array<{
index: number
errors: Array<{
reason: string | null
location: string | null
message: string | null
}>
}>
}
}

View File

@@ -0,0 +1,133 @@
import type { GoogleTasksCreateParams, GoogleTasksResponse } from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const createTool: ToolConfig<GoogleTasksCreateParams, GoogleTasksResponse> = {
id: 'google_tasks_create',
name: 'Google Tasks Create Task',
description: 'Create a new task in a Google Tasks list',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
taskListId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task list ID (defaults to primary task list "@default")',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the task (max 1024 characters)',
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Notes/description for the task (max 8192 characters)',
},
due: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Due date in RFC 3339 format (e.g., 2025-06-03T00:00:00.000Z)',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task status: "needsAction" or "completed"',
},
parent: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Parent task ID to create this task as a subtask. Omit for top-level tasks.',
},
previous: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Previous sibling task ID to position after. Omit to place first among siblings.',
},
},
request: {
url: (params) => {
const taskListId = params.taskListId || '@default'
const queryParams = new URLSearchParams()
if (params.parent) queryParams.set('parent', params.parent)
if (params.previous) queryParams.set('previous', params.previous)
const qs = queryParams.toString()
return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
title: params.title,
}
if (params.notes) body.notes = params.notes
if (params.due) body.due = params.due
if (params.status) body.status = params.status
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message ?? 'Failed to create task')
}
return {
success: true,
output: {
id: data.id ?? null,
title: data.title ?? null,
notes: data.notes ?? null,
status: data.status ?? null,
due: data.due ?? null,
updated: data.updated ?? null,
selfLink: data.selfLink ?? null,
webViewLink: data.webViewLink ?? null,
parent: data.parent ?? null,
position: data.position ?? null,
completed: data.completed ?? null,
deleted: data.deleted ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Task ID' },
title: { type: 'string', description: 'Task title' },
notes: { type: 'string', description: 'Task notes', optional: true },
status: { type: 'string', description: 'Task status (needsAction or completed)' },
due: { type: 'string', description: 'Due date', optional: true },
updated: { type: 'string', description: 'Last modification time' },
selfLink: { type: 'string', description: 'URL for the task' },
webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true },
parent: { type: 'string', description: 'Parent task ID', optional: true },
position: { type: 'string', description: 'Position among sibling tasks' },
completed: { type: 'string', description: 'Completion date', optional: true },
deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true },
},
}

View File

@@ -0,0 +1,67 @@
import type { GoogleTasksDeleteParams, GoogleTasksDeleteResponse } from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<GoogleTasksDeleteParams, GoogleTasksDeleteResponse> = {
id: 'google_tasks_delete',
name: 'Google Tasks Delete Task',
description: 'Delete a task from a Google Tasks list',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
taskListId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task list ID (defaults to primary task list "@default")',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to delete',
},
},
request: {
url: (params) => {
const taskListId = params.taskListId || '@default'
return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
taskId: params?.taskId || '',
deleted: true,
},
}
}
const data = await response.json()
throw new Error(data.error?.message ?? 'Failed to delete task')
},
outputs: {
taskId: { type: 'string', description: 'Deleted task ID' },
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
},
}

View File

@@ -0,0 +1,88 @@
import type { GoogleTasksGetParams, GoogleTasksResponse } from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const getTool: ToolConfig<GoogleTasksGetParams, GoogleTasksResponse> = {
id: 'google_tasks_get',
name: 'Google Tasks Get Task',
description: 'Retrieve a specific task by ID from a Google Tasks list',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
taskListId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task list ID (defaults to primary task list "@default")',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to retrieve',
},
},
request: {
url: (params) => {
const taskListId = params.taskListId || '@default'
return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}`
},
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 task')
}
return {
success: true,
output: {
id: data.id ?? null,
title: data.title ?? null,
notes: data.notes ?? null,
status: data.status ?? null,
due: data.due ?? null,
updated: data.updated ?? null,
selfLink: data.selfLink ?? null,
webViewLink: data.webViewLink ?? null,
parent: data.parent ?? null,
position: data.position ?? null,
completed: data.completed ?? null,
deleted: data.deleted ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Task ID' },
title: { type: 'string', description: 'Task title' },
notes: { type: 'string', description: 'Task notes', optional: true },
status: { type: 'string', description: 'Task status (needsAction or completed)' },
due: { type: 'string', description: 'Due date', optional: true },
updated: { type: 'string', description: 'Last modification time' },
selfLink: { type: 'string', description: 'URL for the task' },
webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true },
parent: { type: 'string', description: 'Parent task ID', optional: true },
position: { type: 'string', description: 'Position among sibling tasks' },
completed: { type: 'string', description: 'Completion date', optional: true },
deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true },
},
}

View File

@@ -0,0 +1,6 @@
export { createTool as googleTasksCreateTool } from '@/tools/google_tasks/create'
export { deleteTool as googleTasksDeleteTool } from '@/tools/google_tasks/delete'
export { getTool as googleTasksGetTool } from '@/tools/google_tasks/get'
export { listTool as googleTasksListTool } from '@/tools/google_tasks/list'
export { listTaskListsTool as googleTasksListTaskListsTool } from '@/tools/google_tasks/list_task_lists'
export { updateTool as googleTasksUpdateTool } from '@/tools/google_tasks/update'

View File

@@ -0,0 +1,214 @@
import type { GoogleTasksListParams, GoogleTasksListResponse } from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const listTool: ToolConfig<GoogleTasksListParams, GoogleTasksListResponse> = {
id: 'google_tasks_list',
name: 'Google Tasks List Tasks',
description: 'List all tasks in a Google Tasks list',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
taskListId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task list ID (defaults to primary task list "@default")',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of tasks to return (default 20, max 100)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Token for pagination',
},
showCompleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to show completed tasks (default true)',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to show deleted tasks (default false)',
},
showHidden: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to show hidden tasks (default false)',
},
dueMin: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lower bound for due date filter (RFC 3339 timestamp)',
},
dueMax: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Upper bound for due date filter (RFC 3339 timestamp)',
},
completedMin: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lower bound for task completion date (RFC 3339 timestamp)',
},
completedMax: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Upper bound for task completion date (RFC 3339 timestamp)',
},
updatedMin: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lower bound for last modification time (RFC 3339 timestamp)',
},
},
request: {
url: (params) => {
const taskListId = params.taskListId || '@default'
const queryParams = new URLSearchParams()
if (params.maxResults) queryParams.set('maxResults', String(params.maxResults))
if (params.pageToken) queryParams.set('pageToken', params.pageToken)
if (params.showCompleted !== undefined)
queryParams.set('showCompleted', String(params.showCompleted))
if (params.showDeleted !== undefined)
queryParams.set('showDeleted', String(params.showDeleted))
if (params.showHidden !== undefined) queryParams.set('showHidden', String(params.showHidden))
if (params.dueMin) queryParams.set('dueMin', params.dueMin)
if (params.dueMax) queryParams.set('dueMax', params.dueMax)
if (params.completedMin) queryParams.set('completedMin', params.completedMin)
if (params.completedMax) queryParams.set('completedMax', params.completedMax)
if (params.updatedMin) queryParams.set('updatedMin', params.updatedMin)
const qs = queryParams.toString()
return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks${qs ? `?${qs}` : ''}`
},
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 tasks')
}
const items = data.items ?? []
return {
success: true,
output: {
tasks: items.map((item: Record<string, unknown>) => ({
id: (item.id as string) ?? null,
title: (item.title as string) ?? null,
notes: (item.notes as string) ?? null,
status: (item.status as string) ?? null,
due: (item.due as string) ?? null,
completed: (item.completed as string) ?? null,
updated: (item.updated as string) ?? null,
selfLink: (item.selfLink as string) ?? null,
webViewLink: (item.webViewLink as string) ?? null,
parent: (item.parent as string) ?? null,
position: (item.position as string) ?? null,
hidden: (item.hidden as boolean) ?? null,
deleted: (item.deleted as boolean) ?? null,
links: Array.isArray(item.links)
? (item.links as Array<Record<string, string>>).map((link) => ({
type: link.type ?? '',
description: link.description ?? '',
link: link.link ?? '',
}))
: [],
})),
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
tasks: {
type: 'array',
description: 'List of tasks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task identifier' },
title: { type: 'string', description: 'Title of the task' },
notes: { type: 'string', description: 'Notes/description for the task', optional: true },
status: {
type: 'string',
description: 'Task status: "needsAction" or "completed"',
},
due: { type: 'string', description: 'Due date (RFC 3339 timestamp)', optional: true },
completed: {
type: 'string',
description: 'Completion date (RFC 3339 timestamp)',
optional: true,
},
updated: { type: 'string', description: 'Last modification time (RFC 3339 timestamp)' },
selfLink: { type: 'string', description: 'URL pointing to this task' },
webViewLink: {
type: 'string',
description: 'Link to task in Google Tasks UI',
optional: true,
},
parent: { type: 'string', description: 'Parent task identifier', optional: true },
position: {
type: 'string',
description: 'Position among sibling tasks (string-based ordering)',
},
hidden: { type: 'boolean', description: 'Whether the task is hidden', optional: true },
deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true },
links: {
type: 'array',
description: 'Collection of links associated with the task',
optional: true,
items: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Link type (e.g., "email", "generic", "chat_message")',
},
description: { type: 'string', description: 'Link description' },
link: { type: 'string', description: 'The URL' },
},
},
},
},
},
},
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,103 @@
import type {
GoogleTasksListTaskListsParams,
GoogleTasksListTaskListsResponse,
} from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const listTaskListsTool: ToolConfig<
GoogleTasksListTaskListsParams,
GoogleTasksListTaskListsResponse
> = {
id: 'google_tasks_list_task_lists',
name: 'Google Tasks List Task Lists',
description: 'Retrieve all task lists for the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of task lists to return (default 20, max 100)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Token for pagination',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.maxResults) queryParams.set('maxResults', String(params.maxResults))
if (params.pageToken) queryParams.set('pageToken', params.pageToken)
const qs = queryParams.toString()
return `${TASKS_API_BASE}/users/@me/lists${qs ? `?${qs}` : ''}`
},
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 task lists')
}
const items = data.items ?? []
return {
success: true,
output: {
taskLists: items.map((item: Record<string, unknown>) => ({
id: (item.id as string) ?? null,
title: (item.title as string) ?? null,
updated: (item.updated as string) ?? null,
selfLink: (item.selfLink as string) ?? null,
})),
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
taskLists: {
type: 'array',
description: 'List of task lists',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task list identifier' },
title: { type: 'string', description: 'Title of the task list' },
updated: {
type: 'string',
description: 'Last modification time (RFC 3339 timestamp)',
},
selfLink: { type: 'string', description: 'URL pointing to this task list' },
},
},
},
nextPageToken: {
type: 'string',
description: 'Token for retrieving the next page of results',
optional: true,
},
},
}

View File

@@ -0,0 +1,113 @@
import type { ToolResponse } from '@/tools/types'
export const TASKS_API_BASE = 'https://tasks.googleapis.com/tasks/v1'
export interface BaseGoogleTasksParams {
accessToken: string
}
export interface GoogleTasksListTaskListsParams extends BaseGoogleTasksParams {
maxResults?: number
pageToken?: string
}
export interface GoogleTasksCreateParams extends BaseGoogleTasksParams {
taskListId?: string
title: string
notes?: string
due?: string
status?: string
parent?: string
previous?: string
}
export interface GoogleTasksListParams extends BaseGoogleTasksParams {
taskListId?: string
maxResults?: number
pageToken?: string
showCompleted?: boolean
showDeleted?: boolean
showHidden?: boolean
dueMin?: string
dueMax?: string
completedMin?: string
completedMax?: string
updatedMin?: string
}
export interface GoogleTasksGetParams extends BaseGoogleTasksParams {
taskListId?: string
taskId: string
}
export interface GoogleTasksUpdateParams extends BaseGoogleTasksParams {
taskListId?: string
taskId: string
title?: string
notes?: string
due?: string
status?: string
}
export interface GoogleTasksDeleteParams extends BaseGoogleTasksParams {
taskListId?: string
taskId: string
}
export interface GoogleTasksResponse extends ToolResponse {
output: {
id: string | null
title: string | null
notes: string | null
status: string | null
due: string | null
updated: string | null
selfLink: string | null
webViewLink: string | null
parent: string | null
position: string | null
completed: string | null
deleted: boolean | null
}
}
export interface GoogleTasksListResponse extends ToolResponse {
output: {
tasks: Array<{
id: string | null
title: string | null
notes: string | null
status: string | null
due: string | null
completed: string | null
updated: string | null
selfLink: string | null
webViewLink: string | null
parent: string | null
position: string | null
hidden: boolean | null
deleted: boolean | null
links: Array<{ type: string; description: string; link: string }>
}>
nextPageToken: string | null
}
}
export interface GoogleTasksListTaskListsResponse extends ToolResponse {
output: {
taskLists: Array<{
id: string | null
title: string | null
updated: string | null
selfLink: string | null
}>
nextPageToken: string | null
}
}
export interface GoogleTasksDeleteResponse extends ToolResponse {
output: {
taskId: string
deleted: boolean
}
}

View File

@@ -0,0 +1,121 @@
import type { GoogleTasksResponse, GoogleTasksUpdateParams } from '@/tools/google_tasks/types'
import { TASKS_API_BASE } from '@/tools/google_tasks/types'
import type { ToolConfig } from '@/tools/types'
export const updateTool: ToolConfig<GoogleTasksUpdateParams, GoogleTasksResponse> = {
id: 'google_tasks_update',
name: 'Google Tasks Update Task',
description: 'Update an existing task in a Google Tasks list',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-tasks',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Google Tasks OAuth access token',
},
taskListId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Task list ID (defaults to primary task list "@default")',
},
taskId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the task to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the task',
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New notes for the task',
},
due: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New due date in RFC 3339 format',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New status: "needsAction" or "completed"',
},
},
request: {
url: (params) => {
const taskListId = params.taskListId || '@default'
return `${TASKS_API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks/${encodeURIComponent(params.taskId)}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {}
if (params.title !== undefined) body.title = params.title
if (params.notes !== undefined) body.notes = params.notes
if (params.due !== undefined) body.due = params.due
if (params.status !== undefined) body.status = params.status
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message ?? 'Failed to update task')
}
return {
success: true,
output: {
id: data.id ?? null,
title: data.title ?? null,
notes: data.notes ?? null,
status: data.status ?? null,
due: data.due ?? null,
updated: data.updated ?? null,
selfLink: data.selfLink ?? null,
webViewLink: data.webViewLink ?? null,
parent: data.parent ?? null,
position: data.position ?? null,
completed: data.completed ?? null,
deleted: data.deleted ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Task ID' },
title: { type: 'string', description: 'Task title' },
notes: { type: 'string', description: 'Task notes', optional: true },
status: { type: 'string', description: 'Task status (needsAction or completed)' },
due: { type: 'string', description: 'Due date', optional: true },
updated: { type: 'string', description: 'Last modification time' },
selfLink: { type: 'string', description: 'URL for the task' },
webViewLink: { type: 'string', description: 'Link to task in Google Tasks UI', optional: true },
parent: { type: 'string', description: 'Parent task ID', optional: true },
position: { type: 'string', description: 'Position among sibling tasks' },
completed: { type: 'string', description: 'Completion date', optional: true },
deleted: { type: 'boolean', description: 'Whether the task is deleted', optional: true },
},
}

View File

@@ -190,17 +190,25 @@ import {
confluenceCreateCommentTool,
confluenceCreatePagePropertyTool,
confluenceCreatePageTool,
confluenceCreateSpacePropertyTool,
confluenceCreateSpaceTool,
confluenceDeleteAttachmentTool,
confluenceDeleteBlogPostTool,
confluenceDeleteCommentTool,
confluenceDeleteLabelTool,
confluenceDeletePagePropertyTool,
confluenceDeletePageTool,
confluenceDeleteSpacePropertyTool,
confluenceDeleteSpaceTool,
confluenceGetBlogPostTool,
confluenceGetPageAncestorsTool,
confluenceGetPageChildrenTool,
confluenceGetPageDescendantsTool,
confluenceGetPagesByLabelTool,
confluenceGetPageVersionTool,
confluenceGetSpaceTool,
confluenceGetTaskTool,
confluenceGetUserTool,
confluenceListAttachmentsTool,
confluenceListBlogPostsInSpaceTool,
confluenceListBlogPostsTool,
@@ -210,11 +218,17 @@ import {
confluenceListPagesInSpaceTool,
confluenceListPageVersionsTool,
confluenceListSpaceLabelsTool,
confluenceListSpacePermissionsTool,
confluenceListSpacePropertiesTool,
confluenceListSpacesTool,
confluenceListTasksTool,
confluenceRetrieveTool,
confluenceSearchInSpaceTool,
confluenceSearchTool,
confluenceUpdateBlogPostTool,
confluenceUpdateCommentTool,
confluenceUpdateSpaceTool,
confluenceUpdateTaskTool,
confluenceUpdateTool,
confluenceUploadAttachmentTool,
} from '@/tools/confluence'
@@ -248,6 +262,12 @@ import {
datadogSendLogsTool,
datadogSubmitMetricsTool,
} from '@/tools/datadog'
import {
devinCreateSessionTool,
devinGetSessionTool,
devinListSessionsTool,
devinSendMessageTool,
} from '@/tools/devin'
import {
discordAddReactionTool,
discordArchiveThreadTool,
@@ -630,6 +650,13 @@ import {
gongLookupPhoneTool,
} from '@/tools/gong'
import { googleSearchTool } from '@/tools/google'
import {
googleBigQueryGetTableTool,
googleBigQueryInsertRowsTool,
googleBigQueryListDatasetsTool,
googleBigQueryListTablesTool,
googleBigQueryQueryTool,
} from '@/tools/google_bigquery'
import { googleBooksVolumeDetailsTool, googleBooksVolumeSearchTool } from '@/tools/google_books'
import {
googleCalendarCreateTool,
@@ -755,6 +782,14 @@ import {
googleSlidesUpdateSlidesPositionTool,
googleSlidesWriteTool,
} from '@/tools/google_slides'
import {
googleTasksCreateTool,
googleTasksDeleteTool,
googleTasksGetTool,
googleTasksListTaskListsTool,
googleTasksListTool,
googleTasksUpdateTool,
} from '@/tools/google_tasks'
import { googleTranslateDetectTool, googleTranslateTool } from '@/tools/google_translate'
import {
createMattersExportTool,
@@ -2556,6 +2591,10 @@ export const tools: Record<string, ToolConfig> = {
dropbox_get_metadata: dropboxGetMetadataTool,
dropbox_create_shared_link: dropboxCreateSharedLinkTool,
dropbox_search: dropboxSearchTool,
devin_create_session: devinCreateSessionTool,
devin_get_session: devinGetSessionTool,
devin_list_sessions: devinListSessionsTool,
devin_send_message: devinSendMessageTool,
duckduckgo_search: duckduckgoSearchTool,
dspy_predict: predictTool,
dspy_chain_of_thought: chainOfThoughtTool,
@@ -2921,6 +2960,12 @@ export const tools: Record<string, ToolConfig> = {
google_maps_speed_limits: googleMapsSpeedLimitsTool,
google_maps_timezone: googleMapsTimezoneTool,
google_maps_validate_address: googleMapsValidateAddressTool,
google_tasks_create: googleTasksCreateTool,
google_tasks_delete: googleTasksDeleteTool,
google_tasks_get: googleTasksGetTool,
google_tasks_list: googleTasksListTool,
google_tasks_list_task_lists: googleTasksListTaskListsTool,
google_tasks_update: googleTasksUpdateTool,
google_translate_detect: googleTranslateDetectTool,
google_translate_text: googleTranslateTool,
google_sheets_read: googleSheetsReadTool,
@@ -3032,8 +3077,22 @@ export const tools: Record<string, ToolConfig> = {
confluence_list_space_labels: confluenceListSpaceLabelsTool,
confluence_delete_label: confluenceDeleteLabelTool,
confluence_delete_page_property: confluenceDeletePagePropertyTool,
confluence_get_page_descendants: confluenceGetPageDescendantsTool,
confluence_get_space: confluenceGetSpaceTool,
confluence_create_space: confluenceCreateSpaceTool,
confluence_update_space: confluenceUpdateSpaceTool,
confluence_delete_space: confluenceDeleteSpaceTool,
confluence_get_user: confluenceGetUserTool,
confluence_list_spaces: confluenceListSpacesTool,
confluence_update_blogpost: confluenceUpdateBlogPostTool,
confluence_delete_blogpost: confluenceDeleteBlogPostTool,
confluence_list_tasks: confluenceListTasksTool,
confluence_get_task: confluenceGetTaskTool,
confluence_update_task: confluenceUpdateTaskTool,
confluence_list_space_permissions: confluenceListSpacePermissionsTool,
confluence_list_space_properties: confluenceListSpacePropertiesTool,
confluence_create_space_property: confluenceCreateSpacePropertyTool,
confluence_delete_space_property: confluenceDeleteSpacePropertyTool,
cursor_list_agents: cursorListAgentsTool,
cursor_list_agents_v2: cursorListAgentsV2Tool,
cursor_get_agent: cursorGetAgentTool,
@@ -3593,6 +3652,11 @@ export const tools: Record<string, ToolConfig> = {
wordpress_list_users: wordpressListUsersTool,
wordpress_get_user: wordpressGetUserTool,
wordpress_search_content: wordpressSearchContentTool,
google_bigquery_query: googleBigQueryQueryTool,
google_bigquery_list_datasets: googleBigQueryListDatasetsTool,
google_bigquery_list_tables: googleBigQueryListTablesTool,
google_bigquery_get_table: googleBigQueryGetTableTool,
google_bigquery_insert_rows: googleBigQueryInsertRowsTool,
google_vault_create_matters_export: createMattersExportTool,
google_vault_list_matters_export: listMattersExportTool,
google_vault_create_matters_holds: createMattersHoldsTool,

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
DROP INDEX "a2a_agent_workspace_id_idx";--> statement-breakpoint
DROP INDEX "a2a_push_notification_config_task_id_idx";--> statement-breakpoint
DROP INDEX "credential_member_credential_id_idx";--> statement-breakpoint
DROP INDEX "credential_set_organization_id_idx";--> statement-breakpoint
DROP INDEX "credential_set_member_set_id_idx";--> statement-breakpoint
DROP INDEX "permission_group_organization_id_idx";--> statement-breakpoint
DROP INDEX "skill_workspace_id_idx";--> statement-breakpoint
DROP INDEX "user_table_rows_workspace_id_idx";--> statement-breakpoint
CREATE INDEX "workflow_execution_logs_running_started_at_idx" ON "workflow_execution_logs" USING btree ("started_at") WHERE status = 'running';

File diff suppressed because it is too large Load Diff

View File

@@ -1114,6 +1114,13 @@
"when": 1771897746619,
"tag": "0159_magical_marten_broadcloak",
"breakpoints": true
},
{
"idx": 160,
"version": "7",
"when": 1772138479896,
"tag": "0160_classy_tyger_tiger",
"breakpoints": true
}
]
}

View File

@@ -334,6 +334,9 @@ export const workflowExecutionLogs = pgTable(
table.workspaceId,
table.startedAt
),
runningStartedAtIdx: index('workflow_execution_logs_running_started_at_idx')
.on(table.startedAt)
.where(sql`status = 'running'`),
})
)
@@ -812,7 +815,6 @@ export const skill = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
workspaceIdIdx: index('skill_workspace_id_idx').on(table.workspaceId),
workspaceNameUnique: uniqueIndex('skill_workspace_name_unique').on(
table.workspaceId,
table.name
@@ -1934,7 +1936,6 @@ export const a2aAgent = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
workspaceIdIdx: index('a2a_agent_workspace_id_idx').on(table.workspaceId),
workflowIdIdx: index('a2a_agent_workflow_id_idx').on(table.workflowId),
createdByIdx: index('a2a_agent_created_by_idx').on(table.createdBy),
workspaceWorkflowUnique: uniqueIndex('a2a_agent_workspace_workflow_unique').on(
@@ -2018,7 +2019,6 @@ export const a2aPushNotificationConfig = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
taskIdIdx: index('a2a_push_notification_config_task_id_idx').on(table.taskId),
taskIdUnique: uniqueIndex('a2a_push_notification_config_task_unique').on(table.taskId),
})
)
@@ -2173,7 +2173,6 @@ export const credentialMember = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
credentialIdIdx: index('credential_member_credential_id_idx').on(table.credentialId),
userIdIdx: index('credential_member_user_id_idx').on(table.userId),
roleIdx: index('credential_member_role_idx').on(table.role),
statusIdx: index('credential_member_status_idx').on(table.status),
@@ -2224,7 +2223,6 @@ export const credentialSet = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
organizationIdIdx: index('credential_set_organization_id_idx').on(table.organizationId),
createdByIdx: index('credential_set_created_by_idx').on(table.createdBy),
orgNameUnique: uniqueIndex('credential_set_org_name_unique').on(
table.organizationId,
@@ -2257,7 +2255,6 @@ export const credentialSetMember = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
credentialSetIdIdx: index('credential_set_member_set_id_idx').on(table.credentialSetId),
userIdIdx: index('credential_set_member_user_id_idx').on(table.userId),
uniqueMembership: uniqueIndex('credential_set_member_unique').on(
table.credentialSetId,
@@ -2320,7 +2317,6 @@ export const permissionGroup = pgTable(
autoAddNewMembers: boolean('auto_add_new_members').notNull().default(false),
},
(table) => ({
organizationIdIdx: index('permission_group_organization_id_idx').on(table.organizationId),
createdByIdx: index('permission_group_created_by_idx').on(table.createdBy),
orgNameUnique: uniqueIndex('permission_group_org_name_unique').on(
table.organizationId,
@@ -2438,7 +2434,6 @@ export const userTableRows = pgTable(
},
(table) => ({
tableIdIdx: index('user_table_rows_table_id_idx').on(table.tableId),
workspaceIdIdx: index('user_table_rows_workspace_id_idx').on(table.workspaceId),
dataGinIdx: index('user_table_rows_data_gin_idx').using('gin', table.data),
workspaceTableIdx: index('user_table_rows_workspace_table_idx').on(
table.workspaceId,

View File

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