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>
This commit is contained in:
Adam Gough
2025-08-06 10:27:21 -07:00
committed by GitHub
parent e43e78fb48
commit a3a5bf1d76
36 changed files with 4113 additions and 51 deletions

View File

@@ -29,9 +29,11 @@
"mem0",
"memory",
"microsoft_excel",
"microsoft_planner",
"microsoft_teams",
"mistral_parse",
"notion",
"onedrive",
"openai",
"outlook",
"perplexity",
@@ -41,6 +43,7 @@
"s3",
"schedule",
"serper",
"sharepoint",
"slack",
"stagehand",
"stagehand_agent",

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 |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Planner API |
| `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 |
| --------- | ---- | ----------- |
| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. |
| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. |
### `microsoft_planner_create_task`
Create a new task in Microsoft Planner
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the Microsoft Planner API |
| `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 |
| --------- | ---- | ----------- |
| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. |
| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. |
## Notes
- Category: `tools`
- Type: `microsoft_planner`

View File

@@ -0,0 +1,127 @@
---
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 |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the OneDrive API |
| `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 |
| --------- | ---- | ----------- |
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
### `onedrive_create_folder`
Create a new folder in OneDrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the OneDrive API |
| `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 |
| --------- | ---- | ----------- |
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
### `onedrive_list`
List files and folders in OneDrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the OneDrive 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 |
| `pageSize` | number | No | The number of files to return |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
## Notes
- Category: `tools`
- Type: `onedrive`

View File

@@ -0,0 +1,135 @@
---
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 |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the SharePoint API |
| `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 |
| --------- | ---- | ----------- |
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
### `sharepoint_read_page`
Read a specific page from a SharePoint site
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the SharePoint API |
| `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 |
| --------- | ---- | ----------- |
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
### `sharepoint_list_sites`
List details of all SharePoint sites
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | The access token for the SharePoint API |
| `siteSelector` | string | No | Select the SharePoint site |
| `groupId` | string | No | The group ID for accessing a group team site |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
## Notes
- Category: `tools`
- Type: `sharepoint`

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

@@ -1,4 +1,4 @@
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'
@@ -114,7 +114,7 @@ async function processDocumentTags(
// 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,
@@ -312,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 {
@@ -423,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 {
@@ -470,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)
@@ -578,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

View File

@@ -0,0 +1,110 @@
import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import type { PlannerTask } from '@/tools/microsoft_planner/types'
const logger = createLogger('MicrosoftPlannerTasksAPI')
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const planId = searchParams.get('planId')
if (!credentialId) {
logger.error(`[${requestId}] Missing credentialId parameter`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
if (!planId) {
logger.error(`[${requestId}] Missing planId parameter`)
return NextResponse.json({ error: 'Plan ID is required' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch tasks directly from Microsoft Graph API
const response = await fetch(`https://graph.microsoft.com/v1.0/planner/plans/${planId}/tasks`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorText = await response.text()
logger.error(`[${requestId}] Microsoft Graph API error:`, errorText)
return NextResponse.json(
{ error: 'Failed to fetch tasks from Microsoft Graph' },
{ status: response.status }
)
}
const data = await response.json()
const tasks = data.value || []
// Filter tasks to only include useful fields (matching our read_task tool)
const filteredTasks = tasks.map((task: PlannerTask) => ({
id: task.id,
title: task.title,
planId: task.planId,
bucketId: task.bucketId,
percentComplete: task.percentComplete,
priority: task.priority,
dueDateTime: task.dueDateTime,
createdDateTime: task.createdDateTime,
completedDateTime: task.completedDateTime,
hasDescription: task.hasDescription,
assignments: task.assignments ? Object.keys(task.assignments) : [],
}))
return NextResponse.json({
tasks: filteredTasks,
metadata: {
planId,
planUrl: `https://graph.microsoft.com/v1.0/planner/plans/${planId}`,
},
})
} catch (error) {
logger.error(`[${requestId}] Error fetching Microsoft Planner tasks:`, error)
return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 })
}
}

View File

@@ -0,0 +1,83 @@
import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('OneDriveFolderAPI')
/**
* Get a single folder from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const fileId = searchParams.get('fileId')
if (!credentialId || !fileId) {
return NextResponse.json({ error: 'Credential ID and File ID are required' }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
if (credential.userId !== session.user.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
const response = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${fileId}?$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch folder from OneDrive' },
{ status: response.status }
)
}
const folder = await response.json()
// Transform the response to match expected format
const transformedFolder = {
id: folder.id,
name: folder.name,
mimeType: 'application/vnd.microsoft.graph.folder',
webViewLink: folder.webUrl,
createdTime: folder.createdDateTime,
modifiedTime: folder.lastModifiedDateTime,
}
return NextResponse.json({ file: transformedFolder }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching folder from OneDrive`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,89 @@
import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('OneDriveFoldersAPI')
import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types'
/**
* Get folders from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query') || ''
if (!credentialId) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
if (credential.userId !== session.user.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build URL for OneDrive folders
let url = `https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=folder ne null&$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime&$top=50`
if (query) {
url += `&$search="${encodeURIComponent(query)}"`
}
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch folders from OneDrive' },
{ status: response.status }
)
}
const data = await response.json()
const folders = (data.value || [])
.filter((item: MicrosoftGraphDriveItem) => item.folder) // Only folders
.map((folder: MicrosoftGraphDriveItem) => ({
id: folder.id,
name: folder.name,
mimeType: 'application/vnd.microsoft.graph.folder',
webViewLink: folder.webUrl,
createdTime: folder.createdDateTime,
modifiedTime: folder.lastModifiedDateTime,
}))
return NextResponse.json({ files: folders }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching folders from OneDrive`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,105 @@
import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointSiteAPI')
/**
* Get a single SharePoint site from Microsoft Graph API
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const siteId = searchParams.get('siteId')
if (!credentialId || !siteId) {
return NextResponse.json({ error: 'Credential ID and Site ID are required' }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
if (credential.userId !== session.user.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Handle different ways to access SharePoint sites:
// 1. Site ID: sites/{site-id}
// 2. Root site: sites/root
// 3. Hostname: sites/{hostname}
// 4. Server-relative URL: sites/{hostname}:/{server-relative-path}
// 5. Group team site: groups/{group-id}/sites/root
let endpoint: string
if (siteId === 'root') {
endpoint = 'sites/root'
} else if (siteId.includes(':')) {
// Server-relative URL format
endpoint = `sites/${siteId}`
} else if (siteId.includes('groups/')) {
// Group team site format
endpoint = siteId
} else {
// Standard site ID or hostname
endpoint = `sites/${siteId}`
}
const response = await fetch(
`https://graph.microsoft.com/v1.0/${endpoint}?$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch site from SharePoint' },
{ status: response.status }
)
}
const site = await response.json()
// Transform the response to match expected format
const transformedSite = {
id: site.id,
name: site.displayName || site.name,
mimeType: 'application/vnd.microsoft.graph.site',
webViewLink: site.webUrl,
createdTime: site.createdDateTime,
modifiedTime: site.lastModifiedDateTime,
}
logger.info(`[${requestId}] Successfully fetched SharePoint site: ${transformedSite.name}`)
return NextResponse.json({ site: transformedSite }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching site from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,85 @@
import { randomUUID } from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import type { SharepointSite } from '@/tools/sharepoint/types'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointSitesAPI')
/**
* Get SharePoint sites from Microsoft Graph API
*/
export async function GET(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const query = searchParams.get('query') || ''
if (!credentialId) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
if (credential.userId !== session.user.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build URL for SharePoint sites
// Use search=* to get all sites the user has access to, or search for specific query
const searchQuery = query || '*'
const url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50`
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch sites from SharePoint' },
{ status: response.status }
)
}
const data = await response.json()
const sites = (data.value || []).map((site: SharepointSite) => ({
id: site.id,
name: site.displayName || site.name,
mimeType: 'application/vnd.microsoft.graph.site',
webViewLink: site.webUrl,
createdTime: site.createdDateTime,
modifiedTime: site.lastModifiedDateTime,
}))
logger.info(`[${requestId}] Successfully fetched ${sites.length} SharePoint sites`)
return NextResponse.json({ files: sites }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching sites from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -24,6 +24,7 @@ import {
parseProvider,
} from '@/lib/oauth'
import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal'
import type { PlannerTask } from '@/tools/microsoft_planner/types'
const logger = createLogger('MicrosoftFileSelector')
@@ -40,6 +41,9 @@ export interface MicrosoftFileInfo {
owners?: { displayName: string; emailAddress: string }[]
}
// Union type for items that can be displayed in the file selector
type SelectableItem = MicrosoftFileInfo | PlannerTask
interface MicrosoftFileSelectorProps {
value: string
onChange: (value: string, fileInfo?: MicrosoftFileInfo) => void
@@ -50,6 +54,7 @@ interface MicrosoftFileSelectorProps {
serviceId?: string
showPreview?: boolean
onFileInfoChange?: (fileInfo: MicrosoftFileInfo | null) => void
planId?: string
}
export function MicrosoftFileSelector({
@@ -62,6 +67,7 @@ export function MicrosoftFileSelector({
serviceId,
showPreview = true,
onFileInfoChange,
planId,
}: MicrosoftFileSelectorProps) {
const [open, setOpen] = useState(false)
const [credentials, setCredentials] = useState<Credential[]>([])
@@ -77,6 +83,11 @@ export function MicrosoftFileSelector({
const [credentialsLoaded, setCredentialsLoaded] = useState(false)
const initialFetchRef = useRef(false)
// Handle Microsoft Planner task selection
const [plannerTasks, setPlannerTasks] = useState<PlannerTask[]>([])
const [isLoadingTasks, setIsLoadingTasks] = useState(false)
const [selectedTask, setSelectedTask] = useState<PlannerTask | null>(null)
// Determine the appropriate service ID based on provider and scopes
const getServiceId = (): string => {
if (serviceId) return serviceId
@@ -128,7 +139,7 @@ export function MicrosoftFileSelector({
}
}, [provider, getProviderId, selectedCredentialId])
// Fetch available Excel files for the selected credential
// Fetch available files for the selected credential
const fetchAvailableFiles = useCallback(async () => {
if (!selectedCredentialId) return
@@ -143,7 +154,17 @@ export function MicrosoftFileSelector({
queryParams.append('query', searchQuery.trim())
}
const response = await fetch(`/api/auth/oauth/microsoft/files?${queryParams.toString()}`)
// Route to correct endpoint based on service
let endpoint: string
if (serviceId === 'onedrive') {
endpoint = `/api/tools/onedrive/folders?${queryParams.toString()}`
} else if (serviceId === 'sharepoint') {
endpoint = `/api/tools/sharepoint/sites?${queryParams.toString()}`
} else {
endpoint = `/api/auth/oauth/microsoft/files?${queryParams.toString()}`
}
const response = await fetch(endpoint)
if (response.ok) {
const data = await response.json()
@@ -160,7 +181,7 @@ export function MicrosoftFileSelector({
} finally {
setIsLoadingFiles(false)
}
}, [selectedCredentialId, searchQuery])
}, [selectedCredentialId, searchQuery, serviceId])
// Fetch a single file by ID when we have a selectedFileId but no metadata
const fetchFileById = useCallback(
@@ -175,7 +196,22 @@ export function MicrosoftFileSelector({
fileId: fileId,
})
const response = await fetch(`/api/auth/oauth/microsoft/file?${queryParams.toString()}`)
// Route to correct endpoint based on service
let endpoint: string
if (serviceId === 'onedrive') {
endpoint = `/api/tools/onedrive/folder?${queryParams.toString()}`
} else if (serviceId === 'sharepoint') {
// Change from fileId to siteId for SharePoint
const sharepointParams = new URLSearchParams({
credentialId: selectedCredentialId,
siteId: fileId, // Use siteId instead of fileId
})
endpoint = `/api/tools/sharepoint/site?${sharepointParams.toString()}`
} else {
endpoint = `/api/auth/oauth/microsoft/file?${queryParams.toString()}`
}
const response = await fetch(endpoint)
if (response.ok) {
const data = await response.json()
@@ -204,9 +240,77 @@ export function MicrosoftFileSelector({
setIsLoadingSelectedFile(false)
}
},
[selectedCredentialId, onFileInfoChange]
[selectedCredentialId, onFileInfoChange, serviceId]
)
// Fetch Microsoft Planner tasks when planId and credentials are available
const fetchPlannerTasks = useCallback(async () => {
if (!selectedCredentialId || !planId || serviceId !== 'microsoft-planner') {
logger.info('Skipping task fetch - missing requirements:', {
selectedCredentialId: !!selectedCredentialId,
planId: !!planId,
serviceId,
})
return
}
logger.info('Fetching Planner tasks with:', {
credentialId: selectedCredentialId,
planId,
serviceId,
})
setIsLoadingTasks(true)
try {
const queryParams = new URLSearchParams({
credentialId: selectedCredentialId,
planId: planId,
})
const url = `/api/tools/microsoft_planner/tasks?${queryParams.toString()}`
logger.info('Calling API endpoint:', url)
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
logger.info('Received task data:', data)
const tasks = data.tasks || []
// Transform tasks to match file info format for consistency
const transformedTasks = tasks.map((task: PlannerTask) => ({
id: task.id,
name: task.title,
mimeType: 'planner/task',
webViewLink: `https://tasks.office.com/planner/task/${task.id}`,
modifiedTime: task.createdDateTime,
createdTime: task.createdDateTime,
planId: task.planId,
bucketId: task.bucketId,
percentComplete: task.percentComplete,
priority: task.priority,
dueDateTime: task.dueDateTime,
}))
logger.info('Transformed tasks:', transformedTasks)
setPlannerTasks(transformedTasks)
} else {
const errorText = await response.text()
logger.error('API response not ok:', {
status: response.status,
statusText: response.statusText,
errorText,
})
setPlannerTasks([])
}
} catch (error) {
logger.error('Network/fetch error:', error)
setPlannerTasks([])
} finally {
setIsLoadingTasks(false)
}
}, [selectedCredentialId, planId, serviceId])
// Fetch credentials on initial mount
useEffect(() => {
if (!initialFetchRef.current) {
@@ -233,6 +337,35 @@ export function MicrosoftFileSelector({
}
}, [searchQuery, selectedCredentialId, fetchAvailableFiles])
// Fetch planner tasks when credentials and planId change
useEffect(() => {
if (serviceId === 'microsoft-planner' && selectedCredentialId && planId) {
fetchPlannerTasks()
}
}, [selectedCredentialId, planId, serviceId, fetchPlannerTasks])
// Handle task selection for planner
const handleTaskSelect = (task: PlannerTask) => {
const taskId = task.id || ''
// Convert PlannerTask to MicrosoftFileInfo format for compatibility
const taskAsFileInfo: MicrosoftFileInfo = {
id: taskId,
name: task.title,
mimeType: 'planner/task',
webViewLink: `https://tasks.office.com/planner/task/${taskId}`,
createdTime: task.createdDateTime,
modifiedTime: task.createdDateTime,
}
setSelectedFileId(taskId)
setSelectedFile(taskAsFileInfo)
setSelectedTask(task)
onChange(taskId, taskAsFileInfo)
onFileInfoChange?.(taskAsFileInfo)
setOpen(false)
setSearchQuery('')
}
// Keep internal selectedFileId in sync with the value prop
useEffect(() => {
if (value !== selectedFileId) {
@@ -276,7 +409,10 @@ export function MicrosoftFileSelector({
selectedCredentialId &&
credentialsLoaded &&
!selectedFile &&
!isLoadingSelectedFile
!isLoadingSelectedFile &&
serviceId !== 'microsoft-planner' &&
serviceId !== 'sharepoint' &&
serviceId !== 'onedrive'
) {
fetchFileById(value)
}
@@ -287,6 +423,7 @@ export function MicrosoftFileSelector({
selectedFile,
isLoadingSelectedFile,
fetchFileById,
serviceId,
])
// Handle selecting a file from the available files
@@ -324,6 +461,22 @@ export function MicrosoftFileSelector({
return <ExternalLink className='h-4 w-4' />
}
// Handle OneDrive specifically by checking serviceId
if (baseProvider === 'microsoft' && serviceId === 'onedrive') {
const onedriveService = baseProviderConfig.services.onedrive
if (onedriveService) {
return onedriveService.icon({ className: 'h-4 w-4' })
}
}
// Handle SharePoint specifically by checking serviceId
if (baseProvider === 'microsoft' && serviceId === 'sharepoint') {
const sharepointService = baseProviderConfig.services.sharepoint
if (sharepointService) {
return sharepointService.icon({ className: 'h-4 w-4' })
}
}
// For compound providers, find the specific service
if (providerName.includes('-')) {
for (const service of Object.values(baseProviderConfig.services)) {
@@ -383,6 +536,9 @@ export function MicrosoftFileSelector({
if (file.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
return <MicrosoftExcelIcon className={`${iconSize} text-green-600`} />
}
if (file.mimeType === 'planner/task') {
return getProviderIcon(provider)
}
// if (file.mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
// return <FileIcon className={`${iconSize} text-blue-600`} />
// }
@@ -397,6 +553,55 @@ export function MicrosoftFileSelector({
setSearchQuery(query)
}
const getFileTypeTitleCase = () => {
if (serviceId === 'onedrive') return 'Folders'
if (serviceId === 'sharepoint') return 'Sites'
if (serviceId === 'microsoft-planner') return 'Tasks'
return 'Excel Files'
}
const getSearchPlaceholder = () => {
if (serviceId === 'onedrive') return 'Search OneDrive folders...'
if (serviceId === 'sharepoint') return 'Search SharePoint sites...'
if (serviceId === 'microsoft-planner') return 'Search tasks...'
return 'Search Excel files...'
}
const getEmptyStateText = () => {
if (serviceId === 'onedrive') {
return {
title: 'No folders found.',
description: 'No folders were found in your OneDrive.',
}
}
if (serviceId === 'sharepoint') {
return {
title: 'No sites found.',
description: 'No SharePoint sites were found.',
}
}
if (serviceId === 'microsoft-planner') {
return {
title: 'No tasks found.',
description: 'No tasks were found in this plan.',
}
}
return {
title: 'No Excel files found.',
description: 'No .xlsx files were found in your OneDrive.',
}
}
// Filter tasks based on search query for planner
const filteredTasks: SelectableItem[] =
serviceId === 'microsoft-planner'
? plannerTasks.filter((task) => {
const title = task.title || ''
const query = searchQuery || ''
return title.toLowerCase().includes(query.toLowerCase())
})
: availableFiles
return (
<>
<div className='space-y-2'>
@@ -405,7 +610,7 @@ export function MicrosoftFileSelector({
onOpenChange={(isOpen) => {
setOpen(isOpen)
if (!isOpen) {
setSearchQuery('') // Clear search when popover closes
setSearchQuery('')
}
}}
>
@@ -415,7 +620,7 @@ export function MicrosoftFileSelector({
role='combobox'
aria-expanded={open}
className='h-10 w-full min-w-0 justify-between'
disabled={disabled}
disabled={disabled || (serviceId === 'microsoft-planner' && !planId)}
>
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
{selectedFile ? (
@@ -463,10 +668,10 @@ export function MicrosoftFileSelector({
)}
<Command>
<CommandInput placeholder='Search Excel files...' onValueChange={handleSearch} />
<CommandInput placeholder={getSearchPlaceholder()} onValueChange={handleSearch} />
<CommandList>
<CommandEmpty>
{isLoading || isLoadingFiles ? (
{isLoading || isLoadingFiles || isLoadingTasks ? (
<div className='flex items-center justify-center p-4'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='ml-2'>Loading...</span>
@@ -478,11 +683,18 @@ export function MicrosoftFileSelector({
Connect a {getProviderName(provider)} account to continue.
</p>
</div>
) : availableFiles.length === 0 ? (
) : serviceId === 'microsoft-planner' && !planId ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No Excel files found.</p>
<p className='font-medium text-sm'>Plan ID required.</p>
<p className='text-muted-foreground text-xs'>
No .xlsx files were found in your OneDrive.
Please enter a Plan ID first to see tasks.
</p>
</div>
) : filteredTasks.length === 0 ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>{getEmptyStateText().title}</p>
<p className='text-muted-foreground text-xs'>
{getEmptyStateText().description}
</p>
</div>
) : null}
@@ -510,32 +722,58 @@ export function MicrosoftFileSelector({
</CommandGroup>
)}
{/* Available Excel files - only show if we have credentials and files */}
{credentials.length > 0 && selectedCredentialId && availableFiles.length > 0 && (
{/* Available files/tasks - only show if we have credentials and items */}
{credentials.length > 0 && selectedCredentialId && filteredTasks.length > 0 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
Excel Files
{getFileTypeTitleCase()}
</div>
{availableFiles.map((file) => (
<CommandItem
key={file.id}
value={`file-${file.id}-${file.name}`}
onSelect={() => handleFileSelect(file)}
>
<div className='flex items-center gap-2 overflow-hidden'>
{getFileIcon(file, 'sm')}
<div className='min-w-0 flex-1'>
<span className='truncate font-normal'>{file.name}</span>
{file.modifiedTime && (
<div className='text-muted-foreground text-xs'>
Modified {new Date(file.modifiedTime).toLocaleDateString()}
</div>
{filteredTasks.map((item) => {
const isPlanner = serviceId === 'microsoft-planner'
const isPlannerTask = isPlanner && 'title' in item
const plannerTask = item as PlannerTask
const fileInfo = item as MicrosoftFileInfo
const displayName = isPlannerTask ? plannerTask.title : fileInfo.name
const dateField = isPlannerTask
? plannerTask.createdDateTime
: fileInfo.createdTime
return (
<CommandItem
key={item.id}
value={`file-${item.id}-${displayName}`}
onSelect={() =>
isPlannerTask
? handleTaskSelect(plannerTask)
: handleFileSelect(fileInfo)
}
>
<div className='flex items-center gap-2 overflow-hidden'>
{getFileIcon(
isPlannerTask
? {
...fileInfo,
id: plannerTask.id || '',
name: plannerTask.title,
mimeType: 'planner/task',
}
: fileInfo,
'sm'
)}
<div className='min-w-0 flex-1'>
<span className='truncate font-normal'>{displayName}</span>
{dateField && (
<div className='text-muted-foreground text-xs'>
Modified {new Date(dateField).toLocaleDateString()}
</div>
)}
</div>
</div>
</div>
{file.id === selectedFileId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
{item.id === selectedFileId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
)
})}
</CommandGroup>
)}
@@ -589,7 +827,13 @@ export function MicrosoftFileSelector({
className='flex items-center gap-1 text-primary text-xs hover:underline'
onClick={(e) => e.stopPropagation()}
>
<span>Open in OneDrive</span>
<span>
{serviceId === 'microsoft-planner'
? 'Open in Planner'
: serviceId === 'sharepoint'
? 'Open in SharePoint'
: 'Open in OneDrive'}
</span>
<ExternalLink className='h-3 w-3' />
</a>
) : (
@@ -600,7 +844,9 @@ export function MicrosoftFileSelector({
className='flex items-center gap-1 text-primary text-xs hover:underline'
onClick={(e) => e.stopPropagation()}
>
<span>Open in OneDrive</span>
<span>
{serviceId === 'sharepoint' ? 'Open in SharePoint' : 'Open in OneDrive'}
</span>
<ExternalLink className='h-3 w-3' />
</a>
)}

View File

@@ -68,8 +68,12 @@ export function FileSelectorInput({
const isDiscord = provider === 'discord'
const isMicrosoftTeams = provider === 'microsoft-teams'
const isMicrosoftExcel = provider === 'microsoft-excel'
const isMicrosoftWord = provider === 'microsoft-word'
const isMicrosoftOneDrive = provider === 'microsoft' && subBlock.serviceId === 'onedrive'
const isGoogleCalendar = subBlock.provider === 'google-calendar'
const isWealthbox = provider === 'wealthbox'
const isMicrosoftSharePoint = provider === 'microsoft' && subBlock.serviceId === 'sharepoint'
const isMicrosoftPlanner = provider === 'microsoft-planner'
// For Confluence and Jira, we need the domain and credentials
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
// For Discord, we need the bot token and server ID
@@ -94,6 +98,8 @@ export function FileSelectorInput({
setSelectedCalendarId(value)
} else if (isWealthbox) {
setSelectedWealthboxItemId(value)
} else if (isMicrosoftSharePoint) {
setSelectedFileId(value)
} else {
setSelectedFileId(value)
}
@@ -111,6 +117,8 @@ export function FileSelectorInput({
setSelectedCalendarId(value)
} else if (isWealthbox) {
setSelectedWealthboxItemId(value)
} else if (isMicrosoftSharePoint) {
setSelectedFileId(value)
} else {
setSelectedFileId(value)
}
@@ -125,6 +133,7 @@ export function FileSelectorInput({
isMicrosoftTeams,
isGoogleCalendar,
isWealthbox,
isMicrosoftSharePoint,
isPreview,
previewValue,
])
@@ -325,6 +334,141 @@ export function FileSelectorInput({
)
}
// Handle Microsoft Word selector
if (isMicrosoftWord) {
// Get credential using the same pattern as other tools
const credential = (getValue(blockId, 'credential') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<MicrosoftFileSelector
value={selectedFileId}
onChange={handleFileChange}
provider='microsoft-word'
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select Microsoft Word document'}
disabled={disabled || !credential}
showPreview={true}
onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select Microsoft Word credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// Handle Microsoft OneDrive selector
if (isMicrosoftOneDrive) {
const credential = (getValue(blockId, 'credential') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<MicrosoftFileSelector
value={selectedFileId}
onChange={handleFileChange}
provider='microsoft'
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select OneDrive folder'}
disabled={disabled || !credential}
showPreview={true}
onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select Microsoft credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// Handle Microsoft SharePoint selector
if (isMicrosoftSharePoint) {
const credential = (getValue(blockId, 'credential') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<MicrosoftFileSelector
value={selectedFileId}
onChange={handleFileChange}
provider='microsoft'
requiredScopes={subBlock.requiredScopes || []}
serviceId={subBlock.serviceId}
label={subBlock.placeholder || 'Select SharePoint site'}
disabled={disabled || !credential}
showPreview={true}
onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select SharePoint credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// Handle Microsoft Planner task selector
if (isMicrosoftPlanner) {
const credential = (getValue(blockId, 'credential') as string) || ''
const planId = (getValue(blockId, 'planId') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<MicrosoftFileSelector
value={selectedFileId}
onChange={handleFileChange}
provider='microsoft-planner'
requiredScopes={subBlock.requiredScopes || []}
serviceId='microsoft-planner'
label={subBlock.placeholder || 'Select task'}
disabled={disabled || !credential || !planId}
showPreview={true}
onFileInfoChange={setFileInfo as (info: MicrosoftFileInfo | null) => void}
planId={planId}
/>
</div>
</TooltipTrigger>
{!credential ? (
<TooltipContent side='top'>
<p>Please select Microsoft Planner credentials first</p>
</TooltipContent>
) : !planId ? (
<TooltipContent side='top'>
<p>Please enter a Plan ID first</p>
</TooltipContent>
) : null}
</Tooltip>
</TooltipProvider>
)
}
// Handle Microsoft Teams selector
if (isMicrosoftTeams) {
// Get credential using the same pattern as other tools

View File

@@ -0,0 +1,238 @@
import { MicrosoftPlannerIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types'
interface MicrosoftPlannerBlockParams {
credential: string
accessToken?: string
planId?: string
taskId?: string
title?: string
description?: string
dueDateTime?: string
assigneeUserId?: string
bucketId?: string
[key: string]: string | number | boolean | undefined
}
export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
type: 'microsoft_planner',
name: 'Microsoft Planner',
description: 'Read and create tasks in Microsoft Planner',
longDescription:
'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.',
docsLink: 'https://docs.sim.ai/tools/microsoft_planner',
category: 'tools',
bgColor: '#E0E0E0',
icon: MicrosoftPlannerIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Read Task', id: 'read_task' },
{ label: 'Create Task', id: 'create_task' },
],
},
{
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
layout: 'full',
provider: 'microsoft-planner',
serviceId: 'microsoft-planner',
requiredScopes: [
'openid',
'profile',
'email',
'Group.ReadWrite.All',
'Group.Read.All',
'Tasks.ReadWrite',
'offline_access',
],
placeholder: 'Select Microsoft account',
},
{
id: 'planId',
title: 'Plan ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the plan ID',
condition: { field: 'operation', value: ['create_task', 'read_task'] },
},
{
id: 'taskId',
title: 'Task ID',
type: 'file-selector',
layout: 'full',
placeholder: 'Select a task',
provider: 'microsoft-planner',
condition: { field: 'operation', value: ['read_task'] },
mode: 'basic',
},
// Advanced mode
{
id: 'taskId',
title: 'Manual Task ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the task ID',
condition: { field: 'operation', value: ['read_task'] },
mode: 'advanced',
},
{
id: 'title',
title: 'Task Title',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the task title',
condition: { field: 'operation', value: ['create_task'] },
},
{
id: 'description',
title: 'Description',
type: 'long-input',
layout: 'full',
placeholder: 'Enter task description (optional)',
condition: { field: 'operation', value: ['create_task'] },
},
{
id: 'dueDateTime',
title: 'Due Date',
type: 'short-input',
layout: 'full',
placeholder: 'Enter due date in ISO 8601 format (e.g., 2024-12-31T23:59:59Z)',
condition: { field: 'operation', value: ['create_task'] },
},
{
id: 'assigneeUserId',
title: 'Assignee User ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the user ID to assign this task to (optional)',
condition: { field: 'operation', value: ['create_task'] },
},
{
id: 'bucketId',
title: 'Bucket ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the bucket ID to organize the task (optional)',
condition: { field: 'operation', value: ['create_task'] },
},
],
tools: {
access: ['microsoft_planner_read_task', 'microsoft_planner_create_task'],
config: {
tool: (params) => {
switch (params.operation) {
case 'read_task':
return 'microsoft_planner_read_task'
case 'create_task':
return 'microsoft_planner_create_task'
default:
throw new Error(`Invalid Microsoft Planner operation: ${params.operation}`)
}
},
params: (params) => {
const {
credential,
operation,
planId,
taskId,
title,
description,
dueDateTime,
assigneeUserId,
bucketId,
...rest
} = params
const baseParams = {
...rest,
credential,
}
// For read operations
if (operation === 'read_task') {
const readParams: MicrosoftPlannerBlockParams = { ...baseParams }
// If taskId is provided, add it (highest priority - get specific task)
if (taskId?.trim()) {
readParams.taskId = taskId.trim()
}
// If no taskId but planId is provided, add planId (get tasks from plan)
else if (planId?.trim()) {
readParams.planId = planId.trim()
}
// If neither, get all user tasks (baseParams only)
return readParams
}
// For create operation
if (operation === 'create_task') {
if (!planId?.trim()) {
throw new Error('Plan ID is required to create a task.')
}
if (!title?.trim()) {
throw new Error('Task title is required to create a task.')
}
const createParams: MicrosoftPlannerBlockParams = {
...baseParams,
planId: planId.trim(),
title: title.trim(),
}
if (description?.trim()) {
createParams.description = description.trim()
}
if (dueDateTime?.trim()) {
createParams.dueDateTime = dueDateTime.trim()
}
if (assigneeUserId?.trim()) {
createParams.assigneeUserId = assigneeUserId.trim()
}
if (bucketId?.trim()) {
createParams.bucketId = bucketId.trim()
}
return createParams
}
return baseParams
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft account credential' },
planId: { type: 'string', description: 'Plan ID' },
taskId: { type: 'string', description: 'Task ID' },
title: { type: 'string', description: 'Task title' },
description: { type: 'string', description: 'Task description' },
dueDateTime: { type: 'string', description: 'Due date' },
assigneeUserId: { type: 'string', description: 'Assignee user ID' },
bucketId: { type: 'string', description: 'Bucket ID' },
},
outputs: {
task: {
type: 'json',
description:
'The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees.',
},
metadata: {
type: 'json',
description:
'Additional metadata about the operation, such as timestamps, request status, or other relevant information.',
},
},
}

View File

@@ -0,0 +1,235 @@
import { MicrosoftOneDriveIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { OneDriveResponse } from '@/tools/onedrive/types'
export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'onedrive',
name: 'OneDrive',
description: 'Create, upload, and list files',
longDescription:
'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.',
docsLink: 'https://docs.sim.ai/tools/onedrive',
category: 'tools',
bgColor: '#E0E0E0',
icon: MicrosoftOneDriveIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Create Folder', id: 'create_folder' },
{ label: 'Upload File', id: 'upload' },
{ label: 'List Files', id: 'list' },
],
},
// One Drive Credentials
{
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
layout: 'full',
provider: 'onedrive',
serviceId: 'onedrive',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
placeholder: 'Select Microsoft account',
},
// Upload Fields
{
id: 'fileName',
title: 'File Name',
type: 'short-input',
layout: 'full',
placeholder: 'Name of the file',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
layout: 'full',
placeholder: 'Content to upload to the file',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'folderSelector',
title: 'Select Parent Folder',
type: 'file-selector',
layout: 'full',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
mimeType: 'application/vnd.microsoft.graph.folder',
placeholder: 'Select a parent folder',
mode: 'basic',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'manualFolderId',
title: 'Parent Folder ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'folderName',
title: 'Folder Name',
type: 'short-input',
layout: 'full',
placeholder: 'Name for the new folder',
condition: { field: 'operation', value: 'create_folder' },
},
{
id: 'folderSelector',
title: 'Select Parent Folder',
type: 'file-selector',
layout: 'full',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
mimeType: 'application/vnd.microsoft.graph.folder',
placeholder: 'Select a parent folder',
mode: 'basic',
condition: { field: 'operation', value: 'create_folder' },
},
// Manual Folder ID input (advanced mode)
{
id: 'manualFolderId',
title: 'Parent Folder ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: 'create_folder' },
},
// List Fields - Folder Selector (basic mode)
{
id: 'folderSelector',
title: 'Select Folder',
type: 'file-selector',
layout: 'full',
provider: 'microsoft',
serviceId: 'onedrive',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
mimeType: 'application/vnd.microsoft.graph.folder',
placeholder: 'Select a folder to list files from',
mode: 'basic',
condition: { field: 'operation', value: 'list' },
},
// Manual Folder ID input (advanced mode)
{
id: 'manualFolderId',
title: 'Folder ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter folder ID (leave empty for root folder)',
mode: 'advanced',
condition: { field: 'operation', value: 'list' },
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
layout: 'full',
placeholder: 'Search for specific files (e.g., name contains "report")',
condition: { field: 'operation', value: 'list' },
},
{
id: 'pageSize',
title: 'Results Per Page',
type: 'short-input',
layout: 'full',
placeholder: 'Number of results (default: 100, max: 1000)',
condition: { field: 'operation', value: 'list' },
},
],
tools: {
access: ['onedrive_upload', 'onedrive_create_folder', 'onedrive_list'],
config: {
tool: (params) => {
switch (params.operation) {
case 'upload':
return 'onedrive_upload'
case 'create_folder':
return 'onedrive_create_folder'
case 'list':
return 'onedrive_list'
default:
throw new Error(`Invalid OneDrive operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, folderSelector, manualFolderId, mimeType, ...rest } = params
// Use folderSelector if provided, otherwise use manualFolderId
const effectiveFolderId = (folderSelector || manualFolderId || '').trim()
return {
accessToken: credential,
folderId: effectiveFolderId,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType,
...rest,
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft account credential' },
// Upload and Create Folder operation inputs
fileName: { type: 'string', description: 'File name' },
content: { type: 'string', description: 'File content' },
// Get Content operation inputs
// fileId: { type: 'string', required: false },
// List operation inputs
folderSelector: { type: 'string', description: 'Folder selector' },
manualFolderId: { type: 'string', description: 'Manual folder ID' },
query: { type: 'string', description: 'Search query' },
pageSize: { type: 'number', description: 'Results per page' },
},
outputs: {
file: {
type: 'json',
description: 'The OneDrive file object, including details such as id, name, size, and more.',
},
files: {
type: 'json',
description:
'An array of OneDrive file objects, each containing details such as id, name, size, and more.',
},
},
}

View File

@@ -0,0 +1,158 @@
import { MicrosoftSharepointIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { SharepointResponse } from '@/tools/sharepoint/types'
export const SharepointBlock: BlockConfig<SharepointResponse> = {
type: 'sharepoint',
name: 'Sharepoint',
description: 'Read and create pages',
longDescription:
'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.',
docsLink: 'https://docs.sim.ai/tools/sharepoint',
category: 'tools',
bgColor: '#E0E0E0',
icon: MicrosoftSharepointIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Create Page', id: 'create_page' },
{ label: 'Read Page', id: 'read_page' },
{ label: 'List Sites', id: 'list_sites' },
],
},
// Sharepoint Credentials
{
id: 'credential',
title: 'Microsoft Account',
type: 'oauth-input',
layout: 'full',
provider: 'sharepoint',
serviceId: 'sharepoint',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
placeholder: 'Select Microsoft account',
},
{
id: 'siteSelector',
title: 'Select Site',
type: 'file-selector',
layout: 'full',
provider: 'microsoft',
serviceId: 'sharepoint',
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
mimeType: 'application/vnd.microsoft.graph.folder',
placeholder: 'Select a site',
mode: 'basic',
condition: { field: 'operation', value: ['create_page', 'read_page', 'list_sites'] },
},
{
id: 'pageName',
title: 'Page Name',
type: 'short-input',
layout: 'full',
placeholder: 'Name of the page',
condition: { field: 'operation', value: ['create_page', 'read_page'] },
},
{
id: 'pageId',
title: 'Page ID',
type: 'short-input',
layout: 'full',
placeholder: 'Page ID (alternative to page name)',
condition: { field: 'operation', value: 'read_page' },
mode: 'advanced',
},
{
id: 'pageContent',
title: 'Page Content',
type: 'long-input',
layout: 'full',
placeholder: 'Content of the page',
condition: { field: 'operation', value: 'create_page' },
},
{
id: 'manualSiteId',
title: 'Site ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter site ID (leave empty for root site)',
mode: 'advanced',
condition: { field: 'operation', value: 'create_page' },
},
],
tools: {
access: ['sharepoint_create_page', 'sharepoint_read_page', 'sharepoint_list_sites'],
config: {
tool: (params) => {
switch (params.operation) {
case 'create_page':
return 'sharepoint_create_page'
case 'read_page':
return 'sharepoint_read_page'
case 'list_sites':
return 'sharepoint_list_sites'
default:
throw new Error(`Invalid Sharepoint operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params
// Use siteSelector if provided, otherwise use manualSiteId
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
return {
accessToken: credential,
siteId: effectiveSiteId,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
mimeType: mimeType,
...rest,
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft account credential' },
// Create Page operation inputs
pageName: { type: 'string', description: 'Page name' },
pageContent: { type: 'string', description: 'Page content' },
pageTitle: { type: 'string', description: 'Page title' },
// Read Page operation inputs
pageId: { type: 'string', description: 'Page ID' },
// List operation inputs
siteSelector: { type: 'string', description: 'Site selector' },
manualSiteId: { type: 'string', description: 'Manual site ID' },
pageSize: { type: 'number', description: 'Results per page' },
},
outputs: {
sites: {
type: 'json',
description:
'An array of SharePoint site objects, each containing details such as id, name, and more.',
},
},
}

View File

@@ -36,9 +36,11 @@ import { LinkupBlock } from '@/blocks/blocks/linkup'
import { Mem0Block } from '@/blocks/blocks/mem0'
import { MemoryBlock } from '@/blocks/blocks/memory'
import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel'
import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner'
import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams'
import { MistralParseBlock } from '@/blocks/blocks/mistral_parse'
import { NotionBlock } from '@/blocks/blocks/notion'
import { OneDriveBlock } from '@/blocks/blocks/onedrive'
import { OpenAIBlock } from '@/blocks/blocks/openai'
import { OutlookBlock } from '@/blocks/blocks/outlook'
import { PerplexityBlock } from '@/blocks/blocks/perplexity'
@@ -50,6 +52,7 @@ import { RouterBlock } from '@/blocks/blocks/router'
import { S3Block } from '@/blocks/blocks/s3'
import { ScheduleBlock } from '@/blocks/blocks/schedule'
import { SerperBlock } from '@/blocks/blocks/serper'
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
import { SlackBlock } from '@/blocks/blocks/slack'
import { StagehandBlock } from '@/blocks/blocks/stagehand'
import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent'
@@ -105,11 +108,13 @@ export const registry: Record<string, BlockConfig> = {
linkup: LinkupBlock,
mem0: Mem0Block,
microsoft_excel: MicrosoftExcelBlock,
microsoft_planner: MicrosoftPlannerBlock,
microsoft_teams: MicrosoftTeamsBlock,
mistral_parse: MistralParseBlock,
notion: NotionBlock,
openai: OpenAIBlock,
outlook: OutlookBlock,
onedrive: OneDriveBlock,
perplexity: PerplexityBlock,
pinecone: PineconeBlock,
qdrant: QdrantBlock,
@@ -120,6 +125,7 @@ export const registry: Record<string, BlockConfig> = {
schedule: ScheduleBlock,
s3: S3Block,
serper: SerperBlock,
sharepoint: SharepointBlock,
stagehand: StagehandBlock,
stagehand_agent: StagehandAgentBlock,
slack: SlackBlock,

View File

@@ -3181,3 +3181,166 @@ export function HunterIOIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function MicrosoftOneDriveIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} 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>
)
}
export function MicrosoftSharepointIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} 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>
)
}
export function MicrosoftPlannerIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} 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>
)
}

View File

@@ -441,6 +441,29 @@ export const auth = betterAuth({
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-excel`,
},
{
providerId: 'microsoft-planner',
clientId: env.MICROSOFT_CLIENT_ID as string,
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: [
'openid',
'profile',
'email',
'Group.ReadWrite.All',
'Group.Read.All',
'Tasks.ReadWrite',
'offline_access',
],
responseType: 'code',
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-planner`,
},
{
providerId: 'outlook',
@@ -467,6 +490,45 @@ export const auth = betterAuth({
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/outlook`,
},
{
providerId: 'onedrive',
clientId: env.MICROSOFT_CLIENT_ID as string,
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
responseType: 'code',
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/onedrive`,
},
{
providerId: 'sharepoint',
clientId: env.MICROSOFT_CLIENT_ID as string,
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: [
'openid',
'profile',
'email',
'Sites.Read.All',
'Sites.ReadWrite.All',
'offline_access',
],
responseType: 'code',
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/sharepoint`,
},
{
providerId: 'wealthbox',
clientId: env.WEALTHBOX_CLIENT_ID as string,

View File

@@ -14,6 +14,9 @@ import {
LinearIcon,
MicrosoftExcelIcon,
MicrosoftIcon,
MicrosoftOneDriveIcon,
MicrosoftPlannerIcon,
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
NotionIcon,
OutlookIcon,
@@ -62,12 +65,14 @@ export type OAuthService =
| 'discord'
| 'microsoft-excel'
| 'microsoft-teams'
| 'microsoft-planner'
| 'sharepoint'
| 'outlook'
| 'linear'
| 'slack'
| 'reddit'
| 'wealthbox'
| 'onedrive'
export interface OAuthProviderConfig {
id: OAuthProvider
name: string
@@ -159,6 +164,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
baseProviderIcon: (props) => MicrosoftIcon(props),
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
},
'microsoft-planner': {
id: 'microsoft-planner',
name: 'Microsoft Planner',
description: 'Connect to Microsoft Planner and manage tasks.',
providerId: 'microsoft-planner',
icon: (props) => MicrosoftPlannerIcon(props),
baseProviderIcon: (props) => MicrosoftIcon(props),
scopes: [
'openid',
'profile',
'email',
'Group.ReadWrite.All',
'Group.Read.All',
'Tasks.ReadWrite',
'offline_access',
],
},
'microsoft-teams': {
id: 'microsoft-teams',
name: 'Microsoft Teams',
@@ -201,6 +223,31 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'offline_access',
],
},
onedrive: {
id: 'onedrive',
name: 'OneDrive',
description: 'Connect to OneDrive and manage files.',
providerId: 'onedrive',
icon: (props) => MicrosoftOneDriveIcon(props),
baseProviderIcon: (props) => MicrosoftIcon(props),
scopes: ['openid', 'profile', 'email', 'Files.Read', 'Files.ReadWrite', 'offline_access'],
},
sharepoint: {
id: 'sharepoint',
name: 'SharePoint',
description: 'Connect to SharePoint and manage sites.',
providerId: 'sharepoint',
icon: (props) => MicrosoftSharepointIcon(props),
baseProviderIcon: (props) => MicrosoftIcon(props),
scopes: [
'openid',
'profile',
'email',
'Sites.Read.All',
'Sites.ReadWrite.All',
'offline_access',
],
},
},
defaultService: 'microsoft',
},
@@ -472,6 +519,12 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
return 'microsoft-teams'
} else if (provider === 'outlook') {
return 'outlook'
} else if (provider === 'sharepoint') {
return 'sharepoint'
} else if (provider === 'microsoft-planner') {
return 'microsoft-planner'
} else if (provider === 'onedrive') {
return 'onedrive'
} else if (provider === 'github') {
return 'github'
} else if (provider === 'supabase') {
@@ -543,6 +596,18 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig {
featureType: 'outlook',
}
}
if (provider === 'onedrive') {
return {
baseProvider: 'microsoft',
featureType: 'onedrive',
}
}
if (provider === 'sharepoint') {
return {
baseProvider: 'microsoft',
featureType: 'sharepoint',
}
}
// Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' })
const [base, feature] = provider.split('-')
@@ -712,6 +777,30 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
useBasicAuth: false,
}
}
case 'onedrive': {
const { clientId, clientSecret } = getCredentials(
env.MICROSOFT_CLIENT_ID,
env.MICROSOFT_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
clientId,
clientSecret,
useBasicAuth: false,
}
}
case 'sharepoint': {
const { clientId, clientSecret } = getCredentials(
env.MICROSOFT_CLIENT_ID,
env.MICROSOFT_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
clientId,
clientSecret,
useBasicAuth: false,
}
}
case 'linear': {
const { clientId, clientSecret } = getCredentials(
env.LINEAR_CLIENT_ID,

View File

@@ -0,0 +1,203 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
MicrosoftPlannerCreateResponse,
MicrosoftPlannerToolParams,
PlannerTask,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerCreateTask')
export const createTaskTool: ToolConfig<
MicrosoftPlannerToolParams,
MicrosoftPlannerCreateResponse
> = {
id: 'microsoft_planner_create_task',
name: 'Create Microsoft Planner Task',
description: 'Create a new task in Microsoft Planner',
version: '1.0',
oauth: {
required: true,
provider: 'microsoft-planner',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the plan where the task will be created',
},
title: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The title of the task',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The description of the task',
},
dueDateTime: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The due date and time for the task (ISO 8601 format)',
},
assigneeUserId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The user ID to assign the task to',
},
bucketId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The bucket ID to place the task in',
},
},
request: {
url: () => 'https://graph.microsoft.com/v1.0/planner/tasks',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.planId) {
throw new Error('Plan ID is required')
}
if (!params.title) {
throw new Error('Task title is required')
}
const body: PlannerTask = {
planId: params.planId,
title: params.title,
}
if (params.bucketId) {
body.bucketId = params.bucketId
}
if (params.dueDateTime) {
body.dueDateTime = params.dueDateTime
}
if (params.assigneeUserId) {
body.assignments = {
[params.assigneeUserId]: {
'@odata.type': 'microsoft.graph.plannerAssignment',
orderHint: ' !',
},
}
}
logger.info('Creating task with body:', body)
return body
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
const errorText =
errorJson.error && typeof errorJson.error === 'object'
? errorJson.error.message || JSON.stringify(errorJson.error)
: errorJson.error || response.statusText
throw new Error(`Failed to create Microsoft Planner task: ${errorText}`)
}
const task = await response.json()
logger.info('Created task:', task)
// If description was provided, update the task details
if (params?.description && task.id) {
try {
const detailsUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}/details`
// Get task details to get the ETag
const getDetailsResponse = await fetch(detailsUrl, {
headers: { Authorization: `Bearer ${params.accessToken}` },
})
const etag = getDetailsResponse.headers.get('ETag')
// Then update with correct ETag
const detailsResponse = await fetch(detailsUrl, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
'If-Match': etag || '*', // Use actual ETag or '*' if not available
},
body: JSON.stringify({
description: params.description,
}),
})
if (detailsResponse.ok) {
const details = await detailsResponse.json()
task.details = details
}
} catch (error) {
logger.warn('Failed to update task description:', error)
}
}
const result: MicrosoftPlannerCreateResponse = {
success: true,
output: {
task,
metadata: {
planId: task.planId,
taskId: task.id,
taskUrl: `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}`,
},
},
}
return result
},
transformError: (error) => {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'object' && error !== null) {
if (error.error) {
if (typeof error.error === 'string') {
return error.error
}
if (typeof error.error === 'object' && error.error.message) {
return error.error.message
}
return JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
try {
return `Microsoft Planner API error: ${JSON.stringify(error)}`
} catch (_e) {
return 'Microsoft Planner API error: Unable to parse error details'
}
}
return 'An error occurred while creating the Microsoft Planner task'
},
}

View File

@@ -0,0 +1,5 @@
import { createTaskTool } from '@/tools/microsoft_planner/create_task'
import { readTaskTool } from '@/tools/microsoft_planner/read_task'
export const microsoftPlannerCreateTaskTool = createTaskTool
export const microsoftPlannerReadTaskTool = readTaskTool

View File

@@ -0,0 +1,149 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
MicrosoftPlannerReadResponse,
MicrosoftPlannerToolParams,
} from '@/tools/microsoft_planner/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('MicrosoftPlannerReadTask')
export const readTaskTool: ToolConfig<MicrosoftPlannerToolParams, MicrosoftPlannerReadResponse> = {
id: 'microsoft_planner_read_task',
name: 'Read Microsoft Planner Tasks',
description:
'Read tasks from Microsoft Planner - get all user tasks or all tasks from a specific plan',
version: '1.0',
oauth: {
required: true,
provider: 'microsoft-planner',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Planner API',
},
planId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The ID of the plan to get tasks from (if not provided, gets all user tasks)',
},
taskId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The ID of the task to get',
},
},
request: {
url: (params) => {
let finalUrl: string
// If taskId is provided, get specific task
if (params.taskId) {
finalUrl = `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}`
}
// Else if planId is provided, get tasks from plan
else if (params.planId) {
finalUrl = `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/tasks`
}
// Else get all user tasks
else {
finalUrl = 'https://graph.microsoft.com/v1.0/me/planner/tasks'
}
logger.info('Microsoft Planner URL:', finalUrl)
return finalUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
logger.info('Access token present:', !!params.accessToken)
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const errorJson = await response.json().catch(() => ({ error: response.statusText }))
const errorText =
errorJson.error && typeof errorJson.error === 'object'
? errorJson.error.message || JSON.stringify(errorJson.error)
: errorJson.error || response.statusText
throw new Error(`Failed to read Microsoft Planner tasks: ${errorText}`)
}
const data = await response.json()
logger.info('Raw response data:', data)
// Handle single task vs multiple tasks response format
const rawTasks = params?.taskId ? [data] : data.value || []
// Filter tasks to only include useful fields
const tasks = rawTasks.map((task: any) => ({
id: task.id,
title: task.title,
planId: task.planId,
bucketId: task.bucketId,
percentComplete: task.percentComplete,
priority: task.priority,
dueDateTime: task.dueDateTime,
createdDateTime: task.createdDateTime,
completedDateTime: task.completedDateTime,
hasDescription: task.hasDescription,
assignments: task.assignments ? Object.keys(task.assignments) : [],
}))
const result: MicrosoftPlannerReadResponse = {
success: true,
output: {
tasks,
metadata: {
planId: params?.planId || '',
userId: params?.planId ? undefined : 'me',
planUrl: params?.planId
? `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}`
: undefined,
},
},
}
return result
},
transformError: (error) => {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'object' && error !== null) {
if (error.error) {
if (typeof error.error === 'string') {
return error.error
}
if (typeof error.error === 'object' && error.error.message) {
return error.error.message
}
return JSON.stringify(error.error)
}
if (error.message) {
return error.message
}
try {
return `Microsoft Planner API error: ${JSON.stringify(error)}`
} catch (_e) {
return 'Microsoft Planner API error: Unable to parse error details'
}
}
return 'An error occurred while reading Microsoft Planner tasks'
},
}

View File

@@ -0,0 +1,117 @@
import type { ToolResponse } from '@/tools/types'
export interface PlannerIdentitySet {
user?: {
displayName?: string
id?: string
}
application?: {
displayName?: string
id?: string
}
}
export interface PlannerAssignment {
'@odata.type': string
assignedDateTime?: string
orderHint?: string
assignedBy?: PlannerIdentitySet
}
export interface PlannerReference {
alias?: string
lastModifiedBy?: PlannerIdentitySet
lastModifiedDateTime?: string
previewPriority?: string
type?: string
}
export interface PlannerChecklistItem {
'@odata.type': string
isChecked?: boolean
title?: string
orderHint?: string
lastModifiedBy?: PlannerIdentitySet
lastModifiedDateTime?: string
}
export interface PlannerContainer {
containerId?: string
type?: string
url?: string
}
export interface PlannerTask {
id?: string
planId: string
title: string
orderHint?: string
assigneePriority?: string
percentComplete?: number
startDateTime?: string
createdDateTime?: string
dueDateTime?: string
hasDescription?: boolean
previewType?: string
completedDateTime?: string
completedBy?: PlannerIdentitySet
referenceCount?: number
checklistItemCount?: number
activeChecklistItemCount?: number
conversationThreadId?: string
priority?: number
assignments?: Record<string, PlannerAssignment>
bucketId?: string
details?: {
description?: string
references?: Record<string, PlannerReference>
checklist?: Record<string, PlannerChecklistItem>
}
}
export interface PlannerPlan {
id: string
title: string
owner?: string
createdDateTime?: string
container?: PlannerContainer
}
export interface MicrosoftPlannerMetadata {
planId?: string
taskId?: string
userId?: string
planUrl?: string
taskUrl?: string
}
export interface MicrosoftPlannerReadResponse extends ToolResponse {
output: {
tasks?: PlannerTask[]
task?: PlannerTask
plan?: PlannerPlan
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerCreateResponse extends ToolResponse {
output: {
task: PlannerTask
metadata: MicrosoftPlannerMetadata
}
}
export interface MicrosoftPlannerToolParams {
accessToken: string
planId?: string
taskId?: string
title?: string
description?: string
dueDateTime?: string
assigneeUserId?: string
bucketId?: string
priority?: number
percentComplete?: number
}
export type MicrosoftPlannerResponse = MicrosoftPlannerReadResponse | MicrosoftPlannerCreateResponse

View File

@@ -0,0 +1,88 @@
import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types'
import type { ToolConfig } from '@/tools/types'
export const createFolderTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse> = {
id: 'onedrive_create_folder',
name: 'Create Folder in OneDrive',
description: 'Create a new folder in OneDrive',
version: '1.0',
oauth: {
required: true,
provider: 'onedrive',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the OneDrive API',
},
folderName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the folder to create',
},
folderSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the parent folder to create the folder in',
},
folderId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'ID of the parent folder (internal use)',
},
},
request: {
url: (params) => {
// Use specific parent folder URL if parentId is provided
const parentFolderId = params.folderSelector || params.folderId
if (parentFolderId) {
return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}/children`
}
return 'https://graph.microsoft.com/v1.0/me/drive/root/children'
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
return {
name: params.folderName,
folder: {}, // Required facet for folder creation in Microsoft Graph API
'@microsoft.graph.conflictBehavior': 'rename', // Handle name conflicts
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(data.error?.message || 'Failed to create folder in OneDrive')
}
const data = await response.json()
return {
success: true,
output: {
file: {
id: data.id,
name: data.name,
mimeType: 'application/vnd.microsoft.graph.folder',
webViewLink: data.webUrl,
size: data.size,
createdTime: data.createdDateTime,
modifiedTime: data.lastModifiedDateTime,
parentReference: data.parentReference,
},
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while creating folder in OneDrive'
},
}

View File

@@ -0,0 +1,7 @@
import { createFolderTool } from '@/tools/onedrive/create_folder'
import { listTool } from '@/tools/onedrive/list'
import { uploadTool } from '@/tools/onedrive/upload'
export const onedriveCreateFolderTool = createFolderTool
export const onedriveListTool = listTool
export const onedriveUploadTool = uploadTool

View File

@@ -0,0 +1,120 @@
import type {
MicrosoftGraphDriveItem,
OneDriveListResponse,
OneDriveToolParams,
} from '@/tools/onedrive/types'
import type { ToolConfig } from '@/tools/types'
export const listTool: ToolConfig<OneDriveToolParams, OneDriveListResponse> = {
id: 'onedrive_list',
name: 'List OneDrive Files',
description: 'List files and folders in OneDrive',
version: '1.0',
oauth: {
required: true,
provider: 'onedrive',
additionalScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the OneDrive API',
},
folderSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the folder to list files from',
},
folderId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the folder to list files from (internal use)',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'A query to filter the files',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'The number of files to return',
},
},
request: {
url: (params) => {
// Use specific folder if provided, otherwise use root
const folderId = params.folderId || params.folderSelector
const baseUrl = folderId
? `https://graph.microsoft.com/v1.0/me/drive/items/${folderId}/children`
: 'https://graph.microsoft.com/v1.0/me/drive/root/children'
const url = new URL(baseUrl)
// Use Microsoft Graph $select parameter
url.searchParams.append(
'$select',
'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference'
)
// Add name filter if query provided
if (params.query) {
url.searchParams.append('$filter', `startswith(name,'${params.query}')`)
}
// Add pagination
if (params.pageSize) {
url.searchParams.append('$top', params.pageSize.toString())
}
// Remove the $skip logic entirely. Instead, use the full nextLink URL if provided
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to list OneDrive files')
}
return {
success: true,
output: {
files: data.value.map((item: MicrosoftGraphDriveItem) => ({
id: item.id,
name: item.name,
mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'),
webViewLink: item.webUrl,
webContentLink: item['@microsoft.graph.downloadUrl'],
size: item.size?.toString() || '0',
createdTime: item.createdDateTime,
modifiedTime: item.lastModifiedDateTime,
parents: item.parentReference ? [item.parentReference.id] : [],
})),
// Use the actual @odata.nextLink URL as the continuation token
nextPageToken: data['@odata.nextLink'] || undefined,
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while listing OneDrive files'
},
}

View File

@@ -0,0 +1,64 @@
import type { ToolResponse } from '@/tools/types'
export interface MicrosoftGraphDriveItem {
id: string
name: string
file?: {
mimeType: string
}
folder?: {
childCount: number
}
webUrl: string
createdDateTime: string
lastModifiedDateTime: string
size?: number
'@microsoft.graph.downloadUrl'?: string
parentReference?: {
id: string
driveId: string
path: string
}
}
export interface OneDriveFile {
id: string
name: string
mimeType: string
webViewLink?: string
webContentLink?: string
size?: string
createdTime?: string
modifiedTime?: string
parents?: string[]
}
export interface OneDriveListResponse extends ToolResponse {
output: {
files: OneDriveFile[]
nextPageToken?: string
}
}
export interface OneDriveUploadResponse extends ToolResponse {
output: {
file: OneDriveFile
}
}
export interface OneDriveToolParams {
accessToken: string
folderId?: string
folderSelector?: string
folderName?: string
fileId?: string
fileName?: string
content?: string
mimeType?: string
query?: string
pageSize?: number
pageToken?: string
exportMimeType?: string
}
export type OneDriveResponse = OneDriveUploadResponse | OneDriveListResponse

View File

@@ -0,0 +1,132 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { OneDriveToolParams, OneDriveUploadResponse } from '@/tools/onedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('OneDriveUploadTool')
export const uploadTool: ToolConfig<OneDriveToolParams, OneDriveUploadResponse> = {
id: 'onedrive_upload',
name: 'Upload to OneDrive',
description: 'Upload a file to OneDrive',
version: '1.0',
oauth: {
required: true,
provider: 'onedrive',
additionalScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the OneDrive API',
},
fileName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the file to upload',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The content of the file to upload',
},
folderSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the folder to upload the file to',
},
folderId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the folder to upload the file to (internal use)',
},
},
request: {
url: (params) => {
let fileName = params.fileName || 'untitled'
// Always create .txt files for text content
if (!fileName.endsWith('.txt')) {
// Remove any existing extensions and add .txt
fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt`
}
// Build the proper URL based on parent folder
const parentFolderId = params.folderSelector || params.folderId
if (parentFolderId && parentFolderId.trim() !== '') {
return `https://graph.microsoft.com/v1.0/me/drive/items/${parentFolderId}:/${fileName}:/content`
}
// Default to root folder
return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'text/plain',
}),
body: (params) => (params.content || '') as unknown as Record<string, unknown>,
},
transformResponse: async (response: Response, params?: OneDriveToolParams) => {
try {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to upload file to OneDrive', {
status: response.status,
statusText: response.statusText,
errorData,
})
throw new Error(errorData.error?.message || 'Failed to upload file to OneDrive')
}
// Microsoft Graph API returns the file metadata directly
const fileData = await response.json()
logger.info('Successfully uploaded file to OneDrive', {
fileId: fileData.id,
fileName: fileData.name,
})
return {
success: true,
output: {
file: {
id: fileData.id,
name: fileData.name,
mimeType: fileData.file?.mimeType || params?.mimeType || 'text/plain',
webViewLink: fileData.webUrl,
webContentLink: fileData['@microsoft.graph.downloadUrl'],
size: fileData.size,
createdTime: fileData.createdDateTime,
modifiedTime: fileData.lastModifiedDateTime,
parentReference: fileData.parentReference,
},
},
}
} catch (error: any) {
logger.error('Error in upload transformation', {
error: error.message,
stack: error.stack,
})
throw error
}
},
transformError: (error) => {
logger.error('Upload error', {
error: error.message,
stack: error.stack,
})
return error.message || 'An error occurred while uploading to OneDrive'
},
}

View File

@@ -79,6 +79,10 @@ import {
microsoftExcelTableAddTool,
microsoftExcelWriteTool,
} from '@/tools/microsoft_excel'
import {
microsoftPlannerCreateTaskTool,
microsoftPlannerReadTaskTool,
} from '@/tools/microsoft_planner'
import {
microsoftTeamsReadChannelTool,
microsoftTeamsReadChatTool,
@@ -95,6 +99,7 @@ import {
notionSearchTool,
notionWriteTool,
} from '@/tools/notion'
import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive'
import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai'
import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook'
import { perplexityChatTool } from '@/tools/perplexity'
@@ -109,6 +114,11 @@ import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdr
import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@/tools/reddit'
import { s3GetObjectTool } from '@/tools/s3'
import { searchTool as serperSearch } from '@/tools/serper'
import {
sharepointCreatePageTool,
sharepointListSitesTool,
sharepointReadPageTool,
} from '@/tools/sharepoint'
import { slackCanvasTool, slackMessageReaderTool, slackMessageTool } from '@/tools/slack'
import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand'
import {
@@ -265,9 +275,14 @@ export const tools: Record<string, ToolConfig> = {
outlook_draft: outlookDraftTool,
linear_read_issues: linearReadIssuesTool,
linear_create_issue: linearCreateIssueTool,
onedrive_create_folder: onedriveCreateFolderTool,
onedrive_list: onedriveListTool,
onedrive_upload: onedriveUploadTool,
microsoft_excel_read: microsoftExcelReadTool,
microsoft_excel_write: microsoftExcelWriteTool,
microsoft_excel_table_add: microsoftExcelTableAddTool,
microsoft_planner_create_task: microsoftPlannerCreateTaskTool,
microsoft_planner_read_task: microsoftPlannerReadTaskTool,
google_calendar_create: googleCalendarCreateTool,
google_calendar_get: googleCalendarGetTool,
google_calendar_list: googleCalendarListTool,
@@ -293,4 +308,7 @@ export const tools: Record<string, ToolConfig> = {
hunter_email_verifier: hunterEmailVerifierTool,
hunter_companies_find: hunterCompaniesFindTool,
hunter_email_count: hunterEmailCountTool,
sharepoint_create_page: sharepointCreatePageTool,
sharepoint_read_page: sharepointReadPageTool,
sharepoint_list_sites: sharepointListSitesTool,
}

View File

@@ -0,0 +1,157 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
SharepointCreatePageResponse,
SharepointPage,
SharepointToolParams,
} from '@/tools/sharepoint/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SharePointCreatePage')
export const createPageTool: ToolConfig<SharepointToolParams, SharepointCreatePageResponse> = {
id: 'sharepoint_create_page',
name: 'Create SharePoint Page',
description: 'Create a new page in a SharePoint site',
version: '1.0',
oauth: {
required: true,
provider: 'sharepoint',
additionalScopes: ['openid', 'profile', 'email', 'Sites.ReadWrite.All', 'offline_access'],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the SharePoint API',
},
siteId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the SharePoint site (internal use)',
},
siteSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the SharePoint site',
},
pageName: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The name of the page to create',
},
pageTitle: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The title of the page (defaults to page name if not provided)',
},
pageContent: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The content of the page',
},
},
request: {
url: (params) => {
// Use specific site if provided, otherwise use root site
const siteId = params.siteSelector || params.siteId || 'root'
return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
Accept: 'application/json',
}),
body: (params) => {
if (!params.pageName) {
throw new Error('Page name is required')
}
const pageTitle = params.pageTitle || params.pageName
// Basic page structure required by Microsoft Graph API
const pageData: SharepointPage = {
'@odata.type': '#microsoft.graph.sitePage',
name: params.pageName,
title: pageTitle,
publishingState: {
level: 'draft',
},
pageLayout: 'article',
}
// Add content if provided using the simple innerHtml approach from the documentation
if (params.pageContent) {
pageData.canvasLayout = {
horizontalSections: [
{
layout: 'oneColumn',
id: '1',
emphasis: 'none',
columns: [
{
id: '1',
width: 12,
webparts: [
{
id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
innerHtml: `<p>${params.pageContent.replace(/"/g, '&quot;').replace(/'/g, '&#39;')}</p>`,
},
],
},
],
},
],
}
}
return pageData
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('SharePoint page creation failed', {
status: response.status,
statusText: response.statusText,
error: data.error,
data,
})
throw new Error(
data.error?.message ||
`Failed to create SharePoint page: ${response.status} ${response.statusText}`
)
}
logger.info('SharePoint page created successfully', {
pageId: data.id,
pageName: data.name,
pageTitle: data.title,
})
return {
success: true,
output: {
page: {
id: data.id,
name: data.name,
title: data.title || data.name,
webUrl: data.webUrl,
pageLayout: data.pageLayout,
createdDateTime: data.createdDateTime,
lastModifiedDateTime: data.lastModifiedDateTime,
},
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while creating the SharePoint page'
},
}

View File

@@ -0,0 +1,7 @@
import { createPageTool } from '@/tools/sharepoint/create_page'
import { listSitesTool } from '@/tools/sharepoint/list_sites'
import { readPageTool } from '@/tools/sharepoint/read_page'
export const sharepointCreatePageTool = createPageTool
export const sharepointListSitesTool = listSitesTool
export const sharepointReadPageTool = readPageTool

View File

@@ -0,0 +1,117 @@
import type {
SharepointReadSiteResponse,
SharepointSite,
SharepointToolParams,
} from '@/tools/sharepoint/types'
import type { ToolConfig } from '@/tools/types'
export const listSitesTool: ToolConfig<SharepointToolParams, SharepointReadSiteResponse> = {
id: 'sharepoint_list_sites',
name: 'List SharePoint Sites',
description: 'List details of all SharePoint sites',
version: '1.0',
oauth: {
required: true,
provider: 'sharepoint',
additionalScopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'offline_access'],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the SharePoint API',
},
siteSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the SharePoint site',
},
groupId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The group ID for accessing a group team site',
},
},
request: {
url: (params) => {
let baseUrl: string
if (params.groupId) {
// Access group team site
baseUrl = `https://graph.microsoft.com/v1.0/groups/${params.groupId}/sites/root`
} else if (params.siteId || params.siteSelector) {
// Access specific site by ID
const siteId = params.siteId || params.siteSelector
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}`
} else {
// get all sites
baseUrl = 'https://graph.microsoft.com/v1.0/sites?search=*'
}
const url = new URL(baseUrl)
// Use Microsoft Graph $select parameter to get site details
url.searchParams.append(
'$select',
'id,name,displayName,webUrl,description,createdDateTime,lastModifiedDateTime,isPersonalSite,root,siteCollection'
)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to read SharePoint site(s)')
}
// Check if this is a search result (multiple sites) or single site
if (data.value && Array.isArray(data.value)) {
// Multiple sites from search
return {
success: true,
output: {
sites: data.value.map((site: SharepointSite) => ({
id: site.id,
name: site.name,
displayName: site.displayName,
webUrl: site.webUrl,
description: site.description,
createdDateTime: site.createdDateTime,
lastModifiedDateTime: site.lastModifiedDateTime,
})),
},
}
}
// Single site response
return {
success: true,
output: {
site: {
id: data.id,
name: data.name,
displayName: data.displayName,
webUrl: data.webUrl,
description: data.description,
createdDateTime: data.createdDateTime,
lastModifiedDateTime: data.lastModifiedDateTime,
isPersonalSite: data.isPersonalSite,
root: data.root,
siteCollection: data.siteCollection,
},
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while reading the SharePoint site'
},
}

View File

@@ -0,0 +1,325 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
GraphApiResponse,
SharepointPageContent,
SharepointReadPageResponse,
SharepointToolParams,
} from '@/tools/sharepoint/types'
import { cleanODataMetadata, extractTextFromCanvasLayout } from '@/tools/sharepoint/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SharePointReadPage')
export const readPageTool: ToolConfig<SharepointToolParams, SharepointReadPageResponse> = {
id: 'sharepoint_read_page',
name: 'Read SharePoint Page',
description: 'Read a specific page from a SharePoint site',
version: '1.0',
oauth: {
required: true,
provider: 'sharepoint',
additionalScopes: ['openid', 'profile', 'email', 'Sites.Read.All', 'offline_access'],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the SharePoint API',
},
siteSelector: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Select the SharePoint site',
},
siteId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the SharePoint site (internal use)',
},
pageId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The ID of the page to read',
},
pageName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The name of the page to read (alternative to pageId)',
},
maxPages: {
type: 'number',
required: false,
visibility: 'user-only',
description:
'Maximum number of pages to return when listing all pages (default: 10, max: 50)',
},
},
request: {
url: (params) => {
// Use specific site if provided, otherwise use root site
const siteId = params.siteId || params.siteSelector || 'root'
let baseUrl: string
if (params.pageId) {
// Read specific page by ID
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${params.pageId}`
} else {
// List all pages (with optional filtering by name)
baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages`
}
const url = new URL(baseUrl)
// Use Microsoft Graph $select parameter to get page details
// Only include valid properties for SharePoint pages
url.searchParams.append(
'$select',
'id,name,title,webUrl,pageLayout,createdDateTime,lastModifiedDateTime'
)
// If searching by name, add filter
if (params.pageName && !params.pageId) {
// Try to handle both with and without .aspx extension
const pageName = params.pageName
const pageNameWithAspx = pageName.endsWith('.aspx') ? pageName : `${pageName}.aspx`
// Search for exact match first, then with .aspx if needed
url.searchParams.append('$filter', `name eq '${pageName}' or name eq '${pageNameWithAspx}'`)
url.searchParams.append('$top', '10') // Get more results to find matches
} else if (!params.pageId && !params.pageName) {
// When listing all pages, apply maxPages limit
const maxPages = Math.min(params.maxPages || 10, 50) // Default 10, max 50
url.searchParams.append('$top', maxPages.toString())
}
// Only expand content when getting a specific page by ID
if (params.pageId) {
url.searchParams.append('$expand', 'canvasLayout')
}
const finalUrl = url.toString()
logger.info('SharePoint API URL', {
finalUrl,
siteId,
pageId: params.pageId,
pageName: params.pageName,
searchParams: Object.fromEntries(url.searchParams),
})
return finalUrl
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const data: GraphApiResponse = await response.json()
if (!response.ok) {
logger.error('SharePoint API error', {
status: response.status,
statusText: response.statusText,
error: data.error,
data,
})
throw new Error(data.error?.message || 'Failed to read SharePoint page')
}
logger.info('SharePoint API response', {
pageId: params?.pageId,
pageName: params?.pageName,
resultsCount: data.value?.length || (data.id ? 1 : 0),
hasDirectPage: !!data.id,
hasSearchResults: !!data.value,
})
if (params?.pageId) {
// Direct page access - return single page
const pageData = data
const contentData = {
content: extractTextFromCanvasLayout(data.canvasLayout),
canvasLayout: data.canvasLayout as any,
}
return {
success: true,
output: {
page: {
id: pageData.id!,
name: pageData.name!,
title: pageData.title || pageData.name!,
webUrl: pageData.webUrl!,
pageLayout: pageData.pageLayout,
createdDateTime: pageData.createdDateTime,
lastModifiedDateTime: pageData.lastModifiedDateTime,
},
content: contentData,
},
}
}
// Multiple pages or search by name
if (!data.value || data.value.length === 0) {
logger.error('No pages found', {
searchName: params?.pageName,
siteId: params?.siteId || params?.siteSelector || 'root',
totalResults: data.value?.length || 0,
})
const errorMessage = params?.pageName
? `Page with name '${params?.pageName}' not found. Make sure the page exists and you have access to it. Note: SharePoint page names typically include the .aspx extension.`
: 'No pages found on this SharePoint site.'
throw new Error(errorMessage)
}
logger.info('Found pages', {
searchName: params?.pageName,
foundPages: data.value.map((p: any) => ({ id: p.id, name: p.name, title: p.title })),
totalCount: data.value.length,
})
if (params?.pageName) {
// Search by name - return single page (first match)
const pageData = data.value[0]
const siteId = params?.siteId || params?.siteSelector || 'root'
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout`
logger.info('Making API call to get page content for searched page', {
pageId: pageData.id,
contentUrl,
siteId,
})
const contentResponse = await fetch(contentUrl, {
headers: {
Authorization: `Bearer ${params?.accessToken}`,
Accept: 'application/json',
},
})
let contentData: SharepointPageContent = { content: '' }
if (contentResponse.ok) {
const contentResult = await contentResponse.json()
contentData = {
content: extractTextFromCanvasLayout(contentResult.canvasLayout),
canvasLayout: cleanODataMetadata(contentResult.canvasLayout),
}
} else {
logger.error('Failed to fetch page content', {
status: contentResponse.status,
statusText: contentResponse.statusText,
})
}
return {
success: true,
output: {
page: {
id: pageData.id,
name: pageData.name,
title: pageData.title || pageData.name,
webUrl: pageData.webUrl,
pageLayout: pageData.pageLayout,
createdDateTime: pageData.createdDateTime,
lastModifiedDateTime: pageData.lastModifiedDateTime,
},
content: contentData,
},
}
}
// List all pages - return multiple pages with content
const siteId = params?.siteId || params?.siteSelector || 'root'
const pagesWithContent = []
logger.info('Fetching content for all pages', {
totalPages: data.value.length,
siteId,
})
// Fetch content for each page
for (const pageInfo of data.value) {
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageInfo.id}/microsoft.graph.sitePage?$expand=canvasLayout`
try {
const contentResponse = await fetch(contentUrl, {
headers: {
Authorization: `Bearer ${params?.accessToken}`,
Accept: 'application/json',
},
})
let contentData = { content: '', canvasLayout: null }
if (contentResponse.ok) {
const contentResult = await contentResponse.json()
contentData = {
content: extractTextFromCanvasLayout(contentResult.canvasLayout),
canvasLayout: cleanODataMetadata(contentResult.canvasLayout),
}
} else {
logger.error('Failed to fetch content for page', {
pageId: pageInfo.id,
pageName: pageInfo.name,
status: contentResponse.status,
})
}
pagesWithContent.push({
page: {
id: pageInfo.id,
name: pageInfo.name,
title: pageInfo.title || pageInfo.name,
webUrl: pageInfo.webUrl,
pageLayout: pageInfo.pageLayout,
createdDateTime: pageInfo.createdDateTime,
lastModifiedDateTime: pageInfo.lastModifiedDateTime,
},
content: contentData,
})
} catch (error) {
logger.error('Error fetching content for page', {
pageId: pageInfo.id,
pageName: pageInfo.name,
error: error instanceof Error ? error.message : String(error),
})
// Still add the page without content
pagesWithContent.push({
page: {
id: pageInfo.id,
name: pageInfo.name,
title: pageInfo.title || pageInfo.name,
webUrl: pageInfo.webUrl,
pageLayout: pageInfo.pageLayout,
createdDateTime: pageInfo.createdDateTime,
lastModifiedDateTime: pageInfo.lastModifiedDateTime,
},
content: { content: 'Failed to fetch content', canvasLayout: null },
})
}
}
logger.info('Completed fetching content for all pages', {
totalPages: pagesWithContent.length,
successfulPages: pagesWithContent.filter(
(p) => p.content.content !== 'Failed to fetch content'
).length,
})
return {
success: true,
output: {
pages: pagesWithContent,
totalPages: pagesWithContent.length,
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while reading the SharePoint page'
},
}

View File

@@ -0,0 +1,213 @@
import type { ToolResponse } from '@/tools/types'
export interface SharepointSite {
id: string
name: string
displayName: string
webUrl: string
description?: string
createdDateTime?: string
lastModifiedDateTime?: string
}
export interface SharepointPage {
'@odata.type'?: string
id?: string
name: string
title: string
webUrl?: string
pageLayout?: string
createdDateTime?: string
lastModifiedDateTime?: string
publishingState?: {
level: string
}
canvasLayout?: {
horizontalSections: Array<{
layout: string
id: string
emphasis: string
columns?: Array<{
id: string
width: number
webparts: Array<{
id: string
innerHtml: string
}>
}>
webparts?: Array<{
id: string
innerHtml: string
}>
}>
}
}
export interface SharepointPageContent {
content: string
canvasLayout?: {
horizontalSections: Array<{
layout: string
id: string
emphasis: string
webparts: Array<{
id: string
innerHtml: string
}>
}>
} | null
}
export interface SharepointListSitesResponse extends ToolResponse {
output: {
sites: SharepointSite[]
nextPageToken?: string
}
}
export interface SharepointCreatePageResponse extends ToolResponse {
output: {
page: SharepointPage
}
}
export interface SharepointPageWithContent {
page: SharepointPage
content: SharepointPageContent
}
export interface SharepointReadPageResponse extends ToolResponse {
output: {
page?: SharepointPage
pages?: SharepointPageWithContent[]
content?: SharepointPageContent
totalPages?: number
}
}
export interface SharepointReadSiteResponse extends ToolResponse {
output: {
site?: {
id: string
name: string
displayName: string
webUrl: string
description?: string
createdDateTime?: string
lastModifiedDateTime?: string
isPersonalSite?: boolean
root?: {
serverRelativeUrl: string
}
siteCollection?: {
hostname: string
}
}
sites?: Array<{
id: string
name: string
displayName: string
webUrl: string
description?: string
createdDateTime?: string
lastModifiedDateTime?: string
}>
}
}
export interface SharepointToolParams {
accessToken: string
siteId?: string
siteSelector?: string
pageId?: string
pageName?: string
pageContent?: string
pageTitle?: string
publishingState?: string
query?: string
pageSize?: number
pageToken?: string
hostname?: string
serverRelativePath?: string
groupId?: string
maxPages?: number
}
export interface GraphApiResponse {
id?: string
name?: string
title?: string
webUrl?: string
pageLayout?: string
createdDateTime?: string
lastModifiedDateTime?: string
canvasLayout?: CanvasLayout
value?: GraphApiPageItem[]
error?: {
message: string
}
}
export interface GraphApiPageItem {
id: string
name: string
title?: string
webUrl?: string
pageLayout?: string
createdDateTime?: string
lastModifiedDateTime?: string
}
export interface CanvasLayout {
horizontalSections?: Array<{
layout?: string
id?: string
emphasis?: string
columns?: Array<{
webparts?: Array<{
id?: string
innerHtml?: string
}>
}>
webparts?: Array<{
id?: string
innerHtml?: string
}>
}>
}
export interface SharepointReadSiteResponse extends ToolResponse {
output: {
site?: {
id: string
name: string
displayName: string
webUrl: string
description?: string
createdDateTime?: string
lastModifiedDateTime?: string
isPersonalSite?: boolean
root?: {
serverRelativeUrl: string
}
siteCollection?: {
hostname: string
}
}
sites?: Array<{
id: string
name: string
displayName: string
webUrl: string
description?: string
createdDateTime?: string
lastModifiedDateTime?: string
}>
}
}
export type SharepointResponse =
| SharepointListSitesResponse
| SharepointCreatePageResponse
| SharepointReadPageResponse
| SharepointReadSiteResponse

View File

@@ -0,0 +1,87 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { CanvasLayout } from '@/tools/sharepoint/types'
const logger = createLogger('SharepointUtils')
// Extract readable text from SharePoint canvas layout
export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | undefined): string {
logger.info('Extracting text from canvas layout', {
hasCanvasLayout: !!canvasLayout,
hasHorizontalSections: !!canvasLayout?.horizontalSections,
sectionsCount: canvasLayout?.horizontalSections?.length || 0,
})
if (!canvasLayout?.horizontalSections) {
logger.info('No canvas layout or horizontal sections found')
return ''
}
const textParts: string[] = []
for (const section of canvasLayout.horizontalSections) {
logger.info('Processing section', {
sectionId: section.id,
hasColumns: !!section.columns,
hasWebparts: !!section.webparts,
columnsCount: section.columns?.length || 0,
})
if (section.columns) {
for (const column of section.columns) {
if (column.webparts) {
for (const webpart of column.webparts) {
logger.info('Processing webpart', {
webpartId: webpart.id,
hasInnerHtml: !!webpart.innerHtml,
innerHtml: webpart.innerHtml,
})
if (webpart.innerHtml) {
// Extract text from HTML, removing tags
const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim()
if (text) {
textParts.push(text)
logger.info('Extracted text', { text })
}
}
}
}
}
} else if (section.webparts) {
for (const webpart of section.webparts) {
if (webpart.innerHtml) {
const text = webpart.innerHtml.replace(/<[^>]*>/g, '').trim()
if (text) textParts.push(text)
}
}
}
}
const finalContent = textParts.join('\n\n')
logger.info('Final extracted content', {
textPartsCount: textParts.length,
finalContentLength: finalContent.length,
finalContent,
})
return finalContent
}
// Remove OData metadata from objects
export function cleanODataMetadata<T>(obj: T): T {
if (!obj || typeof obj !== 'object') return obj
if (Array.isArray(obj)) {
return obj.map((item) => cleanODataMetadata(item)) as T
}
const cleaned: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
// Skip OData metadata keys
if (key.includes('@odata')) continue
cleaned[key] = cleanODataMetadata(value)
}
return cleaned as T
}