Compare commits

...

72 Commits

Author SHA1 Message Date
Vikhyath Mondreti
e1f04f42f8 v0.3.26: fix billing, bubble up workflow block errors, credentials security improvements
v0.3.26: fix billing, bubble up workflow block errors, credentials security improvements
2025-08-14 14:17:25 -05:00
Vikhyath Mondreti
fd9e61f85a improvement(credentials-security): use clear credentials sharing helper, fix google sheets block url split bug (#968)
* improvement(credentials-sharing-security): cleanup and reuse helper to determine credential access

* few more routes

* fix google sheets block

* fix test mocks

* fix calendar route
2025-08-14 14:13:18 -05:00
Waleed Latif
f1934fe76b fix(billing): separate client side and server side envvars for billing (#966) 2025-08-14 11:29:02 -07:00
Vikhyath Mondreti
ac41bf8c17 Revert "fix(workflow-block): revert change bubbling up error for workflow block" (#965)
* Revert "fix(workflow-block): revert change bubbling up error for workflow blo…"

This reverts commit 9f0993ed57.

* revert test changes
2025-08-14 12:18:47 -05:00
Vikhyath Mondreti
56ffb538a0 Merge pull request #964 from simstudioai/staging
v0.3.25: oauth credentials sharing mechanism, workflow block error handling changes
2025-08-14 02:36:19 -05:00
Vikhyath Mondreti
2e8f051e58 fix workflow block test 2025-08-14 02:28:17 -05:00
Vikhyath Mondreti
9f0993ed57 fix(workflow-block): revert change bubbling up error for workflow block (#963) 2025-08-14 02:18:18 -05:00
Waleed Latif
472a22cc94 improvement(helm): added template for external db secret (#957) 2025-08-13 21:21:46 -07:00
Waleed Latif
da04ea0e9f fix(subflows): added change detection for parallels, updated deploy and status schemas to match parallel/loop (#956) 2025-08-13 21:18:07 -07:00
Waleed Latif
d4f412af92 fix(api): fix api post and get without stringifying (#955) 2025-08-13 18:49:22 -05:00
Siddharth Ganesan
70fa628a2a improvement(uploads): add multipart upload + batching + retries (#938)
* File upload retries + multipart uploads

* Lint

* FIle uploads

* File uploads 2

* Lint

* Fix file uploads

* Add auth to file upload routes

* Lint
2025-08-13 15:18:14 -07:00
Vikhyath Mondreti
b159d63fbb improvement(oauth): credentials sharing for workflows (#939)
* improvement(oauth): credential UX while sharing workflows

* fix tests

* address greptile comments

* fix linear, jira, folder selectors

* fix routes

* fix linear

* jira fix attempt

* jira fix attempt

* jira fixes

* fix

* fix

* fix jira

* fix selector disable behaviour

* minor fixes

* clear selectors correctly

* fix project selector jira

* fix gdrive

* fix labels dropdown

* fix webhook realtime collab

* fix

* fix webhooks persistence

* fix folders route

* fix lint

* test webhook intermittent error

* fix

* fix display
2025-08-13 16:51:46 -05:00
Adam Gough
5dfe9330bb added file for microsoft verification (#946)
Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
2025-08-13 12:18:31 -05:00
Waleed Latif
4107948554 Merge pull request #954 from simstudioai/staging
fix
2025-08-12 21:12:18 -07:00
Vikhyath Mondreti
7ebc87564d fix(double-read): API Block (#950)
* fix(double-read-http): double reading body json

* fix

* fix tests
2025-08-12 23:08:31 -05:00
Vikhyath Mondreti
8aa0ed19f1 Revert "fix(api): fix api block (#951)" (#953)
This reverts commit 8016af60f4.
2025-08-12 23:05:08 -05:00
Waleed Latif
f7573fadb1 v0.3.24: api block fixes 2025-08-12 20:35:07 -07:00
Waleed Latif
8016af60f4 fix(api): fix api block (#951) 2025-08-12 20:31:41 -07:00
Vikhyath Mondreti
8fccd5c20d Merge pull request #948 from simstudioai/staging
v0.3.24: revert redis session management change
2025-08-12 17:56:16 -05:00
Vikhyath Mondreti
8de06b63d1 Revert "improvement(performance): use redis for session data (#934)" (#947)
This reverts commit 3c7b3e1a4b.
2025-08-12 17:30:21 -05:00
Vikhyath Mondreti
1c818b2e3e v0.3.23: multiplayer variables, api key fixes, kb improvements, triggers fixes
v0.3.23: multiplayer variables, api key fixes, kb improvements, triggers fixes
2025-08-12 15:23:09 -05:00
Vikhyath Mondreti
1a7de84c7a fix(tag-dropdown): last char dropped bug (#945) 2025-08-12 11:48:34 -05:00
Waleed Latif
a2dea384a4 fix(kb): kb-level deletion should reflect in doc level kb tags sidebar registry (#944) 2025-08-12 09:26:28 -07:00
Waleed Latif
1c3e923f1b fix(kb-ui): fixed tags hover effect (#942) 2025-08-12 08:49:19 -07:00
Waleed Latif
e1d5e38528 fix(chunks): instantaneous search + server side searching instead of client-side (#940)
* fix(chunks): instantaneous search + server side searching instead of client-side

* add knowledge tags component to sidebar, replace old knowledge tags UI

* add types, remove extraneous comments

* added knowledge-base level tag definitions viewer, ability to create/delete slots in sidebar and respective routes

* ui

* fix stale tag issue

* use logger
2025-08-12 01:53:47 -07:00
Waleed Latif
3c7b3e1a4b improvement(performance): use redis for session data (#934) 2025-08-11 22:42:22 -05:00
Waleed Latif
bc455d5bf4 feat(variables): multiplayer variables through sockets, persist server side (#933)
* feat(variables): multiplayer variables through sockets, persist server side

* remove extraneous comments

* breakout variables handler in sockets
2025-08-11 18:32:21 -05:00
Waleed Latif
2a333c7cf7 fix(kb): added proper pagination for documents in kb (#937) 2025-08-11 14:16:15 -07:00
Adam Gough
41cc0cdadc fix(webhooks): fixed all webhook structures (#935)
* fix for variable format + trig

* fixed slack variable

* microsoft teams working

* fixed outlook, plus added other minor documentation changes and fixed subblock

* removed discord webhook logic

* added airtable logic

* bun run lint

* test

* test again

* test again 2

* test again 3

* test again 4

* test again 4

* test again 4

* bun run lint

* test 5

* test 6

* test 7

* test 7

* test 7

* test 7

* test 7

* test 7

* test 8

* test 9

* test 9

* test 9

* test 10

* test 10

* bun run lint, plus github fixed

* removed some debug statements #935

* testing resolver removing

* testing trig

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
2025-08-11 12:50:55 -07:00
Waleed Latif
70aeb0c298 fix(sidebar-ui): fix small ui bug to close gap when creating new workflow (#932) 2025-08-10 18:33:01 -07:00
Emir Karabeg
83f113984d feat(usage-indicator): added ability to see current usage (#925)
* feat(usage-indicator): added ability to see current usage

* feat(billing): added billing ennabled flag for usage indicator, enforcement of billing usage

---------

Co-authored-by: waleedlatif1 <walif6@gmail.com>
2025-08-10 17:20:53 -07:00
Waleed Latif
56ede1c980 improvement(tools): removed transformError, isInternalRoute, directExecution (#928)
* standardized response format for transformError

* removed trasnformError, moved error handling to executeTool for all different error formats

* remove isInternalRoute, make it implicit in executeTool

* removed directExecution, everything on the server nothing on the client

* fix supabase

* fix(tag-dropdown): fix values for parallel & loop blocks (#929)

* fix(search-modal): add parallel and loop blocks to search modal

* reordered tool params

* update docs
2025-08-10 17:19:46 -07:00
Waleed Latif
df16382a19 improvement(subflow): consolidated parallel/loop tags and collaborativeUpdate (#931)
* fix(console): fix typo

* improvement(subflows): consolidated subflows tags
2025-08-10 17:19:21 -07:00
Waleed Latif
e271ed86b6 improvement(console): added iteration info to console entry for parallel/loop (#930) 2025-08-10 16:27:39 -07:00
Waleed Latif
785b86a32e fix(tag-dropdown): fix values for parallel & loop blocks (#929) 2025-08-10 11:55:56 -07:00
Waleed Latif
e5e8082de4 fix(workflow-block): improvements to pulsing effect, active execution state, and running workflow blocks in parallel (#927)
* fix: same child workflow executing in parallel with workflow block

* fixed run button prematurely showing completion before child workflows completed

* prevent child worklfows from touching the activeBlocks & layer logic in the parent executor

* surface child workflow errors to main workfow

* ack PR comments
2025-08-09 16:57:56 -07:00
Waleed Latif
8a08afd733 improvement(control-bar): standardize styling across all control bar buttons (#926) 2025-08-09 12:32:37 -07:00
Vikhyath Mondreti
ebb25469ab fix(apikeys): pinned api key to track API key a workflow is deployed with (#924)
* fix(apikeys): pinned api key to track API key a workflow is deployed with

* remove deprecated behaviour tests
2025-08-09 01:37:27 -05:00
Waleed Latif
a2040322e7 fix(chat): fix chat attachments style in dark mode (#923) 2025-08-08 20:12:30 -07:00
Waleed Latif
a8be7e9fb3 fix(help): fix email for help route (#922) 2025-08-08 20:06:19 -07:00
Waleed Latif
aedf5e70b0 v0.3.22: handle files, trigger mode, email validation, tag dropdown types (#919)
* feat(execution-filesystem): system to pass files between blocks  (#866)

* feat(files): pass files between blocks

* presigned URL for downloads

* Remove latest migration before merge

* starter block file upload wasn't getting logged

* checkpoint in human readable form

* checkpoint files / file type outputs

* file downloads working for block outputs

* checkpoint file download

* fix type issues

* remove filereference interface with simpler user file interface

* show files in the tag dropdown for start block

* more migration to simple url object, reduce presigned time to 5 min

* Remove migration 0065_parallel_nightmare and related files

- Deleted apps/sim/db/migrations/0065_parallel_nightmare.sql
- Deleted apps/sim/db/migrations/meta/0065_snapshot.json
- Removed 0065 entry from apps/sim/db/migrations/meta/_journal.json

Preparing for merge with origin/staging and migration regeneration

* add migration files

* fix tests

* Update apps/sim/lib/uploads/setup.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* cleanup types

* fix lint

* fix logs typing for file refs

* open download in new tab

* fixed

* Update apps/sim/tools/index.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix file block

* cleanup unused code

* fix bugs

* remove hacky file id logic

* fix drag and drop

* fix tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* feat(trigger-mode): added trigger-mode to workflow_blocks table (#902)

* fix(schedules-perms): use regular perm system to view/edit schedule info (#901)

* fix(schedules-perms): use regular perm system to view schedule info

* fix perms

* improve logging

* feat(webhooks): deprecate singular webhook block + add trigger mode to blocks (#903)

* feat(triggers): added new trigger mode for blocks, added socket event, ran migrations

* Rename old trigger/ directory to background/

* cleaned up, ensured that we display active webhook at the block-level

* fix submenu in tag dropdown

* keyboard nav on tag dropdown submenu

* feat(triggers): add outlook to new triggers system

* cleanup

* add types to tag dropdown, type all outputs for tools and use that over block outputs

* update doc generator to truly reflect outputs

* fix docs

* add trigger handler

* fix active webhook tag

* tag dropdown fix for triggers

* remove trigger mode schema change

* feat(execution-filesystem): system to pass files between blocks  (#866)

* feat(files): pass files between blocks

* presigned URL for downloads

* Remove latest migration before merge

* starter block file upload wasn't getting logged

* checkpoint in human readable form

* checkpoint files / file type outputs

* file downloads working for block outputs

* checkpoint file download

* fix type issues

* remove filereference interface with simpler user file interface

* show files in the tag dropdown for start block

* more migration to simple url object, reduce presigned time to 5 min

* Remove migration 0065_parallel_nightmare and related files

- Deleted apps/sim/db/migrations/0065_parallel_nightmare.sql
- Deleted apps/sim/db/migrations/meta/0065_snapshot.json
- Removed 0065 entry from apps/sim/db/migrations/meta/_journal.json

Preparing for merge with origin/staging and migration regeneration

* add migration files

* fix tests

* Update apps/sim/lib/uploads/setup.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* cleanup types

* fix lint

* fix logs typing for file refs

* open download in new tab

* fixed

* Update apps/sim/tools/index.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix file block

* cleanup unused code

* fix bugs

* remove hacky file id logic

* fix drag and drop

* fix tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* feat(trigger-mode): added trigger-mode to workflow_blocks table (#902)

* fix(schedules-perms): use regular perm system to view/edit schedule info (#901)

* fix(schedules-perms): use regular perm system to view schedule info

* fix perms

* improve logging

* cleanup

* prevent tooltip showing up on modal open

* updated trigger config

* fix type issues

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>

* fix(helm): fix helm charts migrations using wrong image (#907)

* fix(helm): fix helm charts migrations using wrong image

* fixed migrations

* feat(whitelist): add email & domain-based whitelisting for signups (#908)

* improvement(helm): fix duplicate SOCKET_SERVER_URL and add additional envvars to template (#909)

* improvement(helm): fix duplicate SOCKET_SERVER_URL and add additional envvars to template

* rm serper & freestyle

* improvement(tag-dropdown): typed tag dropdown values (#910)

* fix(min-chunk): remove minsize for chunk (#911)

* fix(min-chunk): remove minsize for chunk

* fix tests

* improvement(chunk-config): migrate unused default for consistency (#913)

* fix(mailer): update mailer to use the EMAIL_DOMAIN (#914)

* fix(mailer): update mailer to use the EMAIL_DOMAIn

* add more

* Improvement(cc): added cc to gmail and outlook (#900)

* changed just gmail

* bun run lint

* fixed bcc

* updated docs

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: waleedlatif1 <walif6@gmail.com>

* fix(email-validation): add email validation to prevent bouncing, fixed OTP validation (#916)

* feat(email-validation): add email validation to prevent bouncing

* removed suspicious patterns

* fix(verification): fixed OTP verification

* fix failing tests, cleanup

* fix(otp): fix email not sending (#917)

* fix(email): manual OTP instead of better-auth (#921)

* fix(email): manual OTP instead of better-auth

* lint

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
2025-08-08 19:08:30 -07:00
Waleed Latif
503268ebcd fix(email): manual OTP instead of better-auth (#921)
* fix(email): manual OTP instead of better-auth

* lint
2025-08-08 18:49:54 -07:00
Waleed Latif
9a4de1f0c6 fix(otp): fix email not sending (#917) 2025-08-08 17:25:44 -07:00
Waleed Latif
43a3416347 fix(email-validation): add email validation to prevent bouncing, fixed OTP validation (#916)
* feat(email-validation): add email validation to prevent bouncing

* removed suspicious patterns

* fix(verification): fixed OTP verification

* fix failing tests, cleanup
2025-08-08 17:01:41 -07:00
Adam Gough
7f39cd0f23 Improvement(cc): added cc to gmail and outlook (#900)
* changed just gmail

* bun run lint

* fixed bcc

* updated docs

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: waleedlatif1 <walif6@gmail.com>
2025-08-08 14:20:16 -07:00
Waleed Latif
658942deb3 fix(mailer): update mailer to use the EMAIL_DOMAIN (#914)
* fix(mailer): update mailer to use the EMAIL_DOMAIn

* add more
2025-08-08 13:13:52 -07:00
Vikhyath Mondreti
061bd6d5a8 improvement(chunk-config): migrate unused default for consistency (#913) 2025-08-08 12:46:30 -07:00
Vikhyath Mondreti
0ec91f9010 fix(min-chunk): remove minsize for chunk (#911)
* fix(min-chunk): remove minsize for chunk

* fix tests
2025-08-08 12:37:41 -07:00
Waleed Latif
db581dc727 improvement(tag-dropdown): typed tag dropdown values (#910) 2025-08-08 11:34:40 -07:00
Waleed Latif
87e0586d0a improvement(helm): fix duplicate SOCKET_SERVER_URL and add additional envvars to template (#909)
* improvement(helm): fix duplicate SOCKET_SERVER_URL and add additional envvars to template

* rm serper & freestyle
2025-08-08 10:59:34 -07:00
Waleed Latif
9a7c58c8a2 feat(whitelist): add email & domain-based whitelisting for signups (#908) 2025-08-07 23:38:04 -07:00
Waleed Latif
004cd3339d fix(helm): fix helm charts migrations using wrong image (#907)
* fix(helm): fix helm charts migrations using wrong image

* fixed migrations
2025-08-07 23:11:17 -07:00
Waleed Latif
9bd3491eac feat(webhooks): deprecate singular webhook block + add trigger mode to blocks (#903)
* feat(triggers): added new trigger mode for blocks, added socket event, ran migrations

* Rename old trigger/ directory to background/

* cleaned up, ensured that we display active webhook at the block-level

* fix submenu in tag dropdown

* keyboard nav on tag dropdown submenu

* feat(triggers): add outlook to new triggers system

* cleanup

* add types to tag dropdown, type all outputs for tools and use that over block outputs

* update doc generator to truly reflect outputs

* fix docs

* add trigger handler

* fix active webhook tag

* tag dropdown fix for triggers

* remove trigger mode schema change

* feat(execution-filesystem): system to pass files between blocks  (#866)

* feat(files): pass files between blocks

* presigned URL for downloads

* Remove latest migration before merge

* starter block file upload wasn't getting logged

* checkpoint in human readable form

* checkpoint files / file type outputs

* file downloads working for block outputs

* checkpoint file download

* fix type issues

* remove filereference interface with simpler user file interface

* show files in the tag dropdown for start block

* more migration to simple url object, reduce presigned time to 5 min

* Remove migration 0065_parallel_nightmare and related files

- Deleted apps/sim/db/migrations/0065_parallel_nightmare.sql
- Deleted apps/sim/db/migrations/meta/0065_snapshot.json
- Removed 0065 entry from apps/sim/db/migrations/meta/_journal.json

Preparing for merge with origin/staging and migration regeneration

* add migration files

* fix tests

* Update apps/sim/lib/uploads/setup.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* cleanup types

* fix lint

* fix logs typing for file refs

* open download in new tab

* fixed

* Update apps/sim/tools/index.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix file block

* cleanup unused code

* fix bugs

* remove hacky file id logic

* fix drag and drop

* fix tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* feat(trigger-mode): added trigger-mode to workflow_blocks table (#902)

* fix(schedules-perms): use regular perm system to view/edit schedule info (#901)

* fix(schedules-perms): use regular perm system to view schedule info

* fix perms

* improve logging

* cleanup

* prevent tooltip showing up on modal open

* updated trigger config

* fix type issues

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2025-08-07 20:27:54 -07:00
Vikhyath Mondreti
fd3ca87c38 fix(schedules-perms): use regular perm system to view/edit schedule info (#901)
* fix(schedules-perms): use regular perm system to view schedule info

* fix perms

* improve logging
2025-08-07 15:38:09 -07:00
Waleed Latif
d264a6ade8 feat(trigger-mode): added trigger-mode to workflow_blocks table (#902) 2025-08-07 14:59:25 -07:00
Vikhyath Mondreti
de93e167af feat(execution-filesystem): system to pass files between blocks (#866)
* feat(files): pass files between blocks

* presigned URL for downloads

* Remove latest migration before merge

* starter block file upload wasn't getting logged

* checkpoint in human readable form

* checkpoint files / file type outputs

* file downloads working for block outputs

* checkpoint file download

* fix type issues

* remove filereference interface with simpler user file interface

* show files in the tag dropdown for start block

* more migration to simple url object, reduce presigned time to 5 min

* Remove migration 0065_parallel_nightmare and related files

- Deleted apps/sim/db/migrations/0065_parallel_nightmare.sql
- Deleted apps/sim/db/migrations/meta/0065_snapshot.json
- Removed 0065 entry from apps/sim/db/migrations/meta/_journal.json

Preparing for merge with origin/staging and migration regeneration

* add migration files

* fix tests

* Update apps/sim/lib/uploads/setup.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/lib/workflows/execution-file-storage.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* cleanup types

* fix lint

* fix logs typing for file refs

* open download in new tab

* fixed

* Update apps/sim/tools/index.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix file block

* cleanup unused code

* fix bugs

* remove hacky file id logic

* fix drag and drop

* fix tests

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-08-07 12:51:30 -07:00
Waleed Latif
85cdca28f1 v0.3.21: gpt-5, copilot files, configurable rate limits, fix deployed state 2025-08-07 11:32:25 -07:00
Waleed Latif
75963eb851 fix(gpt-5): remove temp, decr socket debounce to 25ms (#898)
* fix(gpt-5): remove temp

* decr debounce for sockets
2025-08-07 11:24:10 -07:00
Waleed Latif
ed7d7a7101 fix(gpt-5): updated pricing (#897) 2025-08-07 10:39:55 -07:00
Waleed Latif
d5c13b72e9 feat(gpt-5): added gpt-5 models (#896)
* feat(gpt-5): added gpt-5

* restore grok-3-fast

* added azure gpt-5
2025-08-07 10:25:49 -07:00
Waleed Latif
e164e32c5b fix(build): fixed build (#893) 2025-08-06 21:48:11 -07:00
Waleed Latif
7461ddf8f7 feat(rate-limits): make rate limits configurable via environment variables (#892)
* feat(rate-limits): make rate limits configurable via environment variables

* add defaults for CI
2025-08-06 20:56:23 -07:00
Siddharth Ganesan
f94258ef83 improvement(copilot): incremental edits (#891)
* v1

* Incremental edits

* Lint

* Remove dev env

* Fix tests

* Lint
2025-08-06 17:14:47 -07:00
Siddharth Ganesan
05e689bc60 fix(agent): export waits for complete workflow state (#889)
* Fix yaml export race condition

* Fix env.ts dev
2025-08-06 11:28:34 -07:00
Adam Gough
a3a5bf1d76 feat(microsoft-tools): added planner, onedrive, and sharepoint (#840)
* first push

* feat: finished onedrive tool

* added refresh

* added sharepoint with create page

* finished sharepoint and onedrive

* planner working

* fixed create task tool

* made read task better

* cleaned up read task

* bun run lint

* cleaned up #840

* greptile changes and clean up

* bun run lint

* fix #840

* added docs #840

* bun run lint #840

* removed unnecessary logic #840

* removed page token #840

* fixed docs and descriptions, added advanced mode #840

* remove unused types, cleaned up a lot, fixed docs

* readded file upload and changed docs

* bun run lint

* added folder name

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: waleedlatif1 <walif6@gmail.com>
2025-08-06 10:27:21 -07:00
Waleed Latif
e43e78fb48 feat(whitelabel): add in the ability to whitelabel via envvars (#887)
* feat(whitelabel): add in the ability to whitelabel via envvars

* restore site.webmanifest

* fix(dynamic): remove force-dynamic from routes that don't need it (#888)

* Reinstall dependencies

* Update docs
2025-08-05 19:01:12 -07:00
Waleed Latif
1b0d304a87 fix(dynamic): remove force-dynamic from routes that don't need it (#888) 2025-08-05 18:44:02 -07:00
Siddharth Ganesan
94368eb1c2 Feat/copilot files (#886)
* Connects to s3

* Checkpoint

* File shows in message

* Make files clickable

* User input image

* Persist thumbnails

* Drag and drop files

* Lint

* Fix isdev

* Dont re-download files on rerender
2025-08-05 17:01:53 -07:00
Vikhyath Mondreti
062e2a2c40 fix(deployed-state): use deployed state for API sync and async execs, deployed state modal visual for enabled/disabled (#885)
* fix(deployments): use deployed state for API sync and async execs

* fix deployed workflow modal visualization for enabled

* fix tests
2025-08-05 15:26:57 -07:00
Waleed Latif
746b87743a feat(ollama): added streaming & tool call support for ollama, updated docs (#884) 2025-08-05 15:04:50 -07:00
Vikhyath Mondreti
9f2ff7e9cd Merge pull request #883 from simstudioai/staging
v0.3.20: KB Tag fixes
2025-08-05 14:07:58 -07:00
Vikhyath Mondreti
be65bf795f fix(kb-tag-slots): finding next slot, create versus edit differentiation (#882)
* fix(kb-tag-slots): finding next slot, create versus edit differentiation

* remove unused test file

* fix lint
2025-08-05 13:58:48 -07:00
623 changed files with 60593 additions and 14306 deletions

View File

@@ -164,10 +164,14 @@ Access the application at [http://localhost:3000/](http://localhost:3000/)
To use local models with Sim:
1. Pull models using our helper script:
1. Install Ollama and pull models:
```bash
./apps/sim/scripts/ollama_docker.sh pull <model_name>
# Install Ollama (if not already installed)
curl -fsSL https://ollama.ai/install.sh | sh
# Pull a model (e.g., gemma3:4b)
ollama pull gemma3:4b
```
2. Start Sim with local model support:
@@ -412,8 +416,8 @@ In addition, you will need to update the registries:
Your tool should export a constant with a naming convention of `{toolName}Tool`. The tool ID should follow the format `{provider}_{tool_name}`. For example:
```typescript:/apps/sim/tools/pinecone/fetch.ts
import { ToolConfig, ToolResponse } from '../types'
import { PineconeParams, PineconeResponse } from './types'
import { ToolConfig, ToolResponse } from '@/tools/types'
import { PineconeParams, PineconeResponse } from '@/tools/pinecone/types'
export const fetchTool: ToolConfig<PineconeParams, PineconeResponse> = {
id: 'pinecone_fetch', // Follow the {provider}_{tool_name} format
@@ -444,9 +448,6 @@ In addition, you will need to update the registries:
transformResponse: async (response: Response) => {
// Transform response
},
transformError: (error) => {
// Handle errors
},
}
```
@@ -454,7 +455,7 @@ In addition, you will need to update the registries:
Update the tools registry in `/apps/sim/tools/index.ts` to include your new tool:
```typescript:/apps/sim/tools/index.ts
import { fetchTool, generateEmbeddingsTool, searchTextTool } from './pinecone'
import { fetchTool, generateEmbeddingsTool, searchTextTool } from '/@tools/pinecone'
// ... other imports
export const tools: Record<string, ToolConfig> = {
@@ -533,7 +534,7 @@ This visibility system ensures clean user interfaces while maintaining full flex
### Guidelines & Best Practices
- **Code Style:** Follow the project's ESLint and Prettier configurations. Use meaningful variable names and small, focused functions.
- **Code Style:** Follow the project's Biome configurations. Use meaningful variable names and small, focused functions.
- **Documentation:** Clearly document the purpose, inputs, outputs, and any special behavior for your block/tool.
- **Error Handling:** Implement robust error handling and provide user-friendly error messages.
- **Parameter Visibility:** Always specify the appropriate visibility level for each parameter to ensure proper UI behavior and LLM integration.

View File

@@ -59,27 +59,21 @@ docker compose -f docker-compose.prod.yml up -d
Access the application at [http://localhost:3000/](http://localhost:3000/)
#### Using Local Models
#### Using Local Models with Ollama
To use local models with Sim:
1. Pull models using our helper script:
Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:
```bash
./apps/sim/scripts/ollama_docker.sh pull <model_name>
# Start with GPU support (automatically downloads gemma3:4b model)
docker compose -f docker-compose.ollama.yml --profile setup up -d
# For CPU-only systems:
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
```
2. Start Sim with local model support:
Wait for the model to download, then visit [http://localhost:3000](http://localhost:3000). Add more models with:
```bash
# With NVIDIA GPU support
docker compose --profile local-gpu -f docker-compose.ollama.yml up -d
# Without GPU (CPU only)
docker compose --profile local-cpu -f docker-compose.ollama.yml up -d
# If hosting on a server, update the environment variables in the docker-compose.prod.yml file to include the server's public IP then start again (OLLAMA_URL to i.e. http://1.1.1.1:11434)
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
```
### Option 3: Dev Containers

View File

@@ -61,7 +61,7 @@ The user prompt represents the primary input data for inference processing. This
The Agent block supports multiple LLM providers through a unified inference interface. Available models include:
**OpenAI Models**: GPT-4o, o1, o3, o4-mini, gpt-4.1 (API-based inference)
**OpenAI Models**: GPT-5, GPT-4o, o1, o3, o4-mini, gpt-4.1 (API-based inference)
**Anthropic Models**: Claude 3.7 Sonnet (API-based inference)
**Google Models**: Gemini 2.5 Pro, Gemini 2.0 Flash (API-based inference)
**Alternative Providers**: Groq, Cerebras, xAI, DeepSeek (API-based inference)

View File

@@ -84,7 +84,7 @@ Different block types produce different output structures. Here's what you can e
```json
{
"content": "Evaluation summary",
"model": "gpt-4o",
"model": "gpt-5",
"tokens": {
"prompt": 120,
"completion": 85,

View File

@@ -71,19 +71,16 @@ Read records from an Airtable table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token |
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID of the table |
| `maxRecords` | number | No | Maximum number of records to return |
| `filterFormula` | string | No | Formula to filter records \(e.g., |
| `filterFormula` | string | No | Formula to filter records \(e.g., "\(\{Field Name\} = \'Value\'\)"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Retrieved record data |
| `record` | json | Single record data |
| `metadata` | json | Operation metadata |
| `records` | json | Array of retrieved Airtable records |
### `airtable_get_record`
@@ -93,7 +90,6 @@ Retrieve a single record from an Airtable table by its ID
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token |
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID or name of the table |
| `recordId` | string | Yes | ID of the record to retrieve |
@@ -102,9 +98,8 @@ Retrieve a single record from an Airtable table by its ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Retrieved record data |
| `record` | json | Single record data |
| `metadata` | json | Operation metadata |
| `record` | json | Retrieved Airtable record with id, createdTime, and fields |
| `metadata` | json | Operation metadata including record count |
### `airtable_create_records`
@@ -114,17 +109,16 @@ Write new records to an Airtable table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token |
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID or name of the table |
| `records` | json | Yes | Array of records to create, each with a `fields` object |
| `fields` | string | No | No description |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Retrieved record data |
| `record` | json | Single record data |
| `metadata` | json | Operation metadata |
| `records` | json | Array of created Airtable records |
### `airtable_update_record`
@@ -134,7 +128,6 @@ Update an existing record in an Airtable table by ID
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token |
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID or name of the table |
| `recordId` | string | Yes | ID of the record to update |
@@ -144,9 +137,8 @@ Update an existing record in an Airtable table by ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Retrieved record data |
| `record` | json | Single record data |
| `metadata` | json | Operation metadata |
| `record` | json | Updated Airtable record with id, createdTime, and fields |
| `metadata` | json | Operation metadata including record count and updated field names |
### `airtable_update_multiple_records`
@@ -156,17 +148,15 @@ Update multiple existing records in an Airtable table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token |
| `baseId` | string | Yes | ID of the Airtable base |
| `tableId` | string | Yes | ID or name of the table |
| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `records` | json | Retrieved record data |
| `record` | json | Single record data |
| `metadata` | json | Operation metadata |
| `records` | json | Array of updated Airtable records |

View File

@@ -71,10 +71,7 @@ Search for academic papers on ArXiv by keywords, authors, titles, or other field
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `papers` | json | Found papers data |
| `totalResults` | number | Total results count |
| `paper` | json | Paper details |
| `authorPapers` | json | Author papers list |
| `papers` | json | Array of papers matching the search query |
### `arxiv_get_paper`
@@ -84,16 +81,13 @@ Get detailed information about a specific ArXiv paper by its ID.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `paperId` | string | Yes | ArXiv paper ID \(e.g., |
| `paperId` | string | Yes | ArXiv paper ID \(e.g., "1706.03762"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `papers` | json | Found papers data |
| `totalResults` | number | Total results count |
| `paper` | json | Paper details |
| `authorPapers` | json | Author papers list |
| `paper` | json | Detailed information about the requested ArXiv paper |
### `arxiv_get_author_papers`
@@ -110,10 +104,7 @@ Search for papers by a specific author on ArXiv.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `papers` | json | Found papers data |
| `totalResults` | number | Total results count |
| `paper` | json | Paper details |
| `authorPapers` | json | Author papers list |
| `authorPapers` | json | Array of papers authored by the specified author |

View File

@@ -73,6 +73,7 @@ Runs a browser automation task using BrowserUse
| --------- | ---- | -------- | ----------- |
| `task` | string | Yes | What should the browser agent do |
| `variables` | json | No | Optional variables to use as secrets \(format: \{key: value\}\) |
| `format` | string | No | No description |
| `save_browser_data` | boolean | No | Whether to save browser data |
| `model` | string | No | LLM model to use \(default: gpt-4o\) |
| `apiKey` | string | Yes | API key for BrowserUse API |
@@ -83,7 +84,7 @@ Runs a browser automation task using BrowserUse
| --------- | ---- | ----------- |
| `id` | string | Task execution identifier |
| `success` | boolean | Task completion status |
| `output` | any | Task output data |
| `output` | json | Task output data |
| `steps` | json | Execution steps taken |

View File

@@ -220,7 +220,8 @@ Populate Clay with data from a JSON file. Enables direct communication and notif
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | any | Response data |
| `success` | boolean | Operation success status |
| `output` | json | Clay populate operation results including response data from Clay webhook |

View File

@@ -57,7 +57,6 @@ Retrieve content from Confluence pages using the Confluence API.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Confluence |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to retrieve |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
@@ -66,11 +65,10 @@ Retrieve content from Confluence pages using the Confluence API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `pageId` | string | Page identifier |
| `content` | string | Page content |
| `ts` | string | Timestamp of retrieval |
| `pageId` | string | Confluence page ID |
| `content` | string | Page content with HTML tags stripped |
| `title` | string | Page title |
| `success` | boolean | Operation success status |
### `confluence_update`
@@ -80,7 +78,6 @@ Update a Confluence page using the Confluence API.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Confluence |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to update |
| `title` | string | No | New title for the page |
@@ -92,11 +89,10 @@ Update a Confluence page using the Confluence API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `pageId` | string | Page identifier |
| `content` | string | Page content |
| `title` | string | Page title |
| `success` | boolean | Operation success status |
| `ts` | string | Timestamp of update |
| `pageId` | string | Confluence page ID |
| `title` | string | Updated page title |
| `success` | boolean | Update operation success status |

View File

@@ -80,8 +80,8 @@ Send a message to a Discord channel
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Message content |
| `data` | any | Response data |
| `message` | string | Success or error message |
| `data` | object | Discord message data |
### `discord_get_messages`
@@ -99,8 +99,8 @@ Retrieve messages from a Discord channel
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Message content |
| `data` | any | Response data |
| `message` | string | Success or error message |
| `messages` | array | Array of Discord messages with full metadata |
### `discord_get_server`
@@ -117,8 +117,8 @@ Retrieve information about a Discord server (guild)
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Message content |
| `data` | any | Response data |
| `message` | string | Success or error message |
| `data` | object | Discord server \(guild\) information |
### `discord_get_user`
@@ -135,8 +135,8 @@ Retrieve information about a Discord user
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Message content |
| `data` | any | Response data |
| `message` | string | Success or error message |
| `data` | object | Discord user information |

View File

@@ -62,7 +62,7 @@ Convert TTS using ElevenLabs voices
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `audioUrl` | string | Generated audio URL |
| `audioUrl` | string | The URL of the generated audio |

View File

@@ -68,11 +68,7 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs,
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `similarLinks` | json | Similar links found |
| `answer` | string | Generated answer |
| `citations` | json | Answer citations |
| `research` | json | Research findings |
| `results` | array | Search results with titles, URLs, and text snippets |
### `exa_get_contents`
@@ -91,11 +87,7 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content,
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `similarLinks` | json | Similar links found |
| `answer` | string | Generated answer |
| `citations` | json | Answer citations |
| `research` | json | Research findings |
| `results` | array | Retrieved content from URLs with title, text, and summaries |
### `exa_find_similar_links`
@@ -114,11 +106,7 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `similarLinks` | json | Similar links found |
| `answer` | string | Generated answer |
| `citations` | json | Answer citations |
| `research` | json | Research findings |
| `similarLinks` | array | Similar links found with titles, URLs, and text snippets |
### `exa_answer`
@@ -136,11 +124,8 @@ Get an AI-generated answer to a question with citations from the web using Exa A
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `similarLinks` | json | Similar links found |
| `answer` | string | Generated answer |
| `citations` | json | Answer citations |
| `research` | json | Research findings |
| `answer` | string | AI-generated answer to the question |
| `citations` | array | Sources and citations for the answer |
### `exa_research`
@@ -158,11 +143,7 @@ Perform comprehensive research using AI to generate detailed reports with citati
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `similarLinks` | json | Similar links found |
| `answer` | string | Generated answer |
| `citations` | json | Answer citations |
| `research` | json | Research findings |
| `research` | array | Comprehensive research findings with citations and summaries |

View File

@@ -50,7 +50,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
## Usage Instructions
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). Upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). You can either provide a URL to a file or upload files directly. Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
@@ -71,8 +71,8 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | json | Parsed file data |
| `combinedContent` | string | Combined file content |
| `files` | array | Array of parsed files |
| `combinedContent` | string | Combined content of all parsed files |

View File

@@ -81,14 +81,9 @@ Extract structured content from web pages with comprehensive metadata support. C
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `markdown` | string | Page content markdown |
| `html` | any | Raw HTML content |
| `metadata` | json | Page metadata |
| `data` | json | Search results data |
| `warning` | any | Warning messages |
| `pages` | json | Crawled pages data |
| `total` | number | Total pages found |
| `creditsUsed` | number | Credits consumed |
| `markdown` | string | Page content in markdown format |
| `html` | string | Raw HTML content of the page |
| `metadata` | object | Page metadata including SEO and Open Graph information |
### `firecrawl_search`
@@ -105,14 +100,7 @@ Search for information on the web using Firecrawl
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `markdown` | string | Page content markdown |
| `html` | any | Raw HTML content |
| `metadata` | json | Page metadata |
| `data` | json | Search results data |
| `warning` | any | Warning messages |
| `pages` | json | Crawled pages data |
| `total` | number | Total pages found |
| `creditsUsed` | number | Credits consumed |
| `data` | array | Search results data |
### `firecrawl_crawl`
@@ -131,14 +119,7 @@ Crawl entire websites and extract structured content from all accessible pages
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `markdown` | string | Page content markdown |
| `html` | any | Raw HTML content |
| `metadata` | json | Page metadata |
| `data` | json | Search results data |
| `warning` | any | Warning messages |
| `pages` | json | Crawled pages data |
| `total` | number | Total pages found |
| `creditsUsed` | number | Credits consumed |
| `pages` | array | Array of crawled pages with their content and metadata |

View File

@@ -0,0 +1,32 @@
---
title: Webhook
description: Receive webhooks from any service
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="generic_webhook"
color="#10B981"
icon={true}
iconSvg={`<svg className="block-icon"
fill='currentColor'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M17.974 7A4.967 4.967 0 0 0 18 6.5a5.5 5.5 0 1 0-8.672 4.491L7.18 15.114A2.428 2.428 0 0 0 6.496 15 2.5 2.5 0 1 0 9 17.496a2.36 2.36 0 0 0-.93-1.925l2.576-4.943-.41-.241A4.5 4.5 0 1 1 17 6.5a4.8 4.8 0 0 1-.022.452zM6.503 18.999a1.5 1.5 0 1 1 1.496-1.503A1.518 1.518 0 0 1 6.503 19zM18.5 12a5.735 5.735 0 0 0-1.453.157l-2.744-3.941A2.414 2.414 0 0 0 15 6.5a2.544 2.544 0 1 0-1.518 2.284l3.17 4.557.36-.13A4.267 4.267 0 0 1 18.5 13a4.5 4.5 0 1 1-.008 9h-.006a4.684 4.684 0 0 1-3.12-1.355l-.703.71A5.653 5.653 0 0 0 18.49 23h.011a5.5 5.5 0 0 0 0-11zM11 6.5A1.5 1.5 0 1 1 12.5 8 1.509 1.509 0 0 1 11 6.5zM18.5 20a2.5 2.5 0 1 0-2.447-3h-5.05l-.003.497A4.546 4.546 0 0 1 6.5 22 4.526 4.526 0 0 1 2 17.5a4.596 4.596 0 0 1 3.148-4.37l-.296-.954A5.606 5.606 0 0 0 1 17.5 5.532 5.532 0 0 0 6.5 23a5.573 5.573 0 0 0 5.478-5h4.08a2.487 2.487 0 0 0 2.442 2zm0-4a1.5 1.5 0 1 1-1.5 1.5 1.509 1.509 0 0 1 1.5-1.5z' />
<path fill='none' d='M0 0h24v24H0z' />
</svg>`}
/>
## Notes
- Category: `triggers`
- Type: `generic_webhook`

View File

@@ -1,6 +1,6 @@
---
title: GitHub
description: Interact with GitHub
description: Interact with GitHub or trigger workflows from GitHub events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -35,7 +35,7 @@ In Sim, the GitHub integration enables your agents to interact directly with Git
## Usage Instructions
Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow.
Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow. Trigger workflows from GitHub events like push, pull requests, and issues.
@@ -58,8 +58,8 @@ Fetch PR details including diff and files changed
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Response metadata |
| `content` | string | Human-readable PR summary |
| `metadata` | object | Detailed PR metadata including file changes |
### `github_comment`
@@ -85,8 +85,8 @@ Create comments on GitHub PRs
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Response metadata |
| `content` | string | Human-readable comment confirmation |
| `metadata` | object | Comment metadata |
### `github_repo_info`
@@ -104,8 +104,8 @@ Retrieve comprehensive GitHub repository metadata including stars, forks, issues
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Response metadata |
| `content` | string | Human-readable repository summary |
| `metadata` | object | Repository metadata |
### `github_latest_commit`
@@ -117,15 +117,15 @@ Retrieve the latest commit from a GitHub repository
| --------- | ---- | -------- | ----------- |
| `owner` | string | Yes | Repository owner \(user or organization\) |
| `repo` | string | Yes | Repository name |
| `branch` | string | No | Branch name \(defaults to the repository |
| `branch` | string | No | Branch name \(defaults to the repository's default branch\) |
| `apiKey` | string | Yes | GitHub API token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Response metadata |
| `content` | string | Human-readable commit summary |
| `metadata` | object | Commit metadata |

View File

@@ -1,6 +1,6 @@
---
title: Gmail
description: Send Gmail
description: Send Gmail or trigger workflows from Gmail events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -51,7 +51,7 @@ In Sim, the Gmail integration enables your agents to send, read, and search emai
## Usage Instructions
Integrate Gmail functionality to send email messages within your workflow. Automate email communications and process email content using OAuth authentication.
Comprehensive Gmail integration with OAuth authentication. Send email messages, read email content, and trigger workflows from Gmail events like new emails and label changes.
@@ -65,17 +65,18 @@ Send emails using Gmail
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Gmail API |
| `to` | string | Yes | Recipient email address |
| `subject` | string | Yes | Email subject |
| `body` | string | Yes | Email body content |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Email metadata |
| `content` | string | Success message |
| `metadata` | object | Email metadata |
### `gmail_draft`
@@ -85,17 +86,18 @@ Draft emails using Gmail
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Gmail API |
| `to` | string | Yes | Recipient email address |
| `subject` | string | Yes | Email subject |
| `body` | string | Yes | Email body content |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Email metadata |
| `content` | string | Success message |
| `metadata` | object | Draft metadata |
### `gmail_read`
@@ -105,18 +107,19 @@ Read emails from Gmail
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Gmail API |
| `messageId` | string | No | ID of the message to read |
| `folder` | string | No | Folder/label to read emails from |
| `unreadOnly` | boolean | No | Only retrieve unread messages |
| `maxResults` | number | No | Maximum number of messages to retrieve \(default: 1, max: 10\) |
| `includeAttachments` | boolean | No | Download and include email attachments |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Email metadata |
| `content` | string | Text content of the email |
| `metadata` | json | Metadata of the email |
| `attachments` | file[] | Attachments of the email |
### `gmail_search`
@@ -126,7 +129,6 @@ Search emails in Gmail
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Gmail API |
| `query` | string | Yes | Search query for emails |
| `maxResults` | number | No | Maximum number of results to return |
@@ -134,8 +136,8 @@ Search emails in Gmail
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Response content |
| `metadata` | json | Email metadata |
| `content` | string | Search results summary |
| `metadata` | object | Search metadata |

View File

@@ -104,7 +104,6 @@ Create a new event in Google Calendar
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `summary` | string | Yes | Event title/summary |
| `description` | string | No | Event description |
@@ -119,8 +118,8 @@ Create a new event in Google Calendar
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Operation response content |
| `metadata` | json | Event metadata |
| `content` | string | Event creation confirmation message |
| `metadata` | json | Created event metadata including ID, status, and details |
### `google_calendar_list`
@@ -130,7 +129,6 @@ List events from Google Calendar
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
| `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
@@ -141,8 +139,8 @@ List events from Google Calendar
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Operation response content |
| `metadata` | json | Event metadata |
| `content` | string | Summary of found events count |
| `metadata` | json | List of events with pagination tokens and event details |
### `google_calendar_get`
@@ -152,7 +150,6 @@ Get a specific event from Google Calendar
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `eventId` | string | Yes | Event ID to retrieve |
@@ -160,8 +157,8 @@ Get a specific event from Google Calendar
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Operation response content |
| `metadata` | json | Event metadata |
| `content` | string | Event retrieval confirmation message |
| `metadata` | json | Event details including ID, status, times, and attendees |
### `google_calendar_quick_add`
@@ -171,9 +168,8 @@ Create events from natural language text
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `text` | string | Yes | Natural language text describing the event \(e.g., |
| `text` | string | Yes | Natural language text describing the event \(e.g., "Meeting with John tomorrow at 3pm"\) |
| `attendees` | array | No | Array of attendee email addresses \(comma-separated string also accepted\) |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
@@ -181,8 +177,8 @@ Create events from natural language text
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Operation response content |
| `metadata` | json | Event metadata |
| `content` | string | Event creation confirmation message from natural language |
| `metadata` | json | Created event metadata including parsed details |
### `google_calendar_invite`
@@ -192,7 +188,6 @@ Invite attendees to an existing Google Calendar event
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `eventId` | string | Yes | Event ID to invite attendees to |
| `attendees` | array | Yes | Array of attendee email addresses to invite |
@@ -203,8 +198,8 @@ Invite attendees to an existing Google Calendar event
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Operation response content |
| `metadata` | json | Event metadata |
| `content` | string | Attendee invitation confirmation message with email delivery status |
| `metadata` | json | Updated event metadata including attendee list and details |

View File

@@ -95,16 +95,14 @@ Read content from a Google Docs document
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Docs API |
| `documentId` | string | Yes | The ID of the document to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Document content |
| `metadata` | json | Document metadata |
| `updatedContent` | boolean | Content update status |
| `content` | string | Extracted document text content |
| `metadata` | json | Document metadata including ID, title, and URL |
### `google_docs_write`
@@ -114,7 +112,6 @@ Write or update content in a Google Docs document
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Docs API |
| `documentId` | string | Yes | The ID of the document to write to |
| `content` | string | Yes | The content to write to the document |
@@ -122,9 +119,8 @@ Write or update content in a Google Docs document
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Document content |
| `metadata` | json | Document metadata |
| `updatedContent` | boolean | Content update status |
| `updatedContent` | boolean | Indicates if document content was updated successfully |
| `metadata` | json | Updated document metadata including ID, title, and URL |
### `google_docs_create`
@@ -134,7 +130,6 @@ Create a new Google Docs document
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Docs API |
| `title` | string | Yes | The title of the document to create |
| `content` | string | No | The content of the document to create |
| `folderSelector` | string | No | Select the folder to create the document in |
@@ -144,9 +139,7 @@ Create a new Google Docs document
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Document content |
| `metadata` | json | Document metadata |
| `updatedContent` | boolean | Content update status |
| `metadata` | json | Created document metadata including ID, title, and URL |

View File

@@ -87,7 +87,6 @@ Upload a file to Google Drive
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Drive API |
| `fileName` | string | Yes | The name of the file to upload |
| `content` | string | Yes | The content of the file to upload |
| `mimeType` | string | No | The MIME type of the file to upload |
@@ -98,8 +97,7 @@ Upload a file to Google Drive
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | File data |
| `files` | json | Files list |
| `file` | json | Uploaded file metadata including ID, name, and links |
### `google_drive_create_folder`
@@ -109,7 +107,6 @@ Create a new folder in Google Drive
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Drive API |
| `fileName` | string | Yes | Name of the folder to create |
| `folderSelector` | string | No | Select the parent folder to create the folder in |
| `folderId` | string | No | ID of the parent folder \(internal use\) |
@@ -118,8 +115,7 @@ Create a new folder in Google Drive
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | File data |
| `files` | json | Files list |
| `file` | json | Created folder metadata including ID, name, and parent information |
### `google_drive_list`
@@ -129,7 +125,6 @@ List files and folders in Google Drive
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Drive API |
| `folderSelector` | string | No | Select the folder to list files from |
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
| `query` | string | No | A query to filter the files |
@@ -140,8 +135,7 @@ List files and folders in Google Drive
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | File data |
| `files` | json | Files list |
| `files` | json | Array of file metadata objects from the specified folder |

View File

@@ -81,8 +81,7 @@ Search the web with the Custom Search API
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | json | Search result items |
| `searchInformation` | json | Search metadata |
| `items` | array | Array of search results from Google |

View File

@@ -110,7 +110,6 @@ Read data from a Google Sheets spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Sheets API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from |
| `range` | string | No | The range of cells to read from |
@@ -118,13 +117,8 @@ Read data from a Google Sheets spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `tableRange` | string | Table range |
| `data` | json | Sheet data including range and cell values |
| `metadata` | json | Spreadsheet metadata including ID and URL |
### `google_sheets_write`
@@ -134,7 +128,6 @@ Write data to a Google Sheets spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Sheets API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to |
| `range` | string | No | The range of cells to write to |
| `values` | array | Yes | The data to write to the spreadsheet |
@@ -145,13 +138,11 @@ Write data to a Google Sheets spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `tableRange` | string | Table range |
| `updatedRange` | string | Range of cells that were updated |
| `updatedRows` | number | Number of rows updated |
| `updatedColumns` | number | Number of columns updated |
| `updatedCells` | number | Number of cells updated |
| `metadata` | json | Spreadsheet metadata including ID and URL |
### `google_sheets_update`
@@ -161,7 +152,6 @@ Update data in a Google Sheets spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Sheets API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to update |
| `range` | string | No | The range of cells to update |
| `values` | array | Yes | The data to update in the spreadsheet |
@@ -172,13 +162,11 @@ Update data in a Google Sheets spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `tableRange` | string | Table range |
| `updatedRange` | string | Range of cells that were updated |
| `updatedRows` | number | Number of rows updated |
| `updatedColumns` | number | Number of columns updated |
| `updatedCells` | number | Number of cells updated |
| `metadata` | json | Spreadsheet metadata including ID and URL |
### `google_sheets_append`
@@ -188,7 +176,6 @@ Append data to the end of a Google Sheets spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Google Sheets API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to |
| `range` | string | No | The range of cells to append after |
| `values` | array | Yes | The data to append to the spreadsheet |
@@ -200,13 +187,12 @@ Append data to the end of a Google Sheets spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `tableRange` | string | Table range |
| `tableRange` | string | Range of the table where data was appended |
| `updatedRange` | string | Range of cells that were updated |
| `updatedRows` | number | Number of rows updated |
| `updatedColumns` | number | Number of columns updated |
| `updatedCells` | number | Number of cells updated |
| `metadata` | json | Spreadsheet metadata including ID and URL |

View File

@@ -92,9 +92,8 @@ Generate completions using Hugging Face Inference API
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Generated response |
| `model` | string | Model used |
| `usage` | json | Token usage stats |
| `success` | boolean | Operation success status |
| `output` | object | Chat completion results |

View File

@@ -38,6 +38,7 @@ With Hunter.io, you can:
In Sim, the Hunter.io integration enables your agents to programmatically search for and verify email addresses, discover companies, and enrich contact data using Hunter.ios API. This allows you to automate lead generation, contact enrichment, and email verification directly within your workflows. Your agents can leverage Hunter.ios tools to streamline outreach, keep your CRM up-to-date, and power intelligent automation scenarios for sales, recruiting, and more.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Search for email addresses, verify their deliverability, discover companies, and enrich contact data using Hunter.io's powerful email finding capabilities.
@@ -56,7 +57,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
| --------- | ---- | -------- | ----------- |
| `query` | string | No | Natural language search query for companies |
| `domain` | string | No | Company domain names to filter by |
| `headcount` | string | No | Company size filter \(e.g., |
| `headcount` | string | No | Company size filter \(e.g., "1-10", "11-50"\) |
| `company_type` | string | No | Type of organization |
| `technology` | string | No | Technology used by companies |
| `apiKey` | string | Yes | Hunter.io API Key |
@@ -65,15 +66,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `results` | array | Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count |
### `hunter_domain_search`
@@ -95,15 +88,26 @@ Returns all the email addresses found using one given domain name, with sources.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `domain` | string | The searched domain name |
| `disposable` | boolean | Whether the domain accepts disposable email addresses |
| `webmail` | boolean | Whether the domain is a webmail provider |
| `accept_all` | boolean | Whether the domain accepts all email addresses |
| `pattern` | string | The email pattern used by the organization |
| `organization` | string | The organization name |
| `description` | string | Description of the organization |
| `industry` | string | Industry of the organization |
| `twitter` | string | Twitter profile of the organization |
| `facebook` | string | Facebook profile of the organization |
| `linkedin` | string | LinkedIn profile of the organization |
| `instagram` | string | Instagram profile of the organization |
| `youtube` | string | YouTube channel of the organization |
| `technologies` | array | Array of technologies used by the organization |
| `country` | string | Country where the organization is located |
| `state` | string | State where the organization is located |
| `city` | string | City where the organization is located |
| `postal_code` | string | Postal code of the organization |
| `street` | string | Street address of the organization |
| `emails` | array | Array of email addresses found for the domain, each containing value, type, confidence, sources, and person details |
### `hunter_email_finder`
@@ -114,8 +118,8 @@ Finds the most likely email address for a person given their name and company do
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Company domain name |
| `first_name` | string | Yes | Person |
| `last_name` | string | Yes | Person |
| `first_name` | string | Yes | Person's first name |
| `last_name` | string | Yes | Person's last name |
| `company` | string | No | Company name |
| `apiKey` | string | Yes | Hunter.io API Key |
@@ -123,15 +127,10 @@ Finds the most likely email address for a person given their name and company do
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `email` | string | The found email address |
| `score` | number | Confidence score for the found email address |
| `sources` | array | Array of sources where the email was found, each containing domain, uri, extracted_on, last_seen_on, and still_on_page |
| `verification` | object | Verification information containing date and status |
### `hunter_email_verifier`
@@ -148,15 +147,20 @@ Verifies the deliverability of an email address and provides detailed verificati
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `result` | string | Deliverability result: deliverable, undeliverable, or risky |
| `score` | number | Confidence score for the verification result |
| `email` | string | The verified email address |
| `regexp` | boolean | Whether the email follows a valid regex pattern |
| `gibberish` | boolean | Whether the email appears to be gibberish |
| `disposable` | boolean | Whether the email is from a disposable email provider |
| `webmail` | boolean | Whether the email is from a webmail provider |
| `mx_records` | boolean | Whether MX records exist for the domain |
| `smtp_server` | boolean | Whether the SMTP server is reachable |
| `smtp_check` | boolean | Whether the SMTP check was successful |
| `accept_all` | boolean | Whether the domain accepts all email addresses |
| `block` | boolean | Whether the email is blocked |
| `status` | string | Verification status: valid, invalid, accept_all, webmail, disposable, or unknown |
| `sources` | array | Array of sources where the email was found |
### `hunter_companies_find`
@@ -173,15 +177,8 @@ Enriches company data using domain name.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `person` | object | Person information \(undefined for companies_find tool\) |
| `company` | object | Company information including name, domain, industry, size, country, linkedin, and twitter |
### `hunter_email_count`
@@ -200,15 +197,11 @@ Returns the total number of email addresses found for a domain or company.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `emails` | json | Email addresses found |
| `email` | string | Found email address |
| `score` | number | Confidence score |
| `result` | string | Verification result |
| `status` | string | Status message |
| `total` | number | Total results count |
| `personal_emails` | number | Personal emails count |
| `generic_emails` | number | Generic emails count |
| `total` | number | Total number of email addresses found |
| `personal_emails` | number | Number of personal email addresses found |
| `generic_emails` | number | Number of generic email addresses found |
| `department` | object | Breakdown of email addresses by department \(executive, it, finance, management, sales, legal, support, hr, marketing, communication\) |
| `seniority` | object | Breakdown of email addresses by seniority level \(junior, senior, executive\) |

View File

@@ -73,9 +73,8 @@ Generate images using OpenAI
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Generation response |
| `image` | string | Generated image URL |
| `metadata` | json | Generation metadata |
| `success` | boolean | Operation success status |
| `output` | object | Generated image data |

View File

@@ -87,7 +87,7 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Extracted content |
| `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text |

View File

@@ -57,7 +57,6 @@ Retrieve detailed information about a specific Jira issue
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Jira |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `projectId` | string | No | Jira project ID to retrieve issues from. If not provided, all issues will be retrieved. |
| `issueKey` | string | Yes | Jira issue key to retrieve \(e.g., PROJ-123\) |
@@ -67,14 +66,8 @@ Retrieve detailed information about a specific Jira issue
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `issueKey` | string | Issue key |
| `summary` | string | Issue summary |
| `description` | string | Issue description |
| `created` | string | Creation date |
| `updated` | string | Update date |
| `success` | boolean | Operation success |
| `url` | string | Issue URL |
| `success` | boolean | Operation success status |
| `output` | object | Jira issue details with issue key, summary, description, created and updated timestamps |
### `jira_update`
@@ -84,7 +77,6 @@ Update a Jira issue
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Jira |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `projectId` | string | No | Jira project ID to update issues in. If not provided, all issues will be retrieved. |
| `issueKey` | string | Yes | Jira issue key to update |
@@ -99,14 +91,8 @@ Update a Jira issue
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `issueKey` | string | Issue key |
| `summary` | string | Issue summary |
| `description` | string | Issue description |
| `created` | string | Creation date |
| `updated` | string | Update date |
| `success` | boolean | Operation success |
| `url` | string | Issue URL |
| `success` | boolean | Operation success status |
| `output` | object | Updated Jira issue details with timestamp, issue key, summary, and success status |
### `jira_write`
@@ -116,7 +102,6 @@ Write a Jira issue
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Jira |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `projectId` | string | Yes | Project ID for the issue |
| `summary` | string | Yes | Summary for the issue |
@@ -130,14 +115,8 @@ Write a Jira issue
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `issueKey` | string | Issue key |
| `summary` | string | Issue summary |
| `description` | string | Issue description |
| `created` | string | Creation date |
| `updated` | string | Update date |
| `success` | boolean | Operation success |
| `url` | string | Issue URL |
| `success` | boolean | Operation success status |
| `output` | object | Created Jira issue details with timestamp, issue key, summary, success status, and URL |
### `jira_bulk_read`
@@ -147,7 +126,6 @@ Retrieve multiple Jira issues in bulk
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Jira |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `projectId` | string | Yes | Jira project ID |
| `cloudId` | string | No | Jira cloud ID |
@@ -156,14 +134,8 @@ Retrieve multiple Jira issues in bulk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp |
| `issueKey` | string | Issue key |
| `summary` | string | Issue summary |
| `description` | string | Issue description |
| `created` | string | Creation date |
| `updated` | string | Update date |
| `success` | boolean | Operation success |
| `url` | string | Issue URL |
| `success` | boolean | Operation success status |
| `output` | array | Array of Jira issues with summary, description, created and updated timestamps |

View File

@@ -64,7 +64,7 @@ Search for similar content in a knowledge base using vector similarity
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `knowledgeBaseId` | string | Yes | ID of the knowledge base to search in |
| `query` | string | Yes | Search query text |
| `query` | string | No | Search query text \(optional when using tag filters\) |
| `topK` | number | No | Number of most similar results to return \(1-100\) |
| `tagFilters` | any | No | Array of tag filters with tagName and tagValue properties |
@@ -72,9 +72,7 @@ Search for similar content in a knowledge base using vector similarity
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `query` | string | Query used |
| `totalResults` | number | Total results count |
| `results` | array | Array of search results from the knowledge base |
### `knowledge_upload_chunk`
@@ -92,9 +90,7 @@ Upload a new chunk to a document in a knowledge base
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `query` | string | Query used |
| `totalResults` | number | Total results count |
| `data` | object | Information about the uploaded chunk |
### `knowledge_create_document`
@@ -120,9 +116,7 @@ Create a new document in a knowledge base
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results |
| `query` | string | Query used |
| `totalResults` | number | Total results count |
| `data` | object | Information about the created document |

View File

@@ -63,8 +63,7 @@ Fetch and filter issues from Linear
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `issues` | json | Issues list |
| `issue` | json | Single issue data |
| `issues` | array | Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId |
### `linear_create_issue`
@@ -83,8 +82,7 @@ Create a new issue in Linear
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `issues` | json | Issues list |
| `issue` | json | Single issue data |
| `issue` | object | The created issue containing id, title, description, state, teamId, and projectId |

View File

@@ -58,16 +58,16 @@ Search the web for information using Linkup
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `q` | string | Yes | The search query |
| `depth` | string | Yes | Search depth \(has to either be |
| `outputType` | string | Yes | Type of output to return \(has to either be |
| `depth` | string | Yes | Search depth \(has to either be "standard" or "deep"\) |
| `outputType` | string | Yes | Type of output to return \(has to either be "sourcedAnswer" or "searchResults"\) |
| `apiKey` | string | Yes | Enter your Linkup API key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `answer` | string | Generated answer |
| `sources` | json | Source references |
| `answer` | string | The sourced answer to the search query |
| `sources` | array | Array of sources used to compile the answer, each containing name, url, and snippet |

View File

@@ -66,9 +66,8 @@ Add memories to Mem0 for persistent storage and retrieval
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ids` | any | Memory identifiers |
| `memories` | any | Memory data |
| `searchResults` | any | Search results |
| `ids` | array | Array of memory IDs that were created |
| `memories` | array | Array of memory objects that were created |
### `mem0_search_memories`
@@ -87,9 +86,8 @@ Search for memories in Mem0 using semantic search
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ids` | any | Memory identifiers |
| `memories` | any | Memory data |
| `searchResults` | any | Search results |
| `searchResults` | array | Array of search results with memory data, each containing id, data, and score |
| `ids` | array | Array of memory IDs found in the search results |
### `mem0_get_memories`
@@ -110,9 +108,8 @@ Retrieve memories from Mem0 by ID or filter criteria
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ids` | any | Memory identifiers |
| `memories` | any | Memory data |
| `searchResults` | any | Search results |
| `memories` | array | Array of retrieved memory objects |
| `ids` | array | Array of memory IDs that were retrieved |

View File

@@ -57,8 +57,9 @@ Add a new memory to the database or append to existing memory with the same ID.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `memories` | any | Memory data |
| `id` | string | Memory identifier |
| `success` | boolean | Whether the memory was added successfully |
| `memories` | array | Array of memory objects including the new or updated memory |
| `error` | string | Error message if operation failed |
### `memory_get`
@@ -74,8 +75,10 @@ Retrieve a specific memory by its ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `memories` | any | Memory data |
| `id` | string | Memory identifier |
| `success` | boolean | Whether the memory was retrieved successfully |
| `memories` | array | Array of memory data for the requested ID |
| `message` | string | Success or error message |
| `error` | string | Error message if operation failed |
### `memory_get_all`
@@ -90,8 +93,10 @@ Retrieve all memories from the database
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `memories` | any | Memory data |
| `id` | string | Memory identifier |
| `success` | boolean | Whether all memories were retrieved successfully |
| `memories` | array | Array of all memory objects with keys, types, and data |
| `message` | string | Success or error message |
| `error` | string | Error message if operation failed |
### `memory_delete`
@@ -107,8 +112,9 @@ Delete a specific memory by its ID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `memories` | any | Memory data |
| `id` | string | Memory identifier |
| `success` | boolean | Whether the memory was deleted successfully |
| `message` | string | Success or error message |
| `error` | string | Error message if operation failed |

View File

@@ -11,6 +11,7 @@
"exa",
"file",
"firecrawl",
"generic_webhook",
"github",
"gmail",
"google_calendar",
@@ -29,9 +30,11 @@
"mem0",
"memory",
"microsoft_excel",
"microsoft_planner",
"microsoft_teams",
"mistral_parse",
"notion",
"onedrive",
"openai",
"outlook",
"perplexity",
@@ -41,6 +44,7 @@
"s3",
"schedule",
"serper",
"sharepoint",
"slack",
"stagehand",
"stagehand_agent",

View File

@@ -108,7 +108,6 @@ Read data from a Microsoft Excel spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from |
| `range` | string | No | The range of cells to read from |
@@ -116,14 +115,8 @@ Read data from a Microsoft Excel spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `index` | number | Row index |
| `values` | json | Table values |
| `success` | boolean | Operation success status |
| `output` | object | Excel spreadsheet data and metadata |
### `microsoft_excel_write`
@@ -133,7 +126,6 @@ Write data to a Microsoft Excel spreadsheet
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to |
| `range` | string | No | The range of cells to write to |
| `values` | array | Yes | The data to write to the spreadsheet |
@@ -144,14 +136,8 @@ Write data to a Microsoft Excel spreadsheet
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `index` | number | Row index |
| `values` | json | Table values |
| `success` | boolean | Operation success status |
| `output` | object | Write operation results and metadata |
### `microsoft_excel_table_add`
@@ -161,7 +147,6 @@ Add new rows to a Microsoft Excel table
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
| `spreadsheetId` | string | Yes | The ID of the spreadsheet containing the table |
| `tableName` | string | Yes | The name of the table to add rows to |
| `values` | array | Yes | The data to add to the table \(array of arrays or array of objects\) |
@@ -170,14 +155,8 @@ Add new rows to a Microsoft Excel table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Sheet data |
| `metadata` | json | Operation metadata |
| `updatedRange` | string | Updated range |
| `updatedRows` | number | Updated rows count |
| `updatedColumns` | number | Updated columns count |
| `updatedCells` | number | Updated cells count |
| `index` | number | Row index |
| `values` | json | Table values |
| `success` | boolean | Operation success status |
| `output` | object | Table add operation results and metadata |

View File

@@ -0,0 +1,178 @@
---
title: Microsoft Planner
description: Read and create tasks in Microsoft Planner
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="microsoft_planner"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon" fill='currentColor' viewBox='-1 -1 27 27' xmlns='http://www.w3.org/2000/svg'>
<defs>
<linearGradient
id='paint0_linear_3984_11038'
x1='6.38724'
y1='3.74167'
x2='2.15779'
y2='12.777'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#8752E0' />
<stop offset='1' stopColor='#541278' />
</linearGradient>
<linearGradient
id='paint1_linear_3984_11038'
x1='8.38032'
y1='11.0696'
x2='4.94062'
y2='7.69244'
gradientUnits='userSpaceOnUse'
>
<stop offset='0.12172' stopColor='#3D0D59' />
<stop offset='1' stopColor='#7034B0' stopOpacity='0' />
</linearGradient>
<linearGradient
id='paint2_linear_3984_11038'
x1='18.3701'
y1='-3.33385e-05'
x2='9.85717'
y2='20.4192'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#DB45E0' />
<stop offset='1' stopColor='#6C0F71' />
</linearGradient>
<linearGradient
id='paint3_linear_3984_11038'
x1='18.3701'
y1='-3.33385e-05'
x2='9.85717'
y2='20.4192'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#DB45E0' />
<stop offset='0.677403' stopColor='#A829AE' />
<stop offset='1' stopColor='#8F28B3' />
</linearGradient>
<linearGradient
id='paint4_linear_3984_11038'
x1='18.0002'
y1='7.49958'
x2='14.0004'
y2='23.9988'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#3DCBFF' />
<stop offset='1' stopColor='#00479E' />
</linearGradient>
<linearGradient
id='paint5_linear_3984_11038'
x1='18.2164'
y1='7.92626'
x2='10.5237'
y2='22.9363'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#3DCBFF' />
<stop offset='1' stopColor='#4A40D4' />
</linearGradient>
</defs>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#paint0_linear_3984_11038)'
/>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#paint1_linear_3984_11038)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#paint2_linear_3984_11038)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#paint3_linear_3984_11038)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#paint4_linear_3984_11038)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#paint5_linear_3984_11038)'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Microsoft Planner](https://www.microsoft.com/en-us/microsoft-365/planner) is a task management tool that helps teams organize work visually using boards, tasks, and buckets. Integrated with Microsoft 365, it offers a simple, intuitive way to manage team projects, assign responsibilities, and track progress.
With Microsoft Planner, you can:
- **Create and manage tasks**: Add new tasks with due dates, priorities, and assigned users
- **Organize with buckets**: Group tasks by phase, status, or category to reflect your teams workflow
- **Visualize project status**: Use boards, charts, and filters to monitor workload and track progress
- **Stay integrated with Microsoft 365**: Seamlessly connect tasks with Teams, Outlook, and other Microsoft tools
In Sim, the Microsoft Planner integration allows your agents to programmatically create, read, and manage tasks as part of their workflows. Agents can generate new tasks based on incoming requests, retrieve task details to drive decisions, and track status across projects — all without human intervention. Whether you're building workflows for client onboarding, internal project tracking, or follow-up task generation, integrating Microsoft Planner with Sim gives your agents a structured way to coordinate work, automate task creation, and keep teams aligned.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Microsoft Planner functionality to manage tasks. Read all user tasks, tasks from specific plans, individual tasks, or create new tasks with various properties like title, description, due date, and assignees using OAuth authentication.
## Tools
### `microsoft_planner_read_task`
Read tasks from Microsoft Planner - get all user tasks or all tasks from a specific plan
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | No | The ID of the plan to get tasks from \(if not provided, gets all user tasks\) |
| `taskId` | string | No | The ID of the task to get |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether tasks were retrieved successfully |
| `tasks` | array | Array of task objects with filtered properties |
| `metadata` | object | Metadata including planId, userId, and planUrl |
### `microsoft_planner_create_task`
Create a new task in Microsoft Planner
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `planId` | string | Yes | The ID of the plan where the task will be created |
| `title` | string | Yes | The title of the task |
| `description` | string | No | The description of the task |
| `dueDateTime` | string | No | The due date and time for the task \(ISO 8601 format\) |
| `assigneeUserId` | string | No | The user ID to assign the task to |
| `bucketId` | string | No | The bucket ID to place the task in |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the task was created successfully |
| `task` | object | The created task object with all properties |
| `metadata` | object | Metadata including planId, taskId, and taskUrl |
## Notes
- Category: `tools`
- Type: `microsoft_planner`

View File

@@ -112,16 +112,19 @@ Read content from a Microsoft Teams chat
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
| `chatId` | string | Yes | The ID of the chat to read from |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Message content |
| `metadata` | json | Message metadata |
| `updatedContent` | boolean | Content update status |
| `success` | boolean | Teams chat read operation success status |
| `messageCount` | number | Number of messages retrieved from chat |
| `chatId` | string | ID of the chat that was read from |
| `messages` | array | Array of chat message objects |
| `attachmentCount` | number | Total number of attachments found |
| `attachmentTypes` | array | Types of attachments found |
| `content` | string | Formatted content of chat messages |
### `microsoft_teams_write_chat`
@@ -131,7 +134,6 @@ Write or update content in a Microsoft Teams chat
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
| `chatId` | string | Yes | The ID of the chat to write to |
| `content` | string | Yes | The content to write to the message |
@@ -139,9 +141,12 @@ Write or update content in a Microsoft Teams chat
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Message content |
| `metadata` | json | Message metadata |
| `updatedContent` | boolean | Content update status |
| `success` | boolean | Teams chat message send success status |
| `messageId` | string | Unique identifier for the sent message |
| `chatId` | string | ID of the chat where message was sent |
| `createdTime` | string | Timestamp when message was created |
| `url` | string | Web URL to the message |
| `updatedContent` | boolean | Whether content was successfully updated |
### `microsoft_teams_read_channel`
@@ -151,7 +156,6 @@ Read content from a Microsoft Teams channel
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
| `teamId` | string | Yes | The ID of the team to read from |
| `channelId` | string | Yes | The ID of the channel to read from |
@@ -159,9 +163,14 @@ Read content from a Microsoft Teams channel
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Message content |
| `metadata` | json | Message metadata |
| `updatedContent` | boolean | Content update status |
| `success` | boolean | Teams channel read operation success status |
| `messageCount` | number | Number of messages retrieved from channel |
| `teamId` | string | ID of the team that was read from |
| `channelId` | string | ID of the channel that was read from |
| `messages` | array | Array of channel message objects |
| `attachmentCount` | number | Total number of attachments found |
| `attachmentTypes` | array | Types of attachments found |
| `content` | string | Formatted content of channel messages |
### `microsoft_teams_write_channel`
@@ -171,7 +180,6 @@ Write or send a message to a Microsoft Teams channel
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
| `teamId` | string | Yes | The ID of the team to write to |
| `channelId` | string | Yes | The ID of the channel to write to |
| `content` | string | Yes | The content to write to the channel |
@@ -180,9 +188,13 @@ Write or send a message to a Microsoft Teams channel
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Message content |
| `metadata` | json | Message metadata |
| `updatedContent` | boolean | Content update status |
| `success` | boolean | Teams channel message send success status |
| `messageId` | string | Unique identifier for the sent message |
| `teamId` | string | ID of the team where message was sent |
| `channelId` | string | ID of the channel where message was sent |
| `createdTime` | string | Timestamp when message was created |
| `url` | string | Web URL to the message |
| `updatedContent` | boolean | Whether content was successfully updated |

View File

@@ -79,7 +79,7 @@ The Mistral Parse tool is particularly useful for scenarios where your agents ne
## Usage Instructions
Extract text and structure from PDF documents using Mistral's OCR API. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.
Extract text and structure from PDF documents using Mistral's OCR API. Either enter a URL to a PDF document or upload a PDF file directly. Configure processing options and get the content in your preferred format. For URLs, they must be publicly accessible and point to a valid PDF file. Note: Google Drive, Dropbox, and other cloud storage links are not supported; use a direct download URL from a web server instead.
@@ -106,8 +106,9 @@ Parse PDF documents using Mistral OCR API
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Extracted content |
| `metadata` | json | Processing metadata |
| `success` | boolean | Whether the PDF was parsed successfully |
| `content` | string | Extracted content in the requested format \(markdown, text, or JSON\) |
| `metadata` | object | Processing metadata including jobId, fileType, pageCount, and usage info |

View File

@@ -59,15 +59,14 @@ Read content from a Notion page
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `pageId` | string | Yes | The ID of the Notion page to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Page content in markdown format with headers, paragraphs, lists, and todos |
| `metadata` | object | Page metadata including title, URL, and timestamps |
### `notion_read_database`
@@ -77,15 +76,14 @@ Read database information and structure from Notion
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `databaseId` | string | Yes | The ID of the Notion database to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Database information including title, properties schema, and metadata |
| `metadata` | object | Database metadata including title, ID, URL, timestamps, and properties schema |
### `notion_write`
@@ -95,7 +93,6 @@ Append content to a Notion page
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `pageId` | string | Yes | The ID of the Notion page to append content to |
| `content` | string | Yes | The content to append to the page |
@@ -103,8 +100,7 @@ Append content to a Notion page
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Success message confirming content was appended to page |
### `notion_create_page`
@@ -114,7 +110,6 @@ Create a new page in Notion
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `parentId` | string | Yes | ID of the parent page |
| `title` | string | No | Title of the new page |
| `content` | string | No | Optional content to add to the page upon creation |
@@ -123,8 +118,8 @@ Create a new page in Notion
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Success message confirming page creation |
| `metadata` | object | Page metadata including title, page ID, URL, and timestamps |
### `notion_query_database`
@@ -134,7 +129,6 @@ Query and filter Notion database entries with advanced filtering
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `databaseId` | string | Yes | The ID of the database to query |
| `filter` | string | No | Filter conditions as JSON \(optional\) |
| `sorts` | string | No | Sort criteria as JSON array \(optional\) |
@@ -144,8 +138,8 @@ Query and filter Notion database entries with advanced filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Formatted list of database entries with their properties |
| `metadata` | object | Query metadata including total results count, pagination info, and raw results array |
### `notion_search`
@@ -155,7 +149,6 @@ Search across all pages and databases in Notion workspace
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `query` | string | No | Search terms \(leave empty to get all pages\) |
| `filterType` | string | No | Filter by object type: page, database, or leave empty for all |
| `pageSize` | number | No | Number of results to return \(default: 100, max: 100\) |
@@ -164,8 +157,8 @@ Search across all pages and databases in Notion workspace
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Formatted list of search results including pages and databases |
| `metadata` | object | Search metadata including total results count, pagination info, and raw results array |
### `notion_create_database`
@@ -175,17 +168,16 @@ Create a new database in Notion with custom properties
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Notion OAuth access token |
| `parentId` | string | Yes | ID of the parent page where the database will be created |
| `title` | string | Yes | Title for the new database |
| `properties` | string | No | Database properties as JSON object \(optional, will create a default |
| `properties` | string | No | Database properties as JSON object \(optional, will create a default "Name" property if empty\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Page content |
| `metadata` | any | Page metadata |
| `content` | string | Success message with database details and properties list |
| `metadata` | object | Database metadata including ID, title, URL, creation time, and properties schema |

View File

@@ -0,0 +1,125 @@
---
title: OneDrive
description: Create, upload, and list files
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="onedrive"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon" fill='currentColor' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
<g>
<path
d='M12.20245,11.19292l.00031-.0011,6.71765,4.02379,4.00293-1.68451.00018.00068A6.4768,6.4768,0,0,1,25.5,13c.14764,0,.29358.0067.43878.01639a10.00075,10.00075,0,0,0-18.041-3.01381C7.932,10.00215,7.9657,10,8,10A7.96073,7.96073,0,0,1,12.20245,11.19292Z'
fill='#0364b8'
/>
<path
d='M12.20276,11.19182l-.00031.0011A7.96073,7.96073,0,0,0,8,10c-.0343,0-.06805.00215-.10223.00258A7.99676,7.99676,0,0,0,1.43732,22.57277l5.924-2.49292,2.63342-1.10819,5.86353-2.46746,3.06213-1.28859Z'
fill='#0078d4'
/>
<path
d='M25.93878,13.01639C25.79358,13.0067,25.64764,13,25.5,13a6.4768,6.4768,0,0,0-2.57648.53178l-.00018-.00068-4.00293,1.68451,1.16077.69528L23.88611,18.19l1.66009.99438,5.67633,3.40007a6.5002,6.5002,0,0,0-5.28375-9.56805Z'
fill='#1490df'
/>
<path
d='M25.5462,19.18437,23.88611,18.19l-3.80493-2.2791-1.16077-.69528L15.85828,16.5042,9.99475,18.97166,7.36133,20.07985l-5.924,2.49292A7.98889,7.98889,0,0,0,8,26H25.5a6.49837,6.49837,0,0,0,5.72253-3.41556Z'
fill='#28a8ea'
/>
</g>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[OneDrive](https://onedrive.live.com) is Microsofts cloud storage and file synchronization service that allows users to securely store, access, and share files across devices. Integrated deeply into the Microsoft 365 ecosystem, OneDrive supports seamless collaboration, version control, and real-time access to content across teams and organizations.
Learn how to integrate the OneDrive tool in Sim to automatically pull, manage, and organize your cloud files within your workflows. This tutorial walks you through connecting OneDrive, setting up file access, and using stored content to power automation. Ideal for syncing essential documents and media with your agents in real time.
With OneDrive, you can:
- **Store files securely in the cloud**: Upload and access documents, images, and other files from any device
- **Organize your content**: Create structured folders and manage file versions with ease
- **Collaborate in real time**: Share files, edit them simultaneously with others, and track changes
- **Access across devices**: Use OneDrive from desktop, mobile, and web platforms
- **Integrate with Microsoft 365**: Work seamlessly with Word, Excel, PowerPoint, and Teams
- **Control permissions**: Share files and folders with custom access settings and expiration controls
In Sim, the OneDrive integration enables your agents to directly interact with your cloud storage. Agents can upload new files to specific folders, retrieve and read existing files, and list folder contents to dynamically organize and access information. This integration allows your agents to incorporate file operations into intelligent workflows — automating document intake, content analysis, and structured storage management. By connecting Sim with OneDrive, you empower your agents to manage and use cloud documents programmatically, eliminating manual steps and enhancing automation with secure, real-time file access.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate OneDrive functionality to manage files and folders. Upload new files, create new folders, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.
## Tools
### `onedrive_upload`
Upload a file to OneDrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Yes | The name of the file to upload |
| `content` | string | Yes | The content of the file to upload |
| `folderSelector` | string | No | Select the folder to upload the file to |
| `folderId` | string | No | The ID of the folder to upload the file to \(internal use\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the file was uploaded successfully |
| `file` | object | The uploaded file object with metadata including id, name, webViewLink, webContentLink, and timestamps |
### `onedrive_create_folder`
Create a new folder in OneDrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `folderName` | string | Yes | Name of the folder to create |
| `folderSelector` | string | No | Select the parent folder to create the folder in |
| `folderId` | string | No | ID of the parent folder \(internal use\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the folder was created successfully |
| `file` | object | The created folder object with metadata including id, name, webViewLink, and timestamps |
### `onedrive_list`
List files and folders in OneDrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `folderSelector` | string | No | Select the folder to list files from |
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
| `query` | string | No | A query to filter the files |
| `pageSize` | number | No | The number of files to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether files were listed successfully |
| `files` | array | Array of file and folder objects with metadata |
| `nextPageToken` | string | Token for retrieving the next page of results \(optional\) |
## Notes
- Category: `tools`
- Type: `onedrive`

View File

@@ -66,9 +66,8 @@ Generate embeddings from text using OpenAI
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `embeddings` | json | Generated embeddings |
| `model` | string | Model used |
| `usage` | json | Token usage |
| `success` | boolean | Operation success status |
| `output` | object | Embeddings generation results |

View File

@@ -154,17 +154,22 @@ Send emails using Outlook
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Outlook API |
| `to` | string | Yes | Recipient email address |
| `subject` | string | Yes | Email subject |
| `body` | string | Yes | Email body content |
| `replyToMessageId` | string | No | Message ID to reply to \(for threading\) |
| `conversationId` | string | No | Conversation ID for threading |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Response message |
| `results` | json | Email results |
| `success` | boolean | Email send success status |
| `status` | string | Delivery status of the email |
| `timestamp` | string | Timestamp when email was sent |
| `message` | string | Success or error message |
### `outlook_draft`
@@ -174,17 +179,22 @@ Draft emails using Outlook
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Outlook API |
| `to` | string | Yes | Recipient email address |
| `subject` | string | Yes | Email subject |
| `body` | string | Yes | Email body content |
| `cc` | string | No | CC recipients \(comma-separated\) |
| `bcc` | string | No | BCC recipients \(comma-separated\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Response message |
| `results` | json | Email results |
| `success` | boolean | Email draft creation success status |
| `messageId` | string | Unique identifier for the drafted email |
| `status` | string | Draft status of the email |
| `subject` | string | Subject of the drafted email |
| `timestamp` | string | Timestamp when draft was created |
| `message` | string | Success or error message |
### `outlook_read`
@@ -194,7 +204,6 @@ Read emails from Outlook
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | OAuth access token for Outlook |
| `folder` | string | No | Folder ID to read emails from \(default: Inbox\) |
| `maxResults` | number | No | Maximum number of emails to retrieve \(default: 1, max: 10\) |
@@ -202,8 +211,10 @@ Read emails from Outlook
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Response message |
| `results` | json | Email results |
| `success` | boolean | Email read operation success status |
| `messageCount` | number | Number of emails retrieved |
| `messages` | array | Array of email message objects |
| `message` | string | Success or status message |

View File

@@ -62,9 +62,8 @@ Generate completions using Perplexity AI chat models
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Generated response |
| `model` | string | Model used |
| `usage` | json | Token usage |
| `success` | boolean | Operation success status |
| `output` | object | Chat completion results |

View File

@@ -67,12 +67,10 @@ Generate embeddings from text using Pinecone
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `model` | any | Model information |
| `vector_type` | any | Vector type |
| `usage` | any | Usage statistics |
| `data` | array | Generated embeddings data with values and vector type |
| `model` | string | Model used for generating embeddings |
| `vector_type` | string | Type of vector generated \(dense/sparse\) |
| `usage` | object | Usage statistics for embeddings generation |
### `pinecone_upsert_text`
@@ -91,12 +89,8 @@ Insert or update text records in a Pinecone index
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `model` | any | Model information |
| `vector_type` | any | Vector type |
| `usage` | any | Usage statistics |
| `statusText` | string | Status of the upsert operation |
| `upsertedCount` | number | Number of records successfully upserted |
### `pinecone_search_text`
@@ -119,12 +113,7 @@ Search for similar text in a Pinecone index
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `model` | any | Model information |
| `vector_type` | any | Vector type |
| `usage` | any | Usage statistics |
| `matches` | array | Search results with ID, score, and metadata |
### `pinecone_search_vector`
@@ -147,12 +136,8 @@ Search for similar vectors in a Pinecone index
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `model` | any | Model information |
| `vector_type` | any | Vector type |
| `usage` | any | Usage statistics |
| `matches` | array | Vector search results with ID, score, values, and metadata |
| `namespace` | string | Namespace where the search was performed |
### `pinecone_fetch`
@@ -171,12 +156,7 @@ Fetch vectors by ID from a Pinecone index
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `model` | any | Model information |
| `vector_type` | any | Vector type |
| `usage` | any | Usage statistics |
| `matches` | array | Fetched vectors with ID, values, metadata, and score |

View File

@@ -126,10 +126,8 @@ Insert or update points in a Qdrant collection
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `status` | any | Operation status |
| `status` | string | Status of the upsert operation |
| `data` | object | Result data from the upsert operation |
### `qdrant_search_vector`
@@ -152,10 +150,8 @@ Search for similar vectors in a Qdrant collection
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `status` | any | Operation status |
| `data` | array | Vector search results with ID, score, payload, and optional vector data |
| `status` | string | Status of the search operation |
### `qdrant_fetch_points`
@@ -176,10 +172,8 @@ Fetch points by ID from a Qdrant collection
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `matches` | any | Search matches |
| `upsertedCount` | any | Upserted count |
| `data` | any | Response data |
| `status` | any | Operation status |
| `data` | array | Fetched points with ID, payload, and optional vector data |
| `status` | string | Status of the fetch operation |

View File

@@ -53,20 +53,17 @@ Fetch posts from a subreddit with different sorting options
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Reddit API |
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
| `sort` | string | No | Sort method for posts: |
| `sort` | string | No | Sort method for posts: "hot", "new", "top", or "rising" \(default: "hot"\) |
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
| `time` | string | No | Time filter for |
| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `subreddit` | string | Subreddit name |
| `posts` | json | Posts data |
| `post` | json | Single post data |
| `comments` | json | Comments data |
| `subreddit` | string | Name of the subreddit where posts were fetched from |
| `posts` | array | Array of posts with title, author, URL, score, comments count, and metadata |
### `reddit_get_comments`
@@ -76,20 +73,16 @@ Fetch comments from a specific Reddit post
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Reddit API |
| `postId` | string | Yes | The ID of the Reddit post to fetch comments from |
| `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) |
| `sort` | string | No | Sort method for comments: |
| `sort` | string | No | Sort method for comments: "confidence", "top", "new", "controversial", "old", "random", "qa" \(default: "confidence"\) |
| `limit` | number | No | Maximum number of comments to return \(default: 50, max: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `subreddit` | string | Subreddit name |
| `posts` | json | Posts data |
| `post` | json | Single post data |
| `comments` | json | Comments data |
| `post` | object | Post information including ID, title, author, content, and metadata |

View File

@@ -84,8 +84,8 @@ Retrieve an object from an AWS S3 bucket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `url` | string | Presigned URL |
| `metadata` | json | Object metadata |
| `url` | string | Pre-signed URL for downloading the S3 object |
| `metadata` | object | File metadata including type, size, name, and last modified date |

View File

@@ -103,7 +103,7 @@ A powerful web search tool that provides access to Google search results through
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchResults` | json | Search results data |
| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images\) |

View File

@@ -0,0 +1,132 @@
---
title: Sharepoint
description: Read and create pages
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="sharepoint"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon" fill='currentColor' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'>
<circle fill='#036C70' cx='16.31' cy='8.90' r='8.90' />
<circle fill='#1A9BA1' cx='23.72' cy='17.05' r='8.15' />
<circle fill='#37C6D0' cx='17.42' cy='24.83' r='6.30' />
<path
fill='#000000'
opacity='0.1'
d='M17.79,8.03v15.82c0,0.55-0.34,1.04-0.85,1.25c-0.16,0.07-0.34,0.10-0.51,0.10H11.13c-0.01-0.13-0.01-0.24-0.01-0.37c0-0.12,0-0.25,0.01-0.37c0.14-2.37,1.59-4.46,3.77-5.40v-1.38c-4.85-0.77-8.15-5.32-7.39-10.17c0.01-0.03,0.01-0.07,0.02-0.10c0.04-0.25,0.09-0.50,0.16-0.74h8.74c0.74,0,1.36,0.60,1.36,1.36z'
/>
<path
fill='#000000'
opacity='0.2'
d='M15.69,7.41H7.54c-0.82,4.84,2.43,9.43,7.27,10.25c0.15,0.02,0.29,0.05,0.44,0.06c-2.30,1.09-3.97,4.18-4.12,6.73c-0.01,0.12-0.02,0.25-0.01,0.37c0,0.13,0,0.24,0.01,0.37c0.01,0.25,0.05,0.50,0.10,0.74h4.47c0.55,0,1.04-0.34,1.25-0.85c0.07-0.16,0.10-0.34,0.10-0.51V8.77c0-0.75-0.61-1.36-1.36-1.36z'
/>
<path
fill='#000000'
opacity='0.2'
d='M15.69,7.41H7.54c-0.82,4.84,2.43,9.43,7.27,10.26c0.10,0.02,0.20,0.03,0.30,0.05c-2.22,1.17-3.83,4.26-3.97,6.75h4.56c0.75,0,1.35-0.61,1.36-1.36V8.77c0-0.75-0.61-1.36-1.36-1.36z'
/>
<path
fill='#000000'
opacity='0.2'
d='M14.95,7.41H7.54c-0.78,4.57,2.08,8.97,6.58,10.11c-1.84,2.43-2.27,5.61-2.58,7.22h3.82c0.75,0,1.35-0.61,1.36-1.36V8.77c0-0.75-0.61-1.36-1.36-1.36z'
/>
<path
fill='#008789'
d='M1.36,7.41h13.58c0.75,0,1.36,0.61,1.36,1.36v13.58c0,0.75-0.61,1.36-1.36,1.36H1.36c-0.75,0-1.36-0.61-1.36-1.36V8.77C0,8.02,0.61,7.41,1.36,7.41z'
/>
<path
fill='#FFFFFF'
d='M6.07,15.42c-0.32-0.21-0.58-0.49-0.78-0.82c-0.19-0.34-0.28-0.73-0.27-1.12c-0.02-0.53,0.16-1.05,0.50-1.46c0.36-0.41,0.82-0.71,1.34-0.87c0.59-0.19,1.21-0.29,1.83-0.28c0.82-0.03,1.63,0.08,2.41,0.34v1.71c-0.34-0.20-0.71-0.35-1.09-0.44c-0.42-0.10-0.84-0.15-1.27-0.15c-0.45-0.02-0.90,0.08-1.31,0.28c-0.31,0.14-0.52,0.44-0.52,0.79c0,0.21,0.08,0.41,0.22,0.56c0.17,0.18,0.37,0.32,0.59,0.42c0.25,0.12,0.62,0.29,1.11,0.49c0.05,0.02,0.11,0.04,0.16,0.06c0.49,0.19,0.96,0.42,1.40,0.69c0.34,0.21,0.62,0.49,0.83,0.83c0.21,0.39,0.31,0.82,0.30,1.26c0.02,0.54-0.14,1.08-0.47,1.52c-0.33,0.40-0.77,0.69-1.26,0.85c-0.58,0.18-1.19,0.27-1.80,0.26c-0.55,0-1.09-0.04-1.63-0.13c-0.45-0.07-0.90-0.20-1.32-0.39v-1.80c0.40,0.29,0.86,0.50,1.34,0.64c0.48,0.15,0.97,0.23,1.47,0.24c0.46,0.03,0.92-0.07,1.34-0.28c0.29-0.16,0.46-0.47,0.46-0.80c0-0.23-0.09-0.45-0.25-0.61c-0.20-0.20-0.44-0.36-0.69-0.48c-0.30-0.15-0.73-0.34-1.31-0.59C6.91,16.14,6.48,15.80,6.07,15.42z'
/>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[SharePoint](https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration) is a collaborative platform from Microsoft that enables users to build and manage internal websites, share documents, and organize team resources. It provides a powerful, flexible solution for creating digital workspaces and streamlining content management across organizations.
With SharePoint, you can:
- **Create team and communication sites**: Set up pages and portals to support collaboration, announcements, and content distribution
- **Organize and share content**: Store documents, manage files, and enable version control with secure sharing capabilities
- **Customize pages**: Add text parts to tailor each site to your team's needs
- **Improve discoverability**: Use metadata, search, and navigation tools to help users quickly find what they need
- **Collaborate securely**: Control access with robust permission settings and Microsoft 365 integration
In Sim, the SharePoint integration empowers your agents to create and access SharePoint sites and pages as part of their workflows. This enables automated document management, knowledge sharing, and workspace creation without manual effort. Agents can generate new project pages, upload or retrieve files, and organize resources dynamically, based on workflow inputs. By connecting Sim with SharePoint, you bring structured collaboration and content management into your automation flows — giving your agents the ability to coordinate team activities, surface key information, and maintain a single source of truth across your organization.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Sharepoint functionality to manage pages. Read and create pages, and list sites using OAuth authentication. Supports page operations with custom MIME types and folder organization.
## Tools
### `sharepoint_create_page`
Create a new page in a SharePoint site
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | No | The ID of the SharePoint site \(internal use\) |
| `siteSelector` | string | No | Select the SharePoint site |
| `pageName` | string | Yes | The name of the page to create |
| `pageTitle` | string | No | The title of the page \(defaults to page name if not provided\) |
| `pageContent` | string | No | The content of the page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `page` | object | Created SharePoint page information |
### `sharepoint_read_page`
Read a specific page from a SharePoint site
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteSelector` | string | No | Select the SharePoint site |
| `siteId` | string | No | The ID of the SharePoint site \(internal use\) |
| `pageId` | string | No | The ID of the page to read |
| `pageName` | string | No | The name of the page to read \(alternative to pageId\) |
| `maxPages` | number | No | Maximum number of pages to return when listing all pages \(default: 10, max: 50\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `page` | object | Information about the SharePoint page |
### `sharepoint_list_sites`
List details of all SharePoint sites
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteSelector` | string | No | Select the SharePoint site |
| `groupId` | string | No | The group ID for accessing a group team site |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `site` | object | Information about the current SharePoint site |
## Notes
- Category: `tools`
- Type: `sharepoint`

View File

@@ -1,6 +1,6 @@
---
title: Slack
description: Send messages to Slack
description: Send messages to Slack or trigger workflows from Slack events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -64,7 +64,7 @@ This allows for powerful automation scenarios such as sending notifications, ale
## Usage Instructions
Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax.
Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax or trigger workflows from Slack events like mentions and messages.
@@ -80,7 +80,6 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
@@ -89,10 +88,7 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Message timestamp |
| `channel` | string | Channel identifier |
| `canvas_id` | string | Canvas identifier |
| `title` | string | Canvas title |
| `messages` | json | Message data |
| `channel` | string | Channel ID where message was sent |
### `slack_canvas`
@@ -104,7 +100,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
| `title` | string | Yes | Title of the canvas |
| `content` | string | Yes | Canvas content in markdown format |
@@ -114,11 +109,9 @@ Create and share Slack canvases in channels. Canvases are collaborative document
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Message timestamp |
| `channel` | string | Channel identifier |
| `canvas_id` | string | Canvas identifier |
| `title` | string | Canvas title |
| `messages` | json | Message data |
| `canvas_id` | string | ID of the created canvas |
| `channel` | string | Channel where canvas was created |
| `title` | string | Title of the canvas |
### `slack_message_reader`
@@ -130,7 +123,6 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| --------- | ---- | -------- | ----------- |
| `authMethod` | string | No | Authentication method: oauth or bot_token |
| `botToken` | string | No | Bot token for Custom Bot |
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
| `channel` | string | Yes | Slack channel to read messages from \(e.g., #general\) |
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 100\) |
| `oldest` | string | No | Start of time range \(timestamp\) |
@@ -140,11 +132,7 @@ Read the latest messages from Slack channels. Retrieve conversation history with
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Message timestamp |
| `channel` | string | Channel identifier |
| `canvas_id` | string | Canvas identifier |
| `title` | string | Canvas title |
| `messages` | json | Message data |
| `messages` | array | Array of message objects from the channel |

View File

@@ -214,7 +214,7 @@ Extract structured data from a webpage using Stagehand
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Extracted data |
| `data` | object | Extracted structured data matching the provided schema |

View File

@@ -212,6 +212,7 @@ Run an autonomous web agent to complete tasks and extract structured data
| `startUrl` | string | Yes | URL of the webpage to start the agent on |
| `task` | string | Yes | The task to complete or goal to achieve on the website |
| `variables` | json | No | Optional variables to substitute in the task \(format: \{key: value\}\). Reference in task using %key% |
| `format` | string | No | No description |
| `apiKey` | string | Yes | OpenAI API key for agent execution \(required by Stagehand\) |
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
@@ -219,8 +220,7 @@ Run an autonomous web agent to complete tasks and extract structured data
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `agentResult` | json | Agent execution result |
| `structuredOutput` | any | Structured output data |
| `agentResult` | object | Result from the Stagehand agent execution |

View File

@@ -92,7 +92,7 @@ Query data from a Supabase table
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to query |
| `filter` | string | No | PostgREST filter \(e.g., |
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
| `limit` | number | No | Maximum number of rows to return |
| `apiKey` | string | Yes | Your Supabase service role secret key |
@@ -101,8 +101,8 @@ Query data from a Supabase table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation message |
| `results` | json | Query results |
| `message` | string | Operation status message |
| `results` | array | Array of records returned from the query |
### `supabase_insert`
@@ -121,8 +121,8 @@ Insert data into a Supabase table
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation message |
| `results` | json | Query results |
| `message` | string | Operation status message |
| `results` | array | Array of inserted records |
### `supabase_get_row`
@@ -134,15 +134,15 @@ Get a single row from a Supabase table based on filter criteria
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to query |
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., |
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) |
| `apiKey` | string | Yes | Your Supabase service role secret key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation message |
| `results` | json | Query results |
| `message` | string | Operation status message |
| `results` | object | The row data if found, null if not found |
### `supabase_update`
@@ -154,7 +154,7 @@ Update rows in a Supabase table based on filter criteria
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to update |
| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., |
| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., "id=eq.123"\) |
| `data` | object | Yes | Data to update in the matching rows |
| `apiKey` | string | Yes | Your Supabase service role secret key |
@@ -162,8 +162,8 @@ Update rows in a Supabase table based on filter criteria
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation message |
| `results` | json | Query results |
| `message` | string | Operation status message |
| `results` | array | Array of updated records |
### `supabase_delete`
@@ -175,15 +175,15 @@ Delete rows from a Supabase table based on filter criteria
| --------- | ---- | -------- | ----------- |
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
| `table` | string | Yes | The name of the Supabase table to delete from |
| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., |
| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., "id=eq.123"\) |
| `apiKey` | string | Yes | Your Supabase service role secret key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation message |
| `results` | json | Query results |
| `message` | string | Operation status message |
| `results` | array | Array of deleted records |

View File

@@ -80,12 +80,8 @@ Perform AI-powered web searches using Tavily
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results data |
| `answer` | any | Search answer |
| `query` | string | Query used |
| `content` | string | Extracted content |
| `title` | string | Page title |
| `url` | string | Source URL |
| `query` | string | The search query that was executed |
| `results` | array | results output from the tool |
### `tavily_extract`
@@ -103,12 +99,7 @@ Extract raw content from multiple web pages simultaneously using Tavily
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results data |
| `answer` | any | Search answer |
| `query` | string | Query used |
| `content` | string | Extracted content |
| `title` | string | Page title |
| `url` | string | Source URL |
| `results` | array | The URL that was extracted |

View File

@@ -1,6 +1,6 @@
---
title: Telegram
description: Send a message through Telegram
description: Send messages through Telegram or trigger workflows from Telegram events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -67,7 +67,7 @@ In Sim, the Telegram integration enables your agents to leverage these powerful
## Usage Instructions
Send messages to any Telegram channel using your Bot API key. Integrate automated notifications and alerts into your workflow to keep your team informed.
Send messages to any Telegram channel using your Bot API key or trigger workflows from Telegram bot messages. Integrate automated notifications and alerts into your workflow to keep your team informed.
@@ -89,8 +89,12 @@ Send messages to Telegram channels or users through the Telegram Bot API. Enable
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ok` | boolean | Success status |
| `result` | json | Message result |
| `success` | boolean | Telegram message send success status |
| `messageId` | number | Unique Telegram message identifier |
| `chatId` | string | Target chat ID where message was sent |
| `text` | string | Text content of the sent message |
| `timestamp` | number | Unix timestamp when message was sent |
| `from` | object | Information about the bot that sent the message |

View File

@@ -69,7 +69,7 @@ Processes a provided thought/instruction, making it available for subsequent ste
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `acknowledgedThought` | string | Acknowledged thought process |
| `acknowledgedThought` | string | The thought that was processed and acknowledged |

View File

@@ -67,7 +67,7 @@ Convert text between languages while preserving meaning, nuance, and formatting.
| --------- | ---- | ----------- |
| `content` | string | Translated text |
| `model` | string | Model used |
| `tokens` | any | Token usage |
| `tokens` | json | Token usage |
### `anthropic_chat`
@@ -85,7 +85,7 @@ Convert text between languages while preserving meaning, nuance, and formatting.
| --------- | ---- | ----------- |
| `content` | string | Translated text |
| `model` | string | Model used |
| `tokens` | any | Token usage |
| `tokens` | json | Token usage |

View File

@@ -58,10 +58,11 @@ Send text messages to single or multiple recipients using the Twilio API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Send success status |
| `messageId` | any | Message identifier |
| `status` | any | Delivery status |
| `error` | any | Error information |
| `success` | boolean | SMS send success status |
| `messageId` | string | Unique Twilio message identifier \(SID\) |
| `status` | string | Message delivery status from Twilio |
| `fromNumber` | string | Phone number message was sent from |
| `toNumber` | string | Phone number message was sent to |

View File

@@ -95,9 +95,9 @@ Download files uploaded in Typeform responses
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `total_items` | number | Total response count |
| `page_count` | number | Total page count |
| `items` | json | Response items |
| `fileUrl` | string | Direct download URL for the uploaded file |
| `contentType` | string | MIME type of the uploaded file |
| `filename` | string | Original filename of the uploaded file |
### `typeform_insights`
@@ -114,9 +114,7 @@ Retrieve insights and analytics for Typeform forms
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `total_items` | number | Total response count |
| `page_count` | number | Total page count |
| `items` | json | Response items |
| `fields` | array | Number of users who dropped off at this field |

View File

@@ -70,9 +70,10 @@ Process and analyze images using advanced vision models. Capable of understandin
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Analysis result |
| `model` | any | Model used |
| `tokens` | any | Token usage |
| `content` | string | The analyzed content and description of the image |
| `model` | string | The vision model that was used for analysis |
| `tokens` | number | Total tokens used for the analysis |
| `usage` | object | Detailed token usage breakdown |

View File

@@ -56,21 +56,14 @@ Read content from a Wealthbox note
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `noteId` | string | No | The ID of the note to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Note data and metadata |
### `wealthbox_write_note`
@@ -80,7 +73,6 @@ Create or update a Wealthbox note
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `content` | string | Yes | The main body of the note |
| `contactId` | string | No | ID of contact to link to this note |
@@ -88,14 +80,8 @@ Create or update a Wealthbox note
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Created or updated note data and metadata |
### `wealthbox_read_contact`
@@ -105,21 +91,14 @@ Read content from a Wealthbox contact
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `contactId` | string | No | The ID of the contact to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Contact data and metadata |
### `wealthbox_write_contact`
@@ -129,7 +108,6 @@ Create a new Wealthbox contact
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `firstName` | string | Yes | The first name of the contact |
| `lastName` | string | Yes | The last name of the contact |
| `emailAddress` | string | No | The email address of the contact |
@@ -139,14 +117,8 @@ Create a new Wealthbox contact
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Created or updated contact data and metadata |
### `wealthbox_read_task`
@@ -156,21 +128,14 @@ Read content from a Wealthbox task
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `taskId` | string | No | The ID of the task to read |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Task data and metadata |
### `wealthbox_write_task`
@@ -180,9 +145,8 @@ Create or update a Wealthbox task
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Wealthbox API |
| `title` | string | Yes | The name/title of the task |
| `dueDate` | string | Yes | The due date and time of the task \(format: |
| `dueDate` | string | Yes | The due date and time of the task \(format: "YYYY-MM-DD HH:MM AM/PM -HHMM", e.g., "2015-05-24 11:00 AM -0400"\) |
| `contactId` | string | No | ID of contact to link to this task |
| `description` | string | No | Description or notes about the task |
@@ -190,14 +154,8 @@ Create or update a Wealthbox task
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | any | Note data |
| `notes` | any | Notes list |
| `contact` | any | Contact data |
| `contacts` | any | Contacts list |
| `task` | any | Task data |
| `tasks` | any | Tasks list |
| `metadata` | json | Operation metadata |
| `success` | any | Success status |
| `success` | boolean | Operation success status |
| `output` | object | Created or updated task data and metadata |

View File

@@ -54,15 +54,16 @@ Send WhatsApp messages
| `phoneNumber` | string | Yes | Recipient phone number with country code |
| `message` | string | Yes | Message content to send |
| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID |
| `accessToken` | string | Yes | WhatsApp Business API Access Token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Send success status |
| `messageId` | any | Message identifier |
| `error` | any | Error information |
| `success` | boolean | WhatsApp message send success status |
| `messageId` | string | Unique WhatsApp message identifier |
| `phoneNumber` | string | Recipient phone number |
| `status` | string | Message delivery status |
| `timestamp` | string | Message send timestamp |

View File

@@ -74,11 +74,7 @@ Get a summary and metadata for a specific Wikipedia page.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `summary` | json | Page summary data |
| `searchResults` | json | Search results data |
| `totalHits` | number | Total search hits |
| `content` | json | Page content data |
| `randomPage` | json | Random page data |
| `summary` | object | Wikipedia page summary and metadata |
### `wikipedia_search`
@@ -95,11 +91,7 @@ Search for Wikipedia pages by title or content.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `summary` | json | Page summary data |
| `searchResults` | json | Search results data |
| `totalHits` | number | Total search hits |
| `content` | json | Page content data |
| `randomPage` | json | Random page data |
| `searchResults` | array | Array of matching Wikipedia pages |
### `wikipedia_content`
@@ -115,11 +107,7 @@ Get the full HTML content of a Wikipedia page.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `summary` | json | Page summary data |
| `searchResults` | json | Search results data |
| `totalHits` | number | Total search hits |
| `content` | json | Page content data |
| `randomPage` | json | Random page data |
| `content` | object | Full HTML content and metadata of the Wikipedia page |
### `wikipedia_random`
@@ -134,11 +122,7 @@ Get a random Wikipedia page.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `summary` | json | Page summary data |
| `searchResults` | json | Search results data |
| `totalHits` | number | Total search hits |
| `content` | json | Page content data |
| `randomPage` | json | Random page data |
| `randomPage` | object | Random Wikipedia page data |

View File

@@ -50,7 +50,6 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | X OAuth access token |
| `text` | string | Yes | The text content of your tweet |
| `replyTo` | string | No | ID of the tweet to reply to |
| `mediaIds` | array | No | Array of media IDs to attach to the tweet |
@@ -60,14 +59,7 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tweet` | json | Tweet data |
| `replies` | any | Tweet replies |
| `context` | any | Tweet context |
| `tweets` | json | Tweets data |
| `includes` | any | Additional data |
| `meta` | json | Response metadata |
| `user` | json | User profile data |
| `recentTweets` | any | Recent tweets data |
| `tweet` | object | The newly created tweet data |
### `x_read`
@@ -77,7 +69,6 @@ Read tweet details, including replies and conversation context
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | X OAuth access token |
| `tweetId` | string | Yes | ID of the tweet to read |
| `includeReplies` | boolean | No | Whether to include replies to the tweet |
@@ -85,14 +76,7 @@ Read tweet details, including replies and conversation context
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tweet` | json | Tweet data |
| `replies` | any | Tweet replies |
| `context` | any | Tweet context |
| `tweets` | json | Tweets data |
| `includes` | any | Additional data |
| `meta` | json | Response metadata |
| `user` | json | User profile data |
| `recentTweets` | any | Recent tweets data |
| `tweet` | object | The main tweet data |
### `x_search`
@@ -102,7 +86,6 @@ Search for tweets using keywords, hashtags, or advanced queries
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | X OAuth access token |
| `query` | string | Yes | Search query \(supports X search operators\) |
| `maxResults` | number | No | Maximum number of results to return \(default: 10, max: 100\) |
| `startTime` | string | No | Start time for search \(ISO 8601 format\) |
@@ -113,14 +96,7 @@ Search for tweets using keywords, hashtags, or advanced queries
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tweet` | json | Tweet data |
| `replies` | any | Tweet replies |
| `context` | any | Tweet context |
| `tweets` | json | Tweets data |
| `includes` | any | Additional data |
| `meta` | json | Response metadata |
| `user` | json | User profile data |
| `recentTweets` | any | Recent tweets data |
| `tweets` | array | Array of tweets matching the search query |
### `x_user`
@@ -130,21 +106,13 @@ Get user profile information
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | X OAuth access token |
| `username` | string | Yes | Username to look up \(without @ symbol\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tweet` | json | Tweet data |
| `replies` | any | Tweet replies |
| `context` | any | Tweet context |
| `tweets` | json | Tweets data |
| `includes` | any | Additional data |
| `meta` | json | Response metadata |
| `user` | json | User profile data |
| `recentTweets` | any | Recent tweets data |
| `user` | object | X user profile information |

View File

@@ -62,8 +62,7 @@ Search for videos on YouTube using the YouTube Data API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | json | The items returned by the YouTube search |
| `totalResults` | number | The total number of results returned by the YouTube search |
| `items` | array | Array of YouTube videos matching the search query |

View File

@@ -50,7 +50,7 @@ Choose your input method from the dropdown:
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/chat-input.mp4"></video>
</div>
<p className="text-sm text-gray-600">Chat with your workflow and access both input text and conversation ID for context-aware responses.</p>
<p className="text-sm text-gray-600">Chat with your workflow and access input text, conversation ID, and uploaded files for context-aware responses.</p>
</div>
</Tab>
</Tabs>
@@ -60,13 +60,15 @@ Choose your input method from the dropdown:
In Chat mode, access user input and conversation context through special variables:
```yaml
# Reference the chat input and conversation ID in your workflow
# Reference the chat input, conversation ID, and files in your workflow
user_message: "<start.input>"
conversation_id: "<start.conversationId>"
uploaded_files: "<start.files>"
```
- **`<start.input>`** - Contains the user's message text
- **`<start.conversationId>`** - Unique identifier for the conversation thread
- **`<start.files>`** - Array of files uploaded by the user (if any)
## API Execution

View File

@@ -15,3 +15,6 @@ ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate
# RESEND_API_KEY= # Uncomment and add your key from https://resend.com to send actual emails
# If left commented out, emails will be logged to console instead
# Local AI Models (Optional)
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models

View File

@@ -2,9 +2,12 @@
import Image from 'next/image'
import Link from 'next/link'
import { useBrandConfig } from '@/lib/branding/branding'
import { GridPattern } from '@/app/(landing)/components/grid-pattern'
export default function AuthLayout({ children }: { children: React.ReactNode }) {
const brand = useBrandConfig()
return (
<main className='relative flex min-h-screen flex-col bg-[#0C0C0C] font-geist-sans text-white'>
{/* Background pattern */}
@@ -21,7 +24,17 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
<div className='relative z-10 px-6 pt-9'>
<div className='mx-auto max-w-7xl'>
<Link href='/' className='inline-flex'>
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={`${brand.name} Logo`}
width={42}
height={42}
className='h-[42px] w-[42px] object-contain'
/>
) : (
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
)}
</Link>
</div>
</div>

View File

@@ -94,10 +94,10 @@ describe('LoginPage', () => {
const emailInput = screen.getByPlaceholderText(/enter your email/i)
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'password123' } })
expect(emailInput).toHaveValue('test@example.com')
expect(emailInput).toHaveValue('user@company.com')
expect(passwordInput).toHaveValue('password123')
})
@@ -117,7 +117,7 @@ describe('LoginPage', () => {
const submitButton = screen.getByRole('button', { name: /sign in/i })
await act(async () => {
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'password123' } })
fireEvent.click(submitButton)
})
@@ -140,14 +140,14 @@ describe('LoginPage', () => {
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
const submitButton = screen.getByRole('button', { name: /sign in/i })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'password123' } })
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockSignIn).toHaveBeenCalledWith(
{
email: 'test@example.com',
email: 'user@company.com',
password: 'password123',
callbackURL: '/workspace',
},
@@ -181,7 +181,7 @@ describe('LoginPage', () => {
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
const submitButton = screen.getByRole('button', { name: /sign in/i })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } })
fireEvent.click(submitButton)
@@ -242,13 +242,13 @@ describe('LoginPage', () => {
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
const submitButton = screen.getByRole('button', { name: /sign in/i })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'password123' } })
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockSendOtp).toHaveBeenCalledWith({
email: 'test@example.com',
email: 'user@company.com',
type: 'email-verification',
})
expect(mockRouter.push).toHaveBeenCalledWith('/verify')

View File

@@ -15,25 +15,27 @@ import {
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth-client'
import { quickValidateEmail } from '@/lib/email/validation'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
const logger = createLogger('LoginForm')
const EMAIL_VALIDATIONS = {
required: {
test: (value: string) => Boolean(value && typeof value === 'string'),
message: 'Email is required.',
},
notEmpty: {
test: (value: string) => value.trim().length > 0,
message: 'Email cannot be empty.',
},
basicFormat: {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email address.',
},
const validateEmailField = (emailValue: string): string[] => {
const errors: string[] = []
if (!emailValue || !emailValue.trim()) {
errors.push('Email is required.')
return errors
}
const validation = quickValidateEmail(emailValue.trim().toLowerCase())
if (!validation.isValid) {
errors.push(validation.reason || 'Please enter a valid email address.')
}
return errors
}
const PASSWORD_VALIDATIONS = {
@@ -68,27 +70,6 @@ const validateCallbackUrl = (url: string): boolean => {
}
}
// Validate email and return array of error messages
const validateEmail = (emailValue: string): string[] => {
const errors: string[] = []
if (!EMAIL_VALIDATIONS.required.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.required.message)
return errors // Return early for required field
}
if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.notEmpty.message)
return errors // Return early for empty field
}
if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.basicFormat.message)
}
return errors
}
// Validate password and return array of error messages
const validatePassword = (passwordValue: string): string[] => {
const errors: string[] = []
@@ -182,7 +163,7 @@ export default function LoginPage({
setEmail(newEmail)
// Silently validate but don't show errors until submit
const errors = validateEmail(newEmail)
const errors = validateEmailField(newEmail)
setEmailErrors(errors)
setShowEmailValidationError(false)
}
@@ -205,7 +186,7 @@ export default function LoginPage({
const email = formData.get('email') as string
// Validate email on submit
const emailValidationErrors = validateEmail(email)
const emailValidationErrors = validateEmailField(email)
setEmailErrors(emailValidationErrors)
setShowEmailValidationError(emailValidationErrors.length > 0)

View File

@@ -96,11 +96,11 @@ describe('SignupPage', () => {
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
expect(nameInput).toHaveValue('John Doe')
expect(emailInput).toHaveValue('test@example.com')
expect(emailInput).toHaveValue('user@company.com')
expect(passwordInput).toHaveValue('Password123!')
})
@@ -118,7 +118,7 @@ describe('SignupPage', () => {
const submitButton = screen.getByRole('button', { name: /create account/i })
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
@@ -144,14 +144,14 @@ describe('SignupPage', () => {
// Use valid input that passes all validation rules
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(
{
email: 'test@example.com',
email: 'user@company.com',
password: 'Password123!',
name: 'John Doe',
},
@@ -174,7 +174,7 @@ describe('SignupPage', () => {
// Use name with leading/trailing spaces which should fail validation
fireEvent.change(nameInput, { target: { value: ' John Doe ' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
@@ -206,15 +206,13 @@ describe('SignupPage', () => {
const submitButton = screen.getByRole('button', { name: /create account/i })
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockSendOtp).toHaveBeenCalledWith({
email: 'test@example.com',
type: 'email-verification',
})
// With sendVerificationOnSignUp: true, OTP is sent automatically by Better Auth
// No manual OTP sending in the component anymore
expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true')
})
})
@@ -267,7 +265,7 @@ describe('SignupPage', () => {
const submitButton = screen.getByRole('button', { name: /create account/i })
fireEvent.change(nameInput, { target: { value: longName } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } })
fireEvent.click(submitButton)
@@ -295,7 +293,7 @@ describe('SignupPage', () => {
const submitButton = screen.getByRole('button', { name: /create account/i })
fireEvent.change(nameInput, { target: { value: exactLengthName } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } })
fireEvent.click(submitButton)
@@ -308,7 +306,7 @@ describe('SignupPage', () => {
await waitFor(() => {
expect(mockSignUp).toHaveBeenCalledWith(
{
email: 'test@example.com',
email: 'user@company.com',
password: 'ValidPass123!',
name: exactLengthName,
},
@@ -343,7 +341,7 @@ describe('SignupPage', () => {
await act(async () => {
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
})
@@ -385,12 +383,12 @@ describe('SignupPage', () => {
const submitButton = screen.getByRole('button', { name: /create account/i })
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
fireEvent.click(submitButton)
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/invite/123')
expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true')
})
})

View File

@@ -8,9 +8,13 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { client } from '@/lib/auth-client'
import { quickValidateEmail } from '@/lib/email/validation'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
const logger = createLogger('SignupForm')
const PASSWORD_VALIDATIONS = {
minLength: { regex: /.{8,}/, message: 'Password must be at least 8 characters long.' },
uppercase: {
@@ -51,31 +55,20 @@ const NAME_VALIDATIONS = {
},
}
const EMAIL_VALIDATIONS = {
required: {
test: (value: string) => Boolean(value && typeof value === 'string'),
message: 'Email is required.',
},
notEmpty: {
test: (value: string) => value.trim().length > 0,
message: 'Email cannot be empty.',
},
maxLength: {
test: (value: string) => value.length <= 254,
message: 'Email must be less than 254 characters.',
},
basicFormat: {
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email address.',
},
noSpaces: {
regex: /^[^\s]*$/,
message: 'Email cannot contain spaces.',
},
validStart: {
regex: /^[a-zA-Z0-9]/,
message: 'Email must start with a letter or number.',
},
const validateEmailField = (emailValue: string): string[] => {
const errors: string[] = []
if (!emailValue || !emailValue.trim()) {
errors.push('Email is required.')
return errors
}
const validation = quickValidateEmail(emailValue.trim().toLowerCase())
if (!validation.isValid) {
errors.push(validation.reason || 'Please enter a valid email address.')
}
return errors
}
function SignupFormContent({
@@ -188,39 +181,6 @@ function SignupFormContent({
return errors
}
// Validate email and return array of error messages
const validateEmail = (emailValue: string): string[] => {
const errors: string[] = []
if (!EMAIL_VALIDATIONS.required.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.required.message)
return errors // Return early for required field
}
if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.notEmpty.message)
return errors // Return early for empty field
}
if (!EMAIL_VALIDATIONS.maxLength.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.maxLength.message)
}
if (!EMAIL_VALIDATIONS.noSpaces.regex.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.noSpaces.message)
}
if (!EMAIL_VALIDATIONS.validStart.regex.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.validStart.message)
}
if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) {
errors.push(EMAIL_VALIDATIONS.basicFormat.message)
}
return errors
}
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.target.value
setPassword(newPassword)
@@ -246,7 +206,7 @@ function SignupFormContent({
setEmail(newEmail)
// Silently validate but don't show errors until submit
const errors = validateEmail(newEmail)
const errors = validateEmailField(newEmail)
setEmailErrors(errors)
setShowEmailValidationError(false)
@@ -271,7 +231,7 @@ function SignupFormContent({
setShowNameValidationError(nameValidationErrors.length > 0)
// Validate email on submit
const emailValidationErrors = validateEmail(emailValue)
const emailValidationErrors = validateEmailField(emailValue)
setEmailErrors(emailValidationErrors)
setShowEmailValidationError(emailValidationErrors.length > 0)
@@ -324,7 +284,7 @@ function SignupFormContent({
},
{
onError: (ctx) => {
console.error('Signup error:', ctx.error)
logger.error('Signup error:', ctx.error)
const errorMessage: string[] = ['Failed to create account']
if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) {
@@ -370,30 +330,37 @@ function SignupFormContent({
return
}
// Handle invitation flow redirect
if (isInviteFlow && redirectUrl) {
router.push(redirectUrl)
return
// For new signups, always require verification
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', emailValue)
localStorage.setItem('has_logged_in_before', 'true')
// Set cookie flag for middleware check
document.cookie = 'requiresEmailVerification=true; path=/; max-age=900; SameSite=Lax' // 15 min expiry
document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax'
// Store invitation flow state if applicable
if (isInviteFlow && redirectUrl) {
sessionStorage.setItem('inviteRedirectUrl', redirectUrl)
sessionStorage.setItem('isInviteFlow', 'true')
}
}
// Send verification OTP manually
try {
await client.emailOtp.sendVerificationOtp({
email: emailValue,
type: 'email-verification',
})
} catch (err) {
console.error('Failed to send verification OTP:', err)
}
if (typeof window !== 'undefined') {
sessionStorage.setItem('verificationEmail', emailValue)
localStorage.setItem('has_logged_in_before', 'true')
document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry
} catch (otpError) {
logger.error('Failed to send OTP:', otpError)
// Continue anyway - user can use resend button
}
// Always redirect to verification for new signups
router.push('/verify?fromSignup=true')
} catch (error) {
console.error('Signup error:', error)
logger.error('Signup error:', error)
setIsLoading(false)
}
}

View File

@@ -121,10 +121,14 @@ export function useVerification({
if (response && !response.error) {
setIsVerified(true)
// Clear email from sessionStorage after successful verification
// Clear verification requirements and session storage
if (typeof window !== 'undefined') {
sessionStorage.removeItem('verificationEmail')
// Clear the verification requirement flag
document.cookie =
'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
// Also clear invite-related items
if (isInviteFlow) {
sessionStorage.removeItem('inviteRedirectUrl')
@@ -223,6 +227,11 @@ export function useVerification({
// Auto-verify and redirect in development/docker environments
if (isDevOrDocker || !hasResendKey) {
setIsVerified(true)
// Clear verification requirement cookie (same as manual verification)
document.cookie =
'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
const timeoutId = setTimeout(() => {
router.push('/workspace')
}, 1000)

View File

@@ -15,6 +15,7 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { useBrandConfig } from '@/lib/branding/branding'
import { usePrefetchOnHover } from '@/app/(landing)/utils/prefetch'
// --- Framer Motion Variants ---
@@ -165,6 +166,7 @@ export default function NavClient({
const [isMobile, setIsMobile] = useState(initialIsMobile ?? false)
const [isSheetOpen, setIsSheetOpen] = useState(false)
const _router = useRouter()
const brand = useBrandConfig()
useEffect(() => {
setMounted(true)
@@ -199,7 +201,17 @@ export default function NavClient({
<div className='flex flex-1 items-center'>
<div className='inline-block'>
<Link href='/' className='inline-flex'>
<Image src='/sim.svg' alt='Sim Logo' width={42} height={42} />
{brand.logoUrl ? (
<img
src={brand.logoUrl}
alt={`${brand.name} Logo`}
width={42}
height={42}
className='h-[42px] w-[42px] object-contain'
/>
) : (
<Image src='/sim.svg' alt={`${brand.name} Logo`} width={42} height={42} />
)}
</Link>
</div>
</div>

View File

@@ -99,6 +99,7 @@ export const sampleWorkflowState = {
horizontalHandles: true,
isWide: false,
advancedMode: false,
triggerMode: false,
height: 95,
},
'agent-id': {
@@ -127,6 +128,7 @@ export const sampleWorkflowState = {
horizontalHandles: true,
isWide: false,
advancedMode: false,
triggerMode: false,
height: 680,
},
},
@@ -712,6 +714,7 @@ export function mockFileSystem(
}
return Promise.reject(new Error('File not found'))
}),
mkdir: vi.fn().mockResolvedValue(undefined),
}))
}
@@ -761,14 +764,15 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
getStorageProvider: vi.fn().mockReturnValue(provider),
isUsingCloudStorage: vi.fn().mockReturnValue(isCloudEnabled),
uploadFile: vi.fn().mockResolvedValue({
path: '/api/files/serve/test-key',
key: 'test-key',
path: '/api/files/serve/test-key.txt',
key: 'test-key.txt',
name: 'test.txt',
size: 100,
type: 'text/plain',
}),
downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')),
deleteFile: vi.fn().mockResolvedValue(undefined),
getPresignedUrl: vi.fn().mockResolvedValue(presignedUrl),
}))
if (provider === 's3') {
@@ -1235,14 +1239,15 @@ export function setupFileApiMocks(
getStorageProvider: vi.fn().mockReturnValue('local'),
isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled),
uploadFile: vi.fn().mockResolvedValue({
path: '/api/files/serve/test-key',
key: 'test-key',
path: '/api/files/serve/test-key.txt',
key: 'test-key.txt',
name: 'test.txt',
size: 100,
type: 'text/plain',
}),
downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')),
deleteFile: vi.fn().mockResolvedValue(undefined),
getPresignedUrl: vi.fn().mockResolvedValue('https://example.com/presigned-url'),
}))
}
@@ -1347,8 +1352,8 @@ export function mockUploadUtils(
const {
isCloudStorage = false,
uploadResult = {
path: '/api/files/serve/test-key',
key: 'test-key',
path: '/api/files/serve/test-key.txt',
key: 'test-key.txt',
name: 'test.txt',
size: 100,
type: 'text/plain',

View File

@@ -6,8 +6,6 @@ import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/db'
import { account, user } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('OAuthConnectionsAPI')
interface GoogleIdToken {

View File

@@ -125,7 +125,7 @@ describe('OAuth Credentials API Route', () => {
})
expect(data.credentials[1]).toMatchObject({
id: 'credential-2',
provider: 'google-email',
provider: 'google-default',
isDefault: true,
})
})
@@ -158,7 +158,7 @@ describe('OAuth Credentials API Route', () => {
const data = await response.json()
expect(response.status).toBe(400)
expect(data.error).toBe('Provider is required')
expect(data.error).toBe('Provider or credentialId is required')
expect(mockLogger.warn).toHaveBeenCalled()
})

View File

@@ -1,12 +1,13 @@
import { and, eq } from 'drizzle-orm'
import { jwtDecode } from 'jwt-decode'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import type { OAuthService } from '@/lib/oauth/oauth'
import { parseProvider } from '@/lib/oauth/oauth'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { account, user } from '@/db/schema'
import { account, user, workflow } from '@/db/schema'
export const dynamic = 'force-dynamic'
@@ -25,36 +26,96 @@ export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
try {
// Get the session
const session = await getSession()
// Get query params
const { searchParams } = new URL(request.url)
const providerParam = searchParams.get('provider') as OAuthService | null
const workflowId = searchParams.get('workflowId')
const credentialId = searchParams.get('credentialId')
// Check if the user is authenticated
if (!session?.user?.id) {
// Authenticate requester (supports session, API key, internal JWT)
const authResult = await checkHybridAuth(request)
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthenticated credentials request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const requesterUserId = authResult.userId
// Get the provider from the query params
const { searchParams } = new URL(request.url)
const provider = searchParams.get('provider') as OAuthService | null
// Resolve effective user id: workflow owner if workflowId provided (with access check); else requester
let effectiveUserId: string
if (workflowId) {
// Load workflow owner and workspace for access control
const rows = await db
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)
if (!provider) {
logger.warn(`[${requestId}] Missing provider parameter`)
return NextResponse.json({ error: 'Provider is required' }, { status: 400 })
if (!rows.length) {
logger.warn(`[${requestId}] Workflow not found for credentials request`, { workflowId })
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
}
const wf = rows[0]
if (requesterUserId !== wf.userId) {
if (!wf.workspaceId) {
logger.warn(
`[${requestId}] Forbidden - workflow has no workspace and requester is not owner`,
{
requesterUserId,
}
)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
const perm = await getUserEntityPermissions(requesterUserId, 'workspace', wf.workspaceId)
if (perm === null) {
logger.warn(`[${requestId}] Forbidden credentials request - no workspace access`, {
requesterUserId,
workspaceId: wf.workspaceId,
})
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}
effectiveUserId = wf.userId
} else {
effectiveUserId = requesterUserId
}
// Parse the provider to get base provider and feature type
const { baseProvider } = parseProvider(provider)
if (!providerParam && !credentialId) {
logger.warn(`[${requestId}] Missing provider parameter`)
return NextResponse.json({ error: 'Provider or credentialId is required' }, { status: 400 })
}
// Get all accounts for this user and provider
const accounts = await db
.select()
.from(account)
.where(and(eq(account.userId, session.user.id), eq(account.providerId, provider)))
// Parse the provider to get base provider and feature type (if provider is present)
const { baseProvider } = parseProvider(providerParam || 'google-default')
let accountsData
if (credentialId) {
// Foreign-aware lookup for a specific credential by id
// If workflowId is provided and requester has access (checked above), allow fetching by id only
if (workflowId) {
accountsData = await db.select().from(account).where(eq(account.id, credentialId))
} else {
// Fallback: constrain to requester's own credentials when not in a workflow context
accountsData = await db
.select()
.from(account)
.where(and(eq(account.userId, effectiveUserId), eq(account.id, credentialId)))
}
} else {
// Fetch all credentials for provider and effective user
accountsData = await db
.select()
.from(account)
.where(and(eq(account.userId, effectiveUserId), eq(account.providerId, providerParam!)))
}
// Transform accounts into credentials
const credentials = await Promise.all(
accounts.map(async (acc) => {
accountsData.map(async (acc) => {
// Extract the feature type from providerId (e.g., 'google-default' -> 'default')
const [_, featureType = 'default'] = acc.providerId.split('-')
@@ -109,7 +170,7 @@ export async function GET(request: NextRequest) {
return {
id: acc.id,
name: displayName,
provider,
provider: acc.providerId,
lastUsed: acc.updatedAt.toISOString(),
isDefault: featureType === 'default',
}

View File

@@ -10,6 +10,8 @@ describe('OAuth Token API Routes', () => {
const mockGetUserId = vi.fn()
const mockGetCredential = vi.fn()
const mockRefreshTokenIfNeeded = vi.fn()
const mockAuthorizeCredentialUse = vi.fn()
const mockCheckHybridAuth = vi.fn()
const mockLogger = {
info: vi.fn(),
@@ -37,6 +39,14 @@ describe('OAuth Token API Routes', () => {
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/auth/credential-access', () => ({
authorizeCredentialUse: mockAuthorizeCredentialUse,
}))
vi.doMock('@/lib/auth/hybrid', () => ({
checkHybridAuth: mockCheckHybridAuth,
}))
})
afterEach(() => {
@@ -48,7 +58,12 @@ describe('OAuth Token API Routes', () => {
*/
describe('POST handler', () => {
it('should return access token successfully', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
@@ -78,13 +93,18 @@ describe('OAuth Token API Routes', () => {
expect(data).toHaveProperty('accessToken', 'fresh-token')
// Verify mocks were called correctly
expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, undefined)
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalled()
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
})
it('should handle workflowId for server-side authentication', async () => {
mockGetUserId.mockResolvedValueOnce('workflow-owner-id')
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'internal_jwt',
requesterUserId: 'workflow-owner-id',
credentialOwnerUserId: 'workflow-owner-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
@@ -110,12 +130,8 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(200)
expect(data).toHaveProperty('accessToken', 'fresh-token')
expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId, 'workflow-id')
expect(mockGetCredential).toHaveBeenCalledWith(
mockRequestId,
'credential-id',
'workflow-owner-id'
)
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalled()
})
it('should handle missing credentialId', async () => {
@@ -132,7 +148,10 @@ describe('OAuth Token API Routes', () => {
})
it('should handle authentication failure', async () => {
mockGetUserId.mockResolvedValueOnce(undefined)
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: false,
error: 'Authentication required',
})
const req = createMockRequest('POST', {
credentialId: 'credential-id',
@@ -143,12 +162,12 @@ describe('OAuth Token API Routes', () => {
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(response.status).toBe(403)
expect(data).toHaveProperty('error')
})
it('should handle workflow not found', async () => {
mockGetUserId.mockResolvedValueOnce(undefined)
mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Workflow not found' })
const req = createMockRequest('POST', {
credentialId: 'credential-id',
@@ -160,12 +179,16 @@ describe('OAuth Token API Routes', () => {
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(404)
expect(data).toHaveProperty('error', 'Workflow not found')
expect(response.status).toBe(403)
})
it('should handle credential not found', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
@@ -177,12 +200,17 @@ describe('OAuth Token API Routes', () => {
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(404)
expect(data).toHaveProperty('error', 'Credential not found')
expect(response.status).toBe(401)
expect(data).toHaveProperty('error')
})
it('should handle token refresh failure', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
@@ -211,7 +239,11 @@ describe('OAuth Token API Routes', () => {
*/
describe('GET handler', () => {
it('should return access token successfully', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
@@ -236,7 +268,7 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(200)
expect(data).toHaveProperty('accessToken', 'fresh-token')
expect(mockGetUserId).toHaveBeenCalledWith(mockRequestId)
expect(mockCheckHybridAuth).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
})
@@ -255,7 +287,10 @@ describe('OAuth Token API Routes', () => {
})
it('should handle authentication failure', async () => {
mockGetUserId.mockResolvedValueOnce(undefined)
mockCheckHybridAuth.mockResolvedValueOnce({
success: false,
error: 'Authentication required',
})
const req = new Request(
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'
@@ -267,11 +302,15 @@ describe('OAuth Token API Routes', () => {
const data = await response.json()
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(data).toHaveProperty('error')
})
it('should handle credential not found', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce(undefined)
const req = new Request(
@@ -284,11 +323,15 @@ describe('OAuth Token API Routes', () => {
const data = await response.json()
expect(response.status).toBe(404)
expect(data).toHaveProperty('error', 'Credential not found')
expect(data).toHaveProperty('error')
})
it('should handle missing access token', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: null,
@@ -306,12 +349,15 @@ describe('OAuth Token API Routes', () => {
const data = await response.json()
expect(response.status).toBe(400)
expect(data).toHaveProperty('error', 'No access token available')
expect(mockLogger.warn).toHaveBeenCalled()
expect(data).toHaveProperty('error')
})
it('should handle token refresh failure', async () => {
mockGetUserId.mockResolvedValueOnce('test-user-id')
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
@@ -331,7 +377,7 @@ describe('OAuth Token API Routes', () => {
const data = await response.json()
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'Failed to refresh access token')
expect(data).toHaveProperty('error')
})
})
})

View File

@@ -1,6 +1,8 @@
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { getCredential, getUserId, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getCredential, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -26,23 +28,13 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Determine the user ID based on the context
const userId = await getUserId(requestId, workflowId)
if (!userId) {
return NextResponse.json(
{ error: workflowId ? 'Workflow not found' : 'User not authenticated' },
{ status: workflowId ? 404 : 401 }
)
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
// Get the credential from the database
const credential = await getCredential(requestId, credentialId, userId)
if (!credential) {
logger.error(`[${requestId}] Credential not found: ${credentialId}`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
// Fetch the credential as the owner to enforce ownership scoping
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
try {
// Refresh the token if needed
@@ -75,14 +67,13 @@ export async function GET(request: NextRequest) {
}
// For GET requests, we only support session-based authentication
const userId = await getUserId(requestId)
if (!userId) {
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get the credential from the database
const credential = await getCredential(requestId, credentialId, userId)
const credential = await getCredential(requestId, credentialId, auth.userId)
if (!credential) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })

View File

@@ -9,8 +9,6 @@ import { member } from '@/db/schema'
const logger = createLogger('UnifiedBillingAPI')
export const dynamic = 'force-dynamic'
/**
* Unified Billing Endpoint
*/

View File

@@ -245,6 +245,8 @@ describe('Chat API Route', () => {
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
}))
const validData = {
@@ -287,6 +289,8 @@ describe('Chat API Route', () => {
NODE_ENV: 'development',
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
},
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
}))
const validData = {

View File

@@ -0,0 +1,132 @@
export interface FileAttachment {
id: string
s3_key: string
filename: string
media_type: string
size: number
}
export interface AnthropicMessageContent {
type: 'text' | 'image' | 'document'
text?: string
source?: {
type: 'base64'
media_type: string
data: string
}
}
/**
* Mapping of MIME types to Anthropic content types
*/
export const MIME_TYPE_MAPPING: Record<string, 'image' | 'document'> = {
// Images
'image/jpeg': 'image',
'image/jpg': 'image',
'image/png': 'image',
'image/gif': 'image',
'image/webp': 'image',
'image/svg+xml': 'image',
// Documents
'application/pdf': 'document',
'text/plain': 'document',
'text/csv': 'document',
'application/json': 'document',
'application/xml': 'document',
'text/xml': 'document',
'text/html': 'document',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'document', // .xlsx
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'document', // .pptx
'application/msword': 'document', // .doc
'application/vnd.ms-excel': 'document', // .xls
'application/vnd.ms-powerpoint': 'document', // .ppt
'text/markdown': 'document',
'application/rtf': 'document',
}
/**
* Get the Anthropic content type for a given MIME type
*/
export function getAnthropicContentType(mimeType: string): 'image' | 'document' | null {
return MIME_TYPE_MAPPING[mimeType.toLowerCase()] || null
}
/**
* Check if a MIME type is supported by Anthropic
*/
export function isSupportedFileType(mimeType: string): boolean {
return mimeType.toLowerCase() in MIME_TYPE_MAPPING
}
/**
* Convert a file buffer to base64
*/
export function bufferToBase64(buffer: Buffer): string {
return buffer.toString('base64')
}
/**
* Create Anthropic message content from file data
*/
export function createAnthropicFileContent(
fileBuffer: Buffer,
mimeType: string
): AnthropicMessageContent | null {
const contentType = getAnthropicContentType(mimeType)
if (!contentType) {
return null
}
return {
type: contentType,
source: {
type: 'base64',
media_type: mimeType,
data: bufferToBase64(fileBuffer),
},
}
}
/**
* Extract file extension from filename
*/
export function getFileExtension(filename: string): string {
const lastDot = filename.lastIndexOf('.')
return lastDot !== -1 ? filename.slice(lastDot + 1).toLowerCase() : ''
}
/**
* Get MIME type from file extension (fallback if not provided)
*/
export function getMimeTypeFromExtension(extension: string): string {
const extensionMimeMap: Record<string, string> = {
// Images
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml',
// Documents
pdf: 'application/pdf',
txt: 'text/plain',
csv: 'text/csv',
json: 'application/json',
xml: 'application/xml',
html: 'text/html',
htm: 'text/html',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
doc: 'application/msword',
xls: 'application/vnd.ms-excel',
ppt: 'application/vnd.ms-powerpoint',
md: 'text/markdown',
rtf: 'application/rtf',
}
return extensionMimeMap[extension.toLowerCase()] || 'application/octet-stream'
}

View File

@@ -13,12 +13,25 @@ import { getCopilotModel } from '@/lib/copilot/config'
import { TITLE_GENERATION_SYSTEM_PROMPT, TITLE_GENERATION_USER_PROMPT } from '@/lib/copilot/prompts'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFile } from '@/lib/uploads'
import { downloadFromS3WithConfig } from '@/lib/uploads/s3/s3-client'
import { S3_COPILOT_CONFIG, USE_S3_STORAGE } from '@/lib/uploads/setup'
import { db } from '@/db'
import { copilotChats } from '@/db/schema'
import { executeProviderRequest } from '@/providers'
import { createAnthropicFileContent, isSupportedFileType } from './file-utils'
const logger = createLogger('CopilotChatAPI')
// Schema for file attachments
const FileAttachmentSchema = z.object({
id: z.string(),
s3_key: z.string(),
filename: z.string(),
media_type: z.string(),
size: z.number(),
})
// Schema for chat messages
const ChatMessageSchema = z.object({
message: z.string().min(1, 'Message is required'),
@@ -29,6 +42,7 @@ const ChatMessageSchema = z.object({
createNewChat: z.boolean().optional().default(false),
stream: z.boolean().optional().default(true),
implicitFeedback: z.string().optional(),
fileAttachments: z.array(FileAttachmentSchema).optional(),
})
// Sim Agent API configuration
@@ -145,6 +159,7 @@ export async function POST(req: NextRequest) {
createNewChat,
stream,
implicitFeedback,
fileAttachments,
} = ChatMessageSchema.parse(body)
logger.info(`[${tracker.requestId}] Processing copilot chat request`, {
@@ -195,15 +210,91 @@ export async function POST(req: NextRequest) {
}
}
// Process file attachments if present
const processedFileContents: any[] = []
if (fileAttachments && fileAttachments.length > 0) {
logger.info(`[${tracker.requestId}] Processing ${fileAttachments.length} file attachments`)
for (const attachment of fileAttachments) {
try {
// Check if file type is supported
if (!isSupportedFileType(attachment.media_type)) {
logger.warn(`[${tracker.requestId}] Unsupported file type: ${attachment.media_type}`)
continue
}
// Download file from S3
logger.info(`[${tracker.requestId}] Downloading file: ${attachment.s3_key}`)
let fileBuffer: Buffer
if (USE_S3_STORAGE) {
fileBuffer = await downloadFromS3WithConfig(attachment.s3_key, S3_COPILOT_CONFIG)
} else {
// Fallback to generic downloadFile for other storage providers
fileBuffer = await downloadFile(attachment.s3_key)
}
// Convert to Anthropic format
const fileContent = createAnthropicFileContent(fileBuffer, attachment.media_type)
if (fileContent) {
processedFileContents.push(fileContent)
logger.info(
`[${tracker.requestId}] Processed file: ${attachment.filename} (${attachment.media_type})`
)
}
} catch (error) {
logger.error(
`[${tracker.requestId}] Failed to process file ${attachment.filename}:`,
error
)
// Continue processing other files
}
}
}
// Build messages array for sim agent with conversation history
const messages = []
// Add conversation history
// Add conversation history (need to rebuild these with file support if they had attachments)
for (const msg of conversationHistory) {
messages.push({
role: msg.role,
content: msg.content,
})
if (msg.fileAttachments && msg.fileAttachments.length > 0) {
// This is a message with file attachments - rebuild with content array
const content: any[] = [{ type: 'text', text: msg.content }]
// Process file attachments for historical messages
for (const attachment of msg.fileAttachments) {
try {
if (isSupportedFileType(attachment.media_type)) {
let fileBuffer: Buffer
if (USE_S3_STORAGE) {
fileBuffer = await downloadFromS3WithConfig(attachment.s3_key, S3_COPILOT_CONFIG)
} else {
// Fallback to generic downloadFile for other storage providers
fileBuffer = await downloadFile(attachment.s3_key)
}
const fileContent = createAnthropicFileContent(fileBuffer, attachment.media_type)
if (fileContent) {
content.push(fileContent)
}
}
} catch (error) {
logger.error(
`[${tracker.requestId}] Failed to process historical file ${attachment.filename}:`,
error
)
}
}
messages.push({
role: msg.role,
content,
})
} else {
// Regular text-only message
messages.push({
role: msg.role,
content: msg.content,
})
}
}
// Add implicit feedback if provided
@@ -214,11 +305,27 @@ export async function POST(req: NextRequest) {
})
}
// Add current user message
messages.push({
role: 'user',
content: message,
})
// Add current user message with file attachments
if (processedFileContents.length > 0) {
// Message with files - use content array format
const content: any[] = [{ type: 'text', text: message }]
// Add file contents
for (const fileContent of processedFileContents) {
content.push(fileContent)
}
messages.push({
role: 'user',
content,
})
} else {
// Text-only message
messages.push({
role: 'user',
content: message,
})
}
// Start title generation in parallel if this is a new chat with first message
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
@@ -270,6 +377,7 @@ export async function POST(req: NextRequest) {
role: 'user',
content: message,
timestamp: new Date().toISOString(),
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
}
// Create a pass-through stream that captures the response
@@ -590,6 +698,7 @@ export async function POST(req: NextRequest) {
role: 'user',
content: message,
timestamp: new Date().toISOString(),
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
}
const assistantMessage = {

View File

@@ -24,6 +24,17 @@ const UpdateMessagesSchema = z.object({
timestamp: z.string(),
toolCalls: z.array(z.any()).optional(),
contentBlocks: z.array(z.any()).optional(),
fileAttachments: z
.array(
z.object({
id: z.string(),
s3_key: z.string(),
filename: z.string(),
media_type: z.string(),
size: z.number(),
})
)
.optional(),
})
),
})

View File

@@ -354,7 +354,14 @@ describe('Copilot Methods API Route', () => {
86400
)
expect(mockRedisGet).toHaveBeenCalledWith('tool_call:tool-call-123')
expect(mockToolRegistryExecute).toHaveBeenCalledWith('interrupt-tool', { key: 'value' })
expect(mockToolRegistryExecute).toHaveBeenCalledWith('interrupt-tool', {
key: 'value',
confirmationMessage: 'User approved',
fullData: {
message: 'User approved',
status: 'accepted',
},
})
})
it('should handle tool execution with interrupt - user rejection', async () => {
@@ -613,6 +620,10 @@ describe('Copilot Methods API Route', () => {
expect(mockToolRegistryExecute).toHaveBeenCalledWith('no_op', {
existing: 'param',
confirmationMessage: 'Confirmation message',
fullData: {
message: 'Confirmation message',
status: 'accepted',
},
})
})

View File

@@ -57,7 +57,7 @@ async function addToolToRedis(toolCallId: string): Promise<void> {
*/
async function pollRedisForTool(
toolCallId: string
): Promise<{ status: NotificationStatus; message?: string } | null> {
): Promise<{ status: NotificationStatus; message?: string; fullData?: any } | null> {
const redis = getRedisClient()
if (!redis) {
logger.warn('pollRedisForTool: Redis client not available')
@@ -86,12 +86,14 @@ async function pollRedisForTool(
let status: NotificationStatus | null = null
let message: string | undefined
let fullData: any = null
// Try to parse as JSON (new format), fallback to string (old format)
try {
const parsedData = JSON.parse(redisValue)
status = parsedData.status as NotificationStatus
message = parsedData.message || undefined
fullData = parsedData // Store the full parsed data
} catch {
// Fallback to old format (direct status string)
status = redisValue as NotificationStatus
@@ -138,7 +140,7 @@ async function pollRedisForTool(
})
}
return { status, message }
return { status, message, fullData }
}
// Wait before next poll
@@ -163,9 +165,13 @@ async function pollRedisForTool(
* Handle tool calls that require user interruption/approval
* Returns { approved: boolean, rejected: boolean, error?: boolean, message?: string } to distinguish between rejection, timeout, and error
*/
async function interruptHandler(
toolCallId: string
): Promise<{ approved: boolean; rejected: boolean; error?: boolean; message?: string }> {
async function interruptHandler(toolCallId: string): Promise<{
approved: boolean
rejected: boolean
error?: boolean
message?: string
fullData?: any
}> {
if (!toolCallId) {
logger.error('interruptHandler: No tool call ID provided')
return { approved: false, rejected: false, error: true, message: 'No tool call ID provided' }
@@ -185,31 +191,31 @@ async function interruptHandler(
return { approved: false, rejected: false }
}
const { status, message } = result
const { status, message, fullData } = result
if (status === 'rejected') {
logger.info('Tool execution rejected by user', { toolCallId, message })
return { approved: false, rejected: true, message }
return { approved: false, rejected: true, message, fullData }
}
if (status === 'accepted') {
logger.info('Tool execution approved by user', { toolCallId, message })
return { approved: true, rejected: false, message }
return { approved: true, rejected: false, message, fullData }
}
if (status === 'error') {
logger.error('Tool execution failed with error', { toolCallId, message })
return { approved: false, rejected: false, error: true, message }
return { approved: false, rejected: false, error: true, message, fullData }
}
if (status === 'background') {
logger.info('Tool execution moved to background', { toolCallId, message })
return { approved: true, rejected: false, message }
return { approved: true, rejected: false, message, fullData }
}
if (status === 'success') {
logger.info('Tool execution completed successfully', { toolCallId, message })
return { approved: true, rejected: false, message }
return { approved: true, rejected: false, message, fullData }
}
logger.warn('Unexpected tool call status', { toolCallId, status, message })
@@ -326,7 +332,7 @@ export async function POST(req: NextRequest) {
})
// Handle interrupt flow
const { approved, rejected, error, message } = await interruptHandler(toolCallId)
const { approved, rejected, error, message, fullData } = await interruptHandler(toolCallId)
if (rejected) {
logger.info(`[${requestId}] Tool execution rejected by user`, {
@@ -371,10 +377,13 @@ export async function POST(req: NextRequest) {
message,
})
// For noop tool, pass the confirmation message as a parameter
if (methodId === 'no_op' && message) {
// For tools that need confirmation data, pass the message and/or fullData as parameters
if (message) {
params.confirmationMessage = message
}
if (fullData) {
params.fullData = fullData
}
}
// Execute the tool directly via registry

View File

@@ -0,0 +1,99 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads'
import { BLOB_EXECUTION_FILES_CONFIG, S3_EXECUTION_FILES_CONFIG } from '@/lib/uploads/setup'
import { createErrorResponse } from '@/app/api/files/utils'
const logger = createLogger('FileDownload')
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { key, name, storageProvider, bucketName, isExecutionFile } = body
if (!key) {
return createErrorResponse(new Error('File key is required'), 400)
}
logger.info(`Generating download URL for file: ${name || key}`)
if (isUsingCloudStorage()) {
// Generate a fresh 5-minute presigned URL for cloud storage
try {
let downloadUrl: string
// Use execution files storage if flagged as execution file
if (isExecutionFile) {
logger.info(`Using execution files storage for file: ${key}`)
downloadUrl = await getPresignedUrlWithConfig(
key,
{
bucket: S3_EXECUTION_FILES_CONFIG.bucket,
region: S3_EXECUTION_FILES_CONFIG.region,
},
5 * 60 // 5 minutes
)
} else if (storageProvider && (storageProvider === 's3' || storageProvider === 'blob')) {
// Use explicitly specified storage provider (legacy support)
logger.info(`Using specified storage provider '${storageProvider}' for file: ${key}`)
if (storageProvider === 's3') {
downloadUrl = await getPresignedUrlWithConfig(
key,
{
bucket: bucketName || S3_EXECUTION_FILES_CONFIG.bucket,
region: S3_EXECUTION_FILES_CONFIG.region,
},
5 * 60 // 5 minutes
)
} else {
// blob
downloadUrl = await getPresignedUrlWithConfig(
key,
{
accountName: BLOB_EXECUTION_FILES_CONFIG.accountName,
accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey,
connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString,
containerName: bucketName || BLOB_EXECUTION_FILES_CONFIG.containerName,
},
5 * 60 // 5 minutes
)
}
} else {
// Use default storage (regular uploads)
logger.info(`Using default storage for file: ${key}`)
downloadUrl = await getPresignedUrl(key, 5 * 60) // 5 minutes
}
return NextResponse.json({
downloadUrl,
expiresIn: 300, // 5 minutes in seconds
fileName: name || key.split('/').pop() || 'download',
})
} catch (error) {
logger.error(`Failed to generate presigned URL for ${key}:`, error)
return createErrorResponse(
error instanceof Error ? error : new Error('Failed to generate download URL'),
500
)
}
} else {
// For local storage, return the direct path
const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/files/serve/${key}`
return NextResponse.json({
downloadUrl,
expiresIn: null, // Local URLs don't expire
fileName: name || key.split('/').pop() || 'download',
})
}
} catch (error) {
logger.error('Error in file download endpoint:', error)
return createErrorResponse(
error instanceof Error ? error : new Error('Internal server error'),
500
)
}
}

View File

@@ -0,0 +1,70 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateExecutionFileDownloadUrl } from '@/lib/workflows/execution-file-storage'
import { getExecutionFiles } from '@/lib/workflows/execution-files-server'
import type { UserFile } from '@/executor/types'
const logger = createLogger('ExecutionFileDownloadAPI')
/**
* Generate a short-lived presigned URL for secure execution file download
* GET /api/files/execution/[executionId]/[fileId]
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ executionId: string; fileId: string }> }
) {
try {
const { executionId, fileId } = await params
if (!executionId || !fileId) {
return NextResponse.json({ error: 'Execution ID and File ID are required' }, { status: 400 })
}
logger.info(`Generating download URL for file ${fileId} in execution ${executionId}`)
// Get files for this execution
const executionFiles = await getExecutionFiles(executionId)
if (executionFiles.length === 0) {
return NextResponse.json({ error: 'No files found for this execution' }, { status: 404 })
}
// Find the specific file
const file = executionFiles.find((f) => f.id === fileId)
if (!file) {
return NextResponse.json({ error: 'File not found in this execution' }, { status: 404 })
}
// Check if file is expired
if (new Date(file.expiresAt) < new Date()) {
return NextResponse.json({ error: 'File has expired' }, { status: 410 })
}
// Since ExecutionFileMetadata is now just UserFile, no conversion needed
const userFile: UserFile = file
// Generate a new short-lived presigned URL (5 minutes)
const downloadUrl = await generateExecutionFileDownloadUrl(userFile)
logger.info(`Generated download URL for file ${file.name} (execution: ${executionId})`)
const response = NextResponse.json({
downloadUrl,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
expiresIn: 300, // 5 minutes
})
// Ensure no caching of download URLs
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
response.headers.set('Pragma', 'no-cache')
response.headers.set('Expires', '0')
return response
} catch (error) {
logger.error('Error generating execution file download URL:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,164 @@
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
UploadPartCommand,
} from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
import { S3_KB_CONFIG } from '@/lib/uploads/setup'
const logger = createLogger('MultipartUploadAPI')
interface InitiateMultipartRequest {
fileName: string
contentType: string
fileSize: number
}
interface GetPartUrlsRequest {
uploadId: string
key: string
partNumbers: number[]
}
interface CompleteMultipartRequest {
uploadId: string
key: string
parts: Array<{
ETag: string
PartNumber: number
}>
}
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const action = request.nextUrl.searchParams.get('action')
if (!isUsingCloudStorage() || getStorageProvider() !== 's3') {
return NextResponse.json(
{ error: 'Multipart upload is only available with S3 storage' },
{ status: 400 }
)
}
const { getS3Client } = await import('@/lib/uploads/s3/s3-client')
const s3Client = getS3Client()
switch (action) {
case 'initiate': {
const data: InitiateMultipartRequest = await request.json()
const { fileName, contentType } = data
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
const uniqueKey = `kb/${uuidv4()}-${safeFileName}`
const command = new CreateMultipartUploadCommand({
Bucket: S3_KB_CONFIG.bucket,
Key: uniqueKey,
ContentType: contentType,
Metadata: {
originalName: fileName,
uploadedAt: new Date().toISOString(),
purpose: 'knowledge-base',
},
})
const response = await s3Client.send(command)
logger.info(`Initiated multipart upload for ${fileName}: ${response.UploadId}`)
return NextResponse.json({
uploadId: response.UploadId,
key: uniqueKey,
})
}
case 'get-part-urls': {
const data: GetPartUrlsRequest = await request.json()
const { uploadId, key, partNumbers } = data
const presignedUrls = await Promise.all(
partNumbers.map(async (partNumber) => {
const command = new UploadPartCommand({
Bucket: S3_KB_CONFIG.bucket,
Key: key,
PartNumber: partNumber,
UploadId: uploadId,
})
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 })
return { partNumber, url }
})
)
return NextResponse.json({ presignedUrls })
}
case 'complete': {
const data: CompleteMultipartRequest = await request.json()
const { uploadId, key, parts } = data
const command = new CompleteMultipartUploadCommand({
Bucket: S3_KB_CONFIG.bucket,
Key: key,
UploadId: uploadId,
MultipartUpload: {
Parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
},
})
const response = await s3Client.send(command)
logger.info(`Completed multipart upload for key ${key}`)
const finalPath = `/api/files/serve/s3/${encodeURIComponent(key)}`
return NextResponse.json({
success: true,
location: response.Location,
path: finalPath,
key,
})
}
case 'abort': {
const data = await request.json()
const { uploadId, key } = data
const command = new AbortMultipartUploadCommand({
Bucket: S3_KB_CONFIG.bucket,
Key: key,
UploadId: uploadId,
})
await s3Client.send(command)
logger.info(`Aborted multipart upload for key ${key}`)
return NextResponse.json({ success: true })
}
default:
return NextResponse.json(
{ error: 'Invalid action. Use: initiate, get-part-urls, complete, or abort' },
{ status: 400 }
)
}
} catch (error) {
logger.error('Multipart upload error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Multipart upload failed' },
{ status: 500 }
)
}
}

View File

@@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
import { createLogger } from '@/lib/logs/console/logger'
import { downloadFile, isUsingCloudStorage } from '@/lib/uploads'
import { UPLOAD_DIR } from '@/lib/uploads/setup'
import { UPLOAD_DIR_SERVER } from '@/lib/uploads/setup.server'
import '@/lib/uploads/setup.server'
export const dynamic = 'force-dynamic'
@@ -70,7 +70,7 @@ export async function POST(request: NextRequest) {
const requestData = await request.json()
const { filePath, fileType } = requestData
if (!filePath) {
if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) {
return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 })
}
@@ -80,6 +80,16 @@ export async function POST(request: NextRequest) {
if (Array.isArray(filePath)) {
const results = []
for (const path of filePath) {
// Skip empty or invalid paths
if (!path || (typeof path === 'string' && path.trim() === '')) {
results.push({
success: false,
error: 'Empty file path in array',
filePath: path || '',
})
continue
}
const result = await parseFileSingle(path, fileType)
// Add processing time to metadata
if (result.metadata) {
@@ -154,6 +164,15 @@ export async function POST(request: NextRequest) {
async function parseFileSingle(filePath: string, fileType?: string): Promise<ParseResult> {
logger.info('Parsing file:', filePath)
// Validate that filePath is not empty
if (!filePath || filePath.trim() === '') {
return {
success: false,
error: 'Empty file path provided',
filePath: filePath || '',
}
}
// Validate path for security before any processing
const pathValidation = validateFilePath(filePath)
if (!pathValidation.isValid) {
@@ -337,7 +356,7 @@ async function handleLocalFile(filePath: string, fileType?: string): Promise<Par
try {
// Extract filename from path
const filename = filePath.split('/').pop() || filePath
const fullPath = path.join(UPLOAD_DIR, filename)
const fullPath = path.join(UPLOAD_DIR_SERVER, filename)
logger.info('Processing local file:', fullPath)

View File

@@ -2,16 +2,18 @@ import { PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client'
import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client'
// Dynamic imports for storage clients to avoid client-side bundling
import {
BLOB_CHAT_CONFIG,
BLOB_CONFIG,
BLOB_COPILOT_CONFIG,
BLOB_KB_CONFIG,
S3_CHAT_CONFIG,
S3_CONFIG,
S3_COPILOT_CONFIG,
S3_KB_CONFIG,
} from '@/lib/uploads/setup'
import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils'
@@ -22,9 +24,11 @@ interface PresignedUrlRequest {
fileName: string
contentType: string
fileSize: number
userId?: string
chatId?: string
}
type UploadType = 'general' | 'knowledge-base' | 'chat'
type UploadType = 'general' | 'knowledge-base' | 'chat' | 'copilot'
class PresignedUrlError extends Error {
constructor(
@@ -51,6 +55,11 @@ class ValidationError extends PresignedUrlError {
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
let data: PresignedUrlRequest
try {
data = await request.json()
@@ -83,7 +92,19 @@ export async function POST(request: NextRequest) {
? 'knowledge-base'
: uploadTypeParam === 'chat'
? 'chat'
: 'general'
: uploadTypeParam === 'copilot'
? 'copilot'
: 'general'
// Evaluate user id from session for copilot uploads
const sessionUserId = session.user.id
// Validate copilot-specific requirements (use session user)
if (uploadType === 'copilot') {
if (!sessionUserId?.trim()) {
throw new ValidationError('Authenticated user session is required for copilot uploads')
}
}
if (!isUsingCloudStorage()) {
throw new StorageConfigError(
@@ -96,9 +117,21 @@ export async function POST(request: NextRequest) {
switch (storageProvider) {
case 's3':
return await handleS3PresignedUrl(fileName, contentType, fileSize, uploadType)
return await handleS3PresignedUrl(
fileName,
contentType,
fileSize,
uploadType,
sessionUserId
)
case 'blob':
return await handleBlobPresignedUrl(fileName, contentType, fileSize, uploadType)
return await handleBlobPresignedUrl(
fileName,
contentType,
fileSize,
uploadType,
sessionUserId
)
default:
throw new StorageConfigError(`Unknown storage provider: ${storageProvider}`)
}
@@ -126,7 +159,8 @@ async function handleS3PresignedUrl(
fileName: string,
contentType: string,
fileSize: number,
uploadType: UploadType
uploadType: UploadType,
userId?: string
) {
try {
const config =
@@ -134,16 +168,28 @@ async function handleS3PresignedUrl(
? S3_KB_CONFIG
: uploadType === 'chat'
? S3_CHAT_CONFIG
: S3_CONFIG
: uploadType === 'copilot'
? S3_COPILOT_CONFIG
: S3_CONFIG
if (!config.bucket || !config.region) {
throw new StorageConfigError(`S3 configuration missing for ${uploadType} uploads`)
}
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : ''
const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}`
let prefix = ''
if (uploadType === 'knowledge-base') {
prefix = 'kb/'
} else if (uploadType === 'chat') {
prefix = 'chat/'
} else if (uploadType === 'copilot') {
prefix = `${userId}/`
}
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
const { sanitizeFilenameForMetadata } = await import('@/lib/uploads/s3/s3-client')
const sanitizedOriginalName = sanitizeFilenameForMetadata(fileName)
const metadata: Record<string, string> = {
@@ -155,6 +201,9 @@ async function handleS3PresignedUrl(
metadata.purpose = 'knowledge-base'
} else if (uploadType === 'chat') {
metadata.purpose = 'chat'
} else if (uploadType === 'copilot') {
metadata.purpose = 'copilot'
metadata.userId = userId || ''
}
const command = new PutObjectCommand({
@@ -166,6 +215,7 @@ async function handleS3PresignedUrl(
let presignedUrl: string
try {
const { getS3Client } = await import('@/lib/uploads/s3/s3-client')
presignedUrl = await getSignedUrl(getS3Client(), command, { expiresIn: 3600 })
} catch (s3Error) {
logger.error('Failed to generate S3 presigned URL:', s3Error)
@@ -210,7 +260,8 @@ async function handleBlobPresignedUrl(
fileName: string,
contentType: string,
fileSize: number,
uploadType: UploadType
uploadType: UploadType,
userId?: string
) {
try {
const config =
@@ -218,7 +269,9 @@ async function handleBlobPresignedUrl(
? BLOB_KB_CONFIG
: uploadType === 'chat'
? BLOB_CHAT_CONFIG
: BLOB_CONFIG
: uploadType === 'copilot'
? BLOB_COPILOT_CONFIG
: BLOB_CONFIG
if (
!config.accountName ||
@@ -229,9 +282,19 @@ async function handleBlobPresignedUrl(
}
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
const prefix = uploadType === 'knowledge-base' ? 'kb/' : uploadType === 'chat' ? 'chat/' : ''
const uniqueKey = `${prefix}${Date.now()}-${uuidv4()}-${safeFileName}`
let prefix = ''
if (uploadType === 'knowledge-base') {
prefix = 'kb/'
} else if (uploadType === 'chat') {
prefix = 'chat/'
} else if (uploadType === 'copilot') {
prefix = `${userId}/`
}
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
const { getBlobServiceClient } = await import('@/lib/uploads/blob/blob-client')
const blobServiceClient = getBlobServiceClient()
const containerClient = blobServiceClient.getContainerClient(config.containerName)
const blockBlobClient = containerClient.getBlockBlobClient(uniqueKey)
@@ -282,6 +345,9 @@ async function handleBlobPresignedUrl(
uploadHeaders['x-ms-meta-purpose'] = 'knowledge-base'
} else if (uploadType === 'chat') {
uploadHeaders['x-ms-meta-purpose'] = 'chat'
} else if (uploadType === 'copilot') {
uploadHeaders['x-ms-meta-purpose'] = 'copilot'
uploadHeaders['x-ms-meta-userid'] = encodeURIComponent(userId || '')
}
return NextResponse.json({

View File

@@ -13,8 +13,6 @@ import {
getContentType,
} from '@/app/api/files/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('FilesServeAPI')
async function streamToBuffer(readableStream: NodeJS.ReadableStream): Promise<Buffer> {
@@ -58,7 +56,11 @@ export async function GET(
if (isUsingCloudStorage() || isCloudPath) {
// Extract the actual key (remove 's3/' or 'blob/' prefix if present)
const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath
return await handleCloudProxy(cloudKey)
// Get bucket type from query parameter
const bucketType = request.nextUrl.searchParams.get('bucket')
return await handleCloudProxy(cloudKey, bucketType)
}
// Use local handler for local files
@@ -152,12 +154,37 @@ async function downloadKBFile(cloudKey: string): Promise<Buffer> {
/**
* Proxy cloud file through our server
*/
async function handleCloudProxy(cloudKey: string): Promise<NextResponse> {
async function handleCloudProxy(
cloudKey: string,
bucketType?: string | null
): Promise<NextResponse> {
try {
// Check if this is a KB file (starts with 'kb/')
const isKBFile = cloudKey.startsWith('kb/')
const fileBuffer = isKBFile ? await downloadKBFile(cloudKey) : await downloadFile(cloudKey)
let fileBuffer: Buffer
if (isKBFile) {
fileBuffer = await downloadKBFile(cloudKey)
} else if (bucketType === 'copilot') {
// Download from copilot-specific bucket
const storageProvider = getStorageProvider()
if (storageProvider === 's3') {
const { downloadFromS3WithConfig } = await import('@/lib/uploads/s3/s3-client')
const { S3_COPILOT_CONFIG } = await import('@/lib/uploads/setup')
fileBuffer = await downloadFromS3WithConfig(cloudKey, S3_COPILOT_CONFIG)
} else if (storageProvider === 'blob') {
// For Azure Blob, use the default downloadFile for now
// TODO: Add downloadFromBlobWithConfig when needed
fileBuffer = await downloadFile(cloudKey)
} else {
fileBuffer = await downloadFile(cloudKey)
}
} else {
// Default bucket
fileBuffer = await downloadFile(cloudKey)
}
// Extract the original filename from the key (last part after last /)
const originalFilename = cloudKey.split('/').pop() || 'download'

View File

@@ -26,7 +26,9 @@ describe('File Upload API Route', () => {
beforeEach(() => {
vi.resetModules()
vi.doMock('@/lib/uploads/setup.server', () => ({}))
vi.doMock('@/lib/uploads/setup.server', () => ({
UPLOAD_DIR_SERVER: '/tmp/test-uploads',
}))
})
afterEach(() => {
@@ -52,6 +54,12 @@ describe('File Upload API Route', () => {
const response = await POST(req)
const data = await response.json()
// Log error details if test fails
if (response.status !== 200) {
console.error('Upload failed with status:', response.status)
console.error('Error response:', data)
}
expect(response.status).toBe(200)
expect(data).toHaveProperty('path')
expect(data.path).toMatch(/\/api\/files\/serve\/.*\.txt$/)
@@ -59,8 +67,9 @@ describe('File Upload API Route', () => {
expect(data).toHaveProperty('size')
expect(data).toHaveProperty('type', 'text/plain')
const fs = await import('fs/promises')
expect(fs.writeFile).toHaveBeenCalled()
// Verify the upload function was called (we're mocking at the uploadFile level)
const { uploadFile } = await import('@/lib/uploads')
expect(uploadFile).toHaveBeenCalled()
})
it('should upload a file to S3 when in S3 mode', async () => {

View File

@@ -1,11 +1,8 @@
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { isUsingCloudStorage, uploadFile } from '@/lib/uploads'
import { UPLOAD_DIR } from '@/lib/uploads/setup'
import { getPresignedUrl, isUsingCloudStorage, uploadFile } from '@/lib/uploads'
import '@/lib/uploads/setup.server'
import { getSession } from '@/lib/auth'
import {
createErrorResponse,
createOptionsResponse,
@@ -18,6 +15,11 @@ const logger = createLogger('FilesUploadAPI')
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const formData = await request.formData()
// Check if multiple files are being uploaded or a single file
@@ -27,10 +29,21 @@ export async function POST(request: NextRequest) {
throw new InvalidRequestError('No files provided')
}
// Get optional scoping parameters for execution-scoped storage
const workflowId = formData.get('workflowId') as string | null
const executionId = formData.get('executionId') as string | null
const workspaceId = formData.get('workspaceId') as string | null
// Log storage mode
const usingCloudStorage = isUsingCloudStorage()
logger.info(`Using storage mode: ${usingCloudStorage ? 'Cloud' : 'Local'} for file upload`)
if (workflowId && executionId) {
logger.info(
`Uploading files for execution-scoped storage: workflow=${workflowId}, execution=${executionId}`
)
}
const uploadResults = []
// Process each file
@@ -39,33 +52,60 @@ export async function POST(request: NextRequest) {
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
if (usingCloudStorage) {
// Upload to cloud storage (S3 or Azure Blob)
try {
logger.info(`Uploading file to cloud storage: ${originalName}`)
const result = await uploadFile(buffer, originalName, file.type, file.size)
logger.info(`Successfully uploaded to cloud storage: ${result.key}`)
uploadResults.push(result)
} catch (error) {
logger.error('Error uploading to cloud storage:', error)
throw error
// For execution-scoped files, use the dedicated execution file storage
if (workflowId && executionId) {
// Use the dedicated execution file storage system
const { uploadExecutionFile } = await import('@/lib/workflows/execution-file-storage')
const userFile = await uploadExecutionFile(
{
workspaceId: workspaceId || '',
workflowId,
executionId,
},
buffer,
originalName,
file.type
)
uploadResults.push(userFile)
continue
}
// Upload to cloud or local storage using the standard uploadFile function
try {
logger.info(`Uploading file: ${originalName}`)
const result = await uploadFile(buffer, originalName, file.type, file.size)
// Generate a presigned URL for cloud storage with appropriate expiry
// Regular files get 24 hours (execution files are handled above)
let presignedUrl: string | undefined
if (usingCloudStorage) {
try {
presignedUrl = await getPresignedUrl(result.key, 24 * 60 * 60) // 24 hours
} catch (error) {
logger.warn(`Failed to generate presigned URL for ${originalName}:`, error)
}
}
} else {
// Upload to local file system in development
const extension = originalName.split('.').pop() || ''
const uniqueFilename = `${uuidv4()}.${extension}`
const filePath = join(UPLOAD_DIR, uniqueFilename)
logger.info(`Uploading file to local storage: ${filePath}`)
await writeFile(filePath, buffer)
logger.info(`Successfully wrote file to: ${filePath}`)
// Create the serve path
const servePath = `/api/files/serve/${result.key}`
uploadResults.push({
path: `/api/files/serve/${uniqueFilename}`,
const uploadResult = {
name: originalName,
size: file.size,
type: file.type,
})
key: result.key,
path: servePath,
url: presignedUrl || servePath,
uploadedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
}
logger.info(`Successfully uploaded: ${result.key}`)
uploadResults.push(uploadResult)
} catch (error) {
logger.error(`Error uploading ${originalName}:`, error)
throw error
}
}

View File

@@ -2,9 +2,6 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowFolder } from '@/db/schema'

View File

@@ -8,8 +8,6 @@ import { workflowFolder } from '@/db/schema'
const logger = createLogger('FoldersAPI')
export const dynamic = 'force-dynamic'
// GET - Fetch folders for a workspace
export async function GET(request: NextRequest) {
try {

View File

@@ -1,4 +1,4 @@
import crypto from 'node:crypto'
import { createHash, randomUUID } from 'crypto'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -22,7 +22,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = randomUUID().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {
@@ -70,7 +70,7 @@ export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = randomUUID().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {
@@ -119,10 +119,7 @@ export async function PUT(
updateData.contentLength = validatedData.content.length
// Update token count estimation (rough approximation: 4 chars per token)
updateData.tokenCount = Math.ceil(validatedData.content.length / 4)
updateData.chunkHash = crypto
.createHash('sha256')
.update(validatedData.content)
.digest('hex')
updateData.chunkHash = createHash('sha256').update(validatedData.content).digest('hex')
}
if (validatedData.enabled !== undefined) updateData.enabled = validatedData.enabled
@@ -166,7 +163,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string; chunkId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = randomUUID().slice(0, 8)
const { id: knowledgeBaseId, documentId, chunkId } = await params
try {

View File

@@ -3,7 +3,11 @@ import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { MAX_TAG_SLOTS, TAG_SLOTS } from '@/lib/constants/knowledge'
import {
getMaxSlotsForFieldType,
getSlotsForFieldType,
SUPPORTED_FIELD_TYPES,
} from '@/lib/constants/knowledge'
import { createLogger } from '@/lib/logs/console/logger'
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
import { db } from '@/db'
@@ -14,17 +18,60 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('DocumentTagDefinitionsAPI')
const TagDefinitionSchema = z.object({
tagSlot: z.enum(TAG_SLOTS as [string, ...string[]]),
tagSlot: z.string(), // Will be validated against field type slots
displayName: z.string().min(1, 'Display name is required').max(100, 'Display name too long'),
fieldType: z.string().default('text'), // Currently only 'text', future: 'date', 'number', 'range'
fieldType: z.enum(SUPPORTED_FIELD_TYPES as [string, ...string[]]).default('text'),
// Optional: for editing existing definitions
_originalDisplayName: z.string().optional(),
})
const BulkTagDefinitionsSchema = z.object({
definitions: z
.array(TagDefinitionSchema)
.max(MAX_TAG_SLOTS, `Cannot define more than ${MAX_TAG_SLOTS} tags`),
definitions: z.array(TagDefinitionSchema),
})
// Helper function to get the next available slot for a knowledge base and field type
async function getNextAvailableSlot(
knowledgeBaseId: string,
fieldType: string,
existingBySlot?: Map<string, any>
): Promise<string | null> {
// Get available slots for this field type
const availableSlots = getSlotsForFieldType(fieldType)
let usedSlots: Set<string>
if (existingBySlot) {
// Use provided map if available (for performance in batch operations)
// Filter by field type
usedSlots = new Set(
Array.from(existingBySlot.entries())
.filter(([_, def]) => def.fieldType === fieldType)
.map(([slot, _]) => slot)
)
} else {
// Query database for existing tag definitions of the same field type
const existingDefinitions = await db
.select({ tagSlot: knowledgeBaseTagDefinitions.tagSlot })
.from(knowledgeBaseTagDefinitions)
.where(
and(
eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId),
eq(knowledgeBaseTagDefinitions.fieldType, fieldType)
)
)
usedSlots = new Set(existingDefinitions.map((def) => def.tagSlot))
}
// Find the first available slot for this field type
for (const slot of availableSlots) {
if (!usedSlots.has(slot)) {
return slot
}
}
return null // No available slots for this field type
}
// Helper function to clean up unused tag definitions
async function cleanupUnusedTagDefinitions(knowledgeBaseId: string, requestId: string) {
try {
@@ -191,35 +238,93 @@ export async function POST(
const validatedData = BulkTagDefinitionsSchema.parse(body)
// Validate no duplicate tag slots
const tagSlots = validatedData.definitions.map((def) => def.tagSlot)
const uniqueTagSlots = new Set(tagSlots)
if (tagSlots.length !== uniqueTagSlots.size) {
return NextResponse.json({ error: 'Duplicate tag slots not allowed' }, { status: 400 })
// Validate slots are valid for their field types
for (const definition of validatedData.definitions) {
const validSlots = getSlotsForFieldType(definition.fieldType)
if (validSlots.length === 0) {
return NextResponse.json(
{ error: `Unsupported field type: ${definition.fieldType}` },
{ status: 400 }
)
}
if (!validSlots.includes(definition.tagSlot)) {
return NextResponse.json(
{
error: `Invalid slot '${definition.tagSlot}' for field type '${definition.fieldType}'. Valid slots: ${validSlots.join(', ')}`,
},
{ status: 400 }
)
}
}
// Validate no duplicate tag slots within the same field type
const slotsByFieldType = new Map<string, Set<string>>()
for (const definition of validatedData.definitions) {
if (!slotsByFieldType.has(definition.fieldType)) {
slotsByFieldType.set(definition.fieldType, new Set())
}
const slotsForType = slotsByFieldType.get(definition.fieldType)!
if (slotsForType.has(definition.tagSlot)) {
return NextResponse.json(
{
error: `Duplicate slot '${definition.tagSlot}' for field type '${definition.fieldType}'`,
},
{ status: 400 }
)
}
slotsForType.add(definition.tagSlot)
}
const now = new Date()
const createdDefinitions: (typeof knowledgeBaseTagDefinitions.$inferSelect)[] = []
// Get existing definitions count before transaction for cleanup check
// Get existing definitions
const existingDefinitions = await db
.select()
.from(knowledgeBaseTagDefinitions)
.where(eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId))
// Check if we're trying to create more tag definitions than available slots
const existingTagNames = new Set(existingDefinitions.map((def) => def.displayName))
const trulyNewTags = validatedData.definitions.filter(
(def) => !existingTagNames.has(def.displayName)
)
// Group by field type for validation
const existingByFieldType = new Map<string, number>()
for (const def of existingDefinitions) {
existingByFieldType.set(def.fieldType, (existingByFieldType.get(def.fieldType) || 0) + 1)
}
if (existingDefinitions.length + trulyNewTags.length > MAX_TAG_SLOTS) {
return NextResponse.json(
{
error: `Cannot create ${trulyNewTags.length} new tags. Knowledge base already has ${existingDefinitions.length} tag definitions. Maximum is ${MAX_TAG_SLOTS} total.`,
},
{ status: 400 }
// Validate we don't exceed limits per field type
const newByFieldType = new Map<string, number>()
for (const definition of validatedData.definitions) {
// Skip validation for edit operations - they don't create new slots
if (definition._originalDisplayName) {
continue
}
const existingTagNames = new Set(
existingDefinitions
.filter((def) => def.fieldType === definition.fieldType)
.map((def) => def.displayName)
)
if (!existingTagNames.has(definition.displayName)) {
newByFieldType.set(
definition.fieldType,
(newByFieldType.get(definition.fieldType) || 0) + 1
)
}
}
for (const [fieldType, newCount] of newByFieldType.entries()) {
const existingCount = existingByFieldType.get(fieldType) || 0
const maxSlots = getMaxSlotsForFieldType(fieldType)
if (existingCount + newCount > maxSlots) {
return NextResponse.json(
{
error: `Cannot create ${newCount} new '${fieldType}' tags. Knowledge base already has ${existingCount} '${fieldType}' tag definitions. Maximum is ${maxSlots} per field type.`,
},
{ status: 400 }
)
}
}
// Use transaction to ensure consistency
@@ -228,30 +333,51 @@ export async function POST(
const existingByName = new Map(existingDefinitions.map((def) => [def.displayName, def]))
const existingBySlot = new Map(existingDefinitions.map((def) => [def.tagSlot, def]))
// Process each new definition
// Process each definition
for (const definition of validatedData.definitions) {
if (definition._originalDisplayName) {
// This is an EDIT operation - find by original name and update
const originalDefinition = existingByName.get(definition._originalDisplayName)
if (originalDefinition) {
logger.info(
`[${requestId}] Editing tag definition: ${definition._originalDisplayName} -> ${definition.displayName} (slot ${originalDefinition.tagSlot})`
)
await tx
.update(knowledgeBaseTagDefinitions)
.set({
displayName: definition.displayName,
fieldType: definition.fieldType,
updatedAt: now,
})
.where(eq(knowledgeBaseTagDefinitions.id, originalDefinition.id))
createdDefinitions.push({
...originalDefinition,
displayName: definition.displayName,
fieldType: definition.fieldType,
updatedAt: now,
})
continue
}
logger.warn(
`[${requestId}] Could not find original definition for: ${definition._originalDisplayName}`
)
}
// Regular create/update logic
const existingByDisplayName = existingByName.get(definition.displayName)
const existingByTagSlot = existingBySlot.get(definition.tagSlot)
if (existingByDisplayName) {
// Update existing definition (same display name)
if (existingByDisplayName.tagSlot !== definition.tagSlot) {
// Slot is changing - check if target slot is available
if (existingByTagSlot && existingByTagSlot.id !== existingByDisplayName.id) {
// Target slot is occupied by a different definition - this is a conflict
// For now, keep the existing slot to avoid constraint violation
logger.warn(
`[${requestId}] Slot conflict for ${definition.displayName}: keeping existing slot ${existingByDisplayName.tagSlot}`
)
createdDefinitions.push(existingByDisplayName)
continue
}
}
// Display name exists - UPDATE operation
logger.info(
`[${requestId}] Updating existing tag definition: ${definition.displayName} (slot ${existingByDisplayName.tagSlot})`
)
await tx
.update(knowledgeBaseTagDefinitions)
.set({
tagSlot: definition.tagSlot,
fieldType: definition.fieldType,
updatedAt: now,
})
@@ -259,33 +385,32 @@ export async function POST(
createdDefinitions.push({
...existingByDisplayName,
tagSlot: definition.tagSlot,
fieldType: definition.fieldType,
updatedAt: now,
})
} else if (existingByTagSlot) {
// Slot is occupied by a different display name - update it
await tx
.update(knowledgeBaseTagDefinitions)
.set({
displayName: definition.displayName,
fieldType: definition.fieldType,
updatedAt: now,
})
.where(eq(knowledgeBaseTagDefinitions.id, existingByTagSlot.id))
createdDefinitions.push({
...existingByTagSlot,
displayName: definition.displayName,
fieldType: definition.fieldType,
updatedAt: now,
})
} else {
// Create new definition
// Display name doesn't exist - CREATE operation
const targetSlot = await getNextAvailableSlot(
knowledgeBaseId,
definition.fieldType,
existingBySlot
)
if (!targetSlot) {
logger.error(
`[${requestId}] No available slots for new tag definition: ${definition.displayName}`
)
continue
}
logger.info(
`[${requestId}] Creating new tag definition: ${definition.displayName} -> ${targetSlot}`
)
const newDefinition = {
id: randomUUID(),
knowledgeBaseId,
tagSlot: definition.tagSlot,
tagSlot: targetSlot as any,
displayName: definition.displayName,
fieldType: definition.fieldType,
createdAt: now,
@@ -293,7 +418,8 @@ export async function POST(
}
await tx.insert(knowledgeBaseTagDefinitions).values(newDefinition)
createdDefinitions.push(newDefinition)
existingBySlot.set(targetSlot as any, newDefinition)
createdDefinitions.push(newDefinition as any)
}
}
})

View File

@@ -376,7 +376,7 @@ describe('Knowledge Base Documents API Route', () => {
],
processingOptions: {
chunkSize: 50, // Invalid: too small
minCharactersPerChunk: 10, // Invalid: too small
minCharactersPerChunk: 0, // Invalid: too small
recipe: 'default',
lang: 'en',
chunkOverlap: 1000, // Invalid: too large

View File

@@ -1,9 +1,9 @@
import crypto from 'node:crypto'
import { randomUUID } from 'crypto'
import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { TAG_SLOTS } from '@/lib/constants/knowledge'
import { getSlotsForFieldType } from '@/lib/constants/knowledge'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserId } from '@/app/api/auth/oauth/utils'
import {
@@ -23,6 +23,48 @@ const PROCESSING_CONFIG = {
delayBetweenDocuments: 500,
}
// Helper function to get the next available slot for a knowledge base and field type
async function getNextAvailableSlot(
knowledgeBaseId: string,
fieldType: string,
existingBySlot?: Map<string, any>
): Promise<string | null> {
let usedSlots: Set<string>
if (existingBySlot) {
// Use provided map if available (for performance in batch operations)
// Filter by field type
usedSlots = new Set(
Array.from(existingBySlot.entries())
.filter(([_, def]) => def.fieldType === fieldType)
.map(([slot, _]) => slot)
)
} else {
// Query database for existing tag definitions of the same field type
const existingDefinitions = await db
.select({ tagSlot: knowledgeBaseTagDefinitions.tagSlot })
.from(knowledgeBaseTagDefinitions)
.where(
and(
eq(knowledgeBaseTagDefinitions.knowledgeBaseId, knowledgeBaseId),
eq(knowledgeBaseTagDefinitions.fieldType, fieldType)
)
)
usedSlots = new Set(existingDefinitions.map((def) => def.tagSlot))
}
// Find the first available slot for this field type
const availableSlots = getSlotsForFieldType(fieldType)
for (const slot of availableSlots) {
if (!usedSlots.has(slot)) {
return slot
}
}
return null // No available slots for this field type
}
// Helper function to process structured document tags
async function processDocumentTags(
knowledgeBaseId: string,
@@ -31,8 +73,9 @@ async function processDocumentTags(
): Promise<Record<string, string | null>> {
const result: Record<string, string | null> = {}
// Initialize all tag slots to null
TAG_SLOTS.forEach((slot) => {
// Initialize all text tag slots to null (only text type is supported currently)
const textSlots = getSlotsForFieldType('text')
textSlots.forEach((slot) => {
result[slot] = null
})
@@ -55,7 +98,7 @@ async function processDocumentTags(
if (!tag.tagName?.trim() || !tag.value?.trim()) continue
const tagName = tag.tagName.trim()
const fieldType = tag.fieldType || 'text'
const fieldType = tag.fieldType
const value = tag.value.trim()
let targetSlot: string | null = null
@@ -65,18 +108,13 @@ async function processDocumentTags(
if (existingDef) {
targetSlot = existingDef.tagSlot
} else {
// Find next available slot
for (const slot of TAG_SLOTS) {
if (!existingBySlot.has(slot)) {
targetSlot = slot
break
}
}
// Find next available slot using the helper function
targetSlot = await getNextAvailableSlot(knowledgeBaseId, fieldType, existingBySlot)
// Create new tag definition if we have a slot
if (targetSlot) {
const newDefinition = {
id: crypto.randomUUID(),
id: randomUUID(),
knowledgeBaseId,
tagSlot: targetSlot as any,
displayName: tagName,
@@ -257,7 +295,7 @@ const BulkCreateDocumentsSchema = z.object({
documents: z.array(CreateDocumentSchema),
processingOptions: z.object({
chunkSize: z.number().min(100).max(4000),
minCharactersPerChunk: z.number().min(50).max(2000),
minCharactersPerChunk: z.number().min(1).max(2000),
recipe: z.string(),
lang: z.string(),
chunkOverlap: z.number().min(0).max(500),
@@ -274,7 +312,7 @@ const BulkUpdateDocumentsSchema = z.object({
})
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = randomUUID().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {
@@ -385,7 +423,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
}
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = randomUUID().slice(0, 8)
const { id: knowledgeBaseId } = await params
try {
@@ -432,7 +470,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const createdDocuments = await db.transaction(async (tx) => {
const documentPromises = validatedData.documents.map(async (docData) => {
const documentId = crypto.randomUUID()
const documentId = randomUUID()
const now = new Date()
// Process documentTagsData if provided (for knowledge base block)
@@ -540,7 +578,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
try {
const validatedData = CreateDocumentSchema.parse(body)
const documentId = crypto.randomUUID()
const documentId = randomUUID()
const now = new Date()
// Process structured tag data if provided

Some files were not shown because too many files have changed in this diff Show More