chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import type { SharepointAddListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
function resolveSanitizedFields(
|
||||
listItemFields: SharepointToolParams['listItemFields']
|
||||
): Record<string, unknown> {
|
||||
if (!listItemFields || Object.keys(listItemFields).length === 0) {
|
||||
throw new Error('listItemFields must not be empty')
|
||||
}
|
||||
|
||||
const providedFields =
|
||||
typeof listItemFields === 'object' &&
|
||||
listItemFields !== null &&
|
||||
'fields' in (listItemFields as Record<string, unknown>) &&
|
||||
Object.keys(listItemFields as Record<string, unknown>).length === 1
|
||||
? ((listItemFields as { fields: Record<string, unknown> }).fields as Record<string, unknown>)
|
||||
: (listItemFields as Record<string, unknown>)
|
||||
|
||||
if (!providedFields || Object.keys(providedFields).length === 0) {
|
||||
throw new Error('No fields provided to create the SharePoint list item')
|
||||
}
|
||||
|
||||
return sanitizeListItemFields(providedFields, { action: 'create' })
|
||||
}
|
||||
|
||||
export const addListItemTool: ToolConfig<SharepointToolParams, SharepointAddListItemResponse> = {
|
||||
id: 'sharepoint_add_list_items',
|
||||
name: 'Add SharePoint List Item',
|
||||
description: 'Add a new item to a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list to add the item to. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
listItemFields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Field values for the new list item',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const listId = optionalTrim(params.listId)
|
||||
if (!listId) {
|
||||
throw new Error('listId must be provided')
|
||||
}
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
fields: resolveSanitizedFields(params.listItemFields),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
let data: Record<string, unknown> | undefined
|
||||
try {
|
||||
data = await response.json()
|
||||
} catch {
|
||||
data = undefined
|
||||
}
|
||||
|
||||
const itemId = data?.id as string | undefined
|
||||
let fields = data?.fields as Record<string, unknown> | undefined
|
||||
if (!fields && params) {
|
||||
try {
|
||||
fields = resolveSanitizedFields(params.listItemFields)
|
||||
} catch {
|
||||
// Item was already created successfully; a malformed fallback input must not fail the response.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: {
|
||||
id: itemId || 'unknown',
|
||||
fields,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'object',
|
||||
description: 'Created SharePoint list item',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Field values for the new item' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import type {
|
||||
SharepointCreateListResponse,
|
||||
SharepointList,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointCreateList')
|
||||
|
||||
export const createListTool: ToolConfig<SharepointToolParams, SharepointCreateListResponse> = {
|
||||
id: 'sharepoint_create_list',
|
||||
name: 'Create SharePoint List',
|
||||
description: 'Create a new list in a SharePoint site',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
listDisplayName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Display name of the list to create. Example: Project Tasks or Customer Contacts',
|
||||
},
|
||||
listDescription: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Description of the list',
|
||||
},
|
||||
listTemplate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: "List template name (e.g., 'genericList')",
|
||||
},
|
||||
pageContent: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Optional JSON of columns. Either a top-level array of column definitions or an object with { columns: [...] }.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const listDisplayName = optionalTrim(params.listDisplayName)
|
||||
if (!listDisplayName) {
|
||||
throw new Error('listDisplayName is required')
|
||||
}
|
||||
|
||||
let columns: unknown[] | undefined
|
||||
if (params.pageContent) {
|
||||
if (typeof params.pageContent === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(params.pageContent)
|
||||
if (Array.isArray(parsed)) {
|
||||
columns = parsed
|
||||
} else if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
Array.isArray((parsed as { columns?: unknown[] }).columns)
|
||||
) {
|
||||
columns = (parsed as { columns: unknown[] }).columns
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Invalid JSON in pageContent for create list; ignoring', {
|
||||
error: toError(error).message,
|
||||
})
|
||||
}
|
||||
} else if (typeof params.pageContent === 'object') {
|
||||
const pageContent = params.pageContent as { columns?: unknown[] } | unknown[]
|
||||
if (Array.isArray(pageContent)) {
|
||||
columns = pageContent
|
||||
} else if (pageContent && Array.isArray(pageContent.columns)) {
|
||||
columns = pageContent.columns
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload: {
|
||||
displayName: string
|
||||
description?: string
|
||||
list: { template: string }
|
||||
columns?: unknown[]
|
||||
} = {
|
||||
displayName: listDisplayName,
|
||||
list: { template: optionalTrim(params.listTemplate) || 'genericList' },
|
||||
}
|
||||
const listDescription = optionalTrim(params.listDescription)
|
||||
if (listDescription) payload.description = listDescription
|
||||
if (columns && columns.length > 0) payload.columns = columns
|
||||
|
||||
logger.info('Creating SharePoint list', {
|
||||
displayName: payload.displayName,
|
||||
template: payload.list.template,
|
||||
hasDescription: !!payload.description,
|
||||
})
|
||||
|
||||
return payload
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const list: SharepointList = {
|
||||
id: data.id,
|
||||
displayName: data.displayName ?? data.name,
|
||||
name: data.name,
|
||||
webUrl: data.webUrl,
|
||||
createdDateTime: data.createdDateTime,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime,
|
||||
list: data.list,
|
||||
}
|
||||
|
||||
logger.info('SharePoint list created successfully', {
|
||||
listId: list.id,
|
||||
displayName: list.displayName,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { list },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
list: {
|
||||
type: 'object',
|
||||
description: 'Created SharePoint list information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the list' },
|
||||
displayName: { type: 'string', description: 'The display name of the list' },
|
||||
name: { type: 'string', description: 'The internal name of the list' },
|
||||
webUrl: { type: 'string', description: 'The web URL of the list' },
|
||||
createdDateTime: { type: 'string', description: 'When the list was created' },
|
||||
lastModifiedDateTime: {
|
||||
type: 'string',
|
||||
description: 'When the list was last modified',
|
||||
},
|
||||
list: { type: 'object', description: 'List properties (e.g., template)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SharepointCreatePageResponse,
|
||||
SharepointPage,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
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.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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-or-llm',
|
||||
description: 'The name of the page to create. Example: My-New-Page.aspx or Report-2024.aspx',
|
||||
},
|
||||
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) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const pageName = optionalTrim(params.pageName)
|
||||
if (!pageName) {
|
||||
throw new Error('Page name is required')
|
||||
}
|
||||
|
||||
const pageTitle = optionalTrim(params.pageTitle) || pageName
|
||||
|
||||
const pageData: SharepointPage = {
|
||||
'@odata.type': '#microsoft.graph.sitePage',
|
||||
name: pageName,
|
||||
title: pageTitle,
|
||||
publishingState: {
|
||||
level: 'draft',
|
||||
},
|
||||
pageLayout: 'article',
|
||||
}
|
||||
|
||||
const pageContent = typeof params.pageContent === 'string' ? params.pageContent : undefined
|
||||
if (pageContent) {
|
||||
pageData.canvasLayout = {
|
||||
horizontalSections: [
|
||||
{
|
||||
layout: 'oneColumn',
|
||||
id: '1',
|
||||
emphasis: 'none',
|
||||
columns: [
|
||||
{
|
||||
id: '1',
|
||||
width: 12,
|
||||
webparts: [
|
||||
{
|
||||
id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
|
||||
innerHtml: `<p>${escapeHtml(pageContent)}</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return pageData
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
page: {
|
||||
type: 'object',
|
||||
description: 'Created SharePoint page information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the created page' },
|
||||
name: { type: 'string', description: 'The name of the created page' },
|
||||
title: { type: 'string', description: 'The title of the created page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { SharepointDeleteFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteFileTool: ToolConfig<SharepointToolParams, SharepointDeleteFileResponse> = {
|
||||
id: 'sharepoint_delete_file',
|
||||
name: 'Delete SharePoint File',
|
||||
description: 'Delete a file (or folder) from a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file (drive item) to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const driveId = optionalTrim(params.driveId)
|
||||
const driveItemId = optionalTrim(params.driveItemId)
|
||||
if (!driveId) throw new Error('driveId must be provided')
|
||||
if (!driveItemId) throw new Error('driveItemId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
itemId: params?.driveItemId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the file was deleted' },
|
||||
itemId: { type: 'string', description: 'The ID of the deleted file' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type {
|
||||
SharepointDeleteListItemResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteListItemTool: ToolConfig<
|
||||
SharepointToolParams,
|
||||
SharepointDeleteListItemResponse
|
||||
> = {
|
||||
id: 'sharepoint_delete_list_item',
|
||||
name: 'Delete SharePoint List Item',
|
||||
description: 'Delete an item from a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the list item to delete. Example: 1, 42, or 123',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const listId = optionalTrim(params.listId)
|
||||
const itemId = optionalTrim(params.itemId)
|
||||
if (!listId) throw new Error('listId must be provided')
|
||||
if (!itemId) throw new Error('itemId must be provided')
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
const itemSegment = encodeURIComponent(itemId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
itemId: params?.itemId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the list item was deleted' },
|
||||
itemId: { type: 'string', description: 'The ID of the deleted list item' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { SharepointDeletePageResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deletePageTool: ToolConfig<SharepointToolParams, SharepointDeletePageResponse> = {
|
||||
id: 'sharepoint_delete_page',
|
||||
name: 'Delete SharePoint Page',
|
||||
description: 'Delete a page from a SharePoint site',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to delete. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
pageId: params?.pageId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the page was deleted' },
|
||||
pageId: { type: 'string', description: 'The ID of the deleted page' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { SharepointDownloadFileResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const downloadFileTool: ToolConfig<SharepointToolParams, SharepointDownloadFileResponse> = {
|
||||
id: 'sharepoint_download_file',
|
||||
name: 'Download File from SharePoint',
|
||||
description: 'Download a file from a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file (drive item) to download',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional filename override (e.g., "report.pdf", "data.xlsx")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/sharepoint/download-file',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
driveId: params.driveId,
|
||||
itemId: params.driveItemId,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
SharepointDriveItem,
|
||||
SharepointGetDriveItemResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getDriveItemTool: ToolConfig<SharepointToolParams, SharepointGetDriveItemResponse> = {
|
||||
id: 'sharepoint_get_drive_item',
|
||||
name: 'Get SharePoint Drive Item',
|
||||
description: 'Get metadata for a file or folder in a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the SharePoint API',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document library (drive). Example: b!abc123def456',
|
||||
},
|
||||
driveItemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file or folder (drive item) to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const driveId = optionalTrim(params.driveId)
|
||||
const driveItemId = optionalTrim(params.driveItemId)
|
||||
if (!driveId) throw new Error('driveId must be provided')
|
||||
if (!driveItemId) throw new Error('driveItemId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
|
||||
const driveItem: SharepointDriveItem = {
|
||||
id: data.id as string,
|
||||
name: data.name as string,
|
||||
webUrl: data.webUrl as string | undefined,
|
||||
size: data.size as number | undefined,
|
||||
createdDateTime: data.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime as string | undefined,
|
||||
file: (data.file as SharepointDriveItem['file']) ?? null,
|
||||
folder: (data.folder as SharepointDriveItem['folder']) ?? null,
|
||||
parentReference: (data.parentReference as SharepointDriveItem['parentReference']) ?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { driveItem },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
driveItem: {
|
||||
type: 'object',
|
||||
description: 'Metadata for the SharePoint file or folder',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the drive item' },
|
||||
name: { type: 'string', description: 'The name of the file or folder' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the item' },
|
||||
size: { type: 'number', description: 'The size of the item in bytes', optional: true },
|
||||
createdDateTime: { type: 'string', description: 'When the item was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the item was last modified' },
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Present if the item is a file (contains mimeType)',
|
||||
optional: true,
|
||||
},
|
||||
folder: {
|
||||
type: 'object',
|
||||
description: 'Present if the item is a folder (contains childCount)',
|
||||
optional: true,
|
||||
},
|
||||
parentReference: {
|
||||
type: 'object',
|
||||
description: 'Reference to the parent folder/drive',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
SharepointGetListResponse,
|
||||
SharepointList,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { assertGraphNextPageUrl, getGraphNextPageUrl, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointGetList')
|
||||
|
||||
export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResponse> = {
|
||||
id: 'sharepoint_get_list',
|
||||
name: 'Get SharePoint List',
|
||||
description: 'Get metadata (and optionally columns/items) for a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list to retrieve. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
includeColumns: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Whether to include column definitions when retrieving a specific list',
|
||||
},
|
||||
includeItems: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Whether to include list items when retrieving a specific list',
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full @odata.nextLink URL from a previous Microsoft Graph page response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (params.nextPageUrl) {
|
||||
return assertGraphNextPageUrl(params.nextPageUrl)
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
const listId = optionalTrim(params.listId)
|
||||
|
||||
if (!listId) {
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists`
|
||||
const url = new URL(baseUrl)
|
||||
const finalUrl = url.toString()
|
||||
logger.info('SharePoint List All Lists URL', {
|
||||
finalUrl,
|
||||
siteId,
|
||||
})
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
const wantsItems = typeof params.includeItems === 'boolean' ? params.includeItems : true
|
||||
|
||||
if (wantsItems && !params.includeColumns) {
|
||||
const itemsUrl = new URL(
|
||||
`https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists/${listSegment}/items`
|
||||
)
|
||||
itemsUrl.searchParams.set('$expand', 'fields')
|
||||
const finalItemsUrl = itemsUrl.toString()
|
||||
logger.info('SharePoint Get List Items URL', {
|
||||
finalUrl: finalItemsUrl,
|
||||
siteId,
|
||||
listId,
|
||||
})
|
||||
return finalItemsUrl
|
||||
}
|
||||
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/lists/${listSegment}`
|
||||
const url = new URL(baseUrl)
|
||||
const expandParts: string[] = []
|
||||
if (params.includeColumns) expandParts.push('columns')
|
||||
if (wantsItems) expandParts.push('items(expand=fields)')
|
||||
if (expandParts.length > 0) url.searchParams.append('$expand', expandParts.join(','))
|
||||
|
||||
const finalUrl = url.toString()
|
||||
logger.info('SharePoint Get List URL', {
|
||||
finalUrl,
|
||||
siteId,
|
||||
listId,
|
||||
includeColumns: !!params.includeColumns,
|
||||
includeItems: wantsItems,
|
||||
})
|
||||
return finalUrl
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
const value = data.value
|
||||
|
||||
// If the response is a collection of items (from the items endpoint)
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value[0] &&
|
||||
typeof value[0] === 'object' &&
|
||||
'fields' in value[0]
|
||||
) {
|
||||
const items = value.map((i: Record<string, unknown>) => ({
|
||||
id: i.id as string,
|
||||
fields: i.fields as Record<string, unknown>,
|
||||
}))
|
||||
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
list: { id: optionalTrim(params?.listId) || '', items } as SharepointList,
|
||||
items,
|
||||
nextPageUrl,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const lists: SharepointList[] = value.map((l: Record<string, unknown>) => ({
|
||||
id: l.id as string,
|
||||
displayName: (l.displayName ?? l.name) as string | undefined,
|
||||
name: l.name as string | undefined,
|
||||
webUrl: l.webUrl as string | undefined,
|
||||
createdDateTime: l.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: l.lastModifiedDateTime as string | undefined,
|
||||
list: l.list as SharepointList['list'],
|
||||
}))
|
||||
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { lists, nextPageUrl },
|
||||
}
|
||||
}
|
||||
|
||||
const list: SharepointList = {
|
||||
id: data.id as string,
|
||||
displayName: (data.displayName ?? data.name) as string | undefined,
|
||||
name: data.name as string | undefined,
|
||||
webUrl: data.webUrl as string | undefined,
|
||||
createdDateTime: data.createdDateTime as string | undefined,
|
||||
lastModifiedDateTime: data.lastModifiedDateTime as string | undefined,
|
||||
list: data.list as SharepointList['list'],
|
||||
columns: Array.isArray(data.columns)
|
||||
? data.columns.map((c: Record<string, unknown>) => ({
|
||||
id: c.id as string | undefined,
|
||||
name: c.name as string | undefined,
|
||||
displayName: c.displayName as string | undefined,
|
||||
description: c.description as string | undefined,
|
||||
indexed: c.indexed as boolean | undefined,
|
||||
enforcedUniqueValues: c.enforcedUniqueValues as boolean | undefined,
|
||||
hidden: c.hidden as boolean | undefined,
|
||||
readOnly: c.readOnly as boolean | undefined,
|
||||
required: c.required as boolean | undefined,
|
||||
columnGroup: c.columnGroup as string | undefined,
|
||||
}))
|
||||
: undefined,
|
||||
items: Array.isArray(data.items)
|
||||
? data.items.map((i: Record<string, unknown>) => ({
|
||||
id: i.id as string,
|
||||
fields: i.fields as Record<string, unknown>,
|
||||
}))
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { list },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
list: {
|
||||
type: 'object',
|
||||
description: 'Information about the SharePoint list',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the list' },
|
||||
displayName: { type: 'string', description: 'The display name of the list' },
|
||||
name: { type: 'string', description: 'The internal name of the list' },
|
||||
webUrl: { type: 'string', description: 'The web URL of the list' },
|
||||
createdDateTime: { type: 'string', description: 'When the list was created' },
|
||||
lastModifiedDateTime: {
|
||||
type: 'string',
|
||||
description: 'When the list was last modified',
|
||||
},
|
||||
list: { type: 'object', description: 'List properties (e.g., template)' },
|
||||
columns: {
|
||||
type: 'array',
|
||||
description: 'List column definitions',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List items (with fields when expanded)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Field values for the item' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
type: 'array',
|
||||
description: 'All lists in the site when no listId/title provided',
|
||||
items: { type: 'object' },
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List items with expanded fields when reading list items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Field values for the item' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { SharepointGetListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getListItemTool: ToolConfig<SharepointToolParams, SharepointGetListItemResponse> = {
|
||||
id: 'sharepoint_get_list_item',
|
||||
name: 'Get SharePoint List Item',
|
||||
description: 'Get a single item (with field values) from a SharePoint list',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the list item to retrieve. Example: 1, 42, or 123',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const listId = optionalTrim(params.listId)
|
||||
const itemId = optionalTrim(params.itemId)
|
||||
if (!listId) throw new Error('listId must be provided')
|
||||
if (!itemId) throw new Error('itemId must be provided')
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
const itemSegment = encodeURIComponent(itemId)
|
||||
const url = new URL(
|
||||
`https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}`
|
||||
)
|
||||
url.searchParams.set('$expand', 'fields')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data: Record<string, unknown> = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: {
|
||||
id: data.id as string,
|
||||
fields: data.fields as Record<string, unknown> | undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'object',
|
||||
description: 'SharePoint list item with field values',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Field values for the item' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { addListItemTool } from '@/tools/sharepoint/add_list_items'
|
||||
import { createListTool } from '@/tools/sharepoint/create_list'
|
||||
import { createPageTool } from '@/tools/sharepoint/create_page'
|
||||
import { deleteFileTool } from '@/tools/sharepoint/delete_file'
|
||||
import { deleteListItemTool } from '@/tools/sharepoint/delete_list_item'
|
||||
import { deletePageTool } from '@/tools/sharepoint/delete_page'
|
||||
import { downloadFileTool } from '@/tools/sharepoint/download_file'
|
||||
import { getDriveItemTool } from '@/tools/sharepoint/get_drive_item'
|
||||
import { getListTool } from '@/tools/sharepoint/get_list'
|
||||
import { getListItemTool } from '@/tools/sharepoint/get_list_item'
|
||||
import { listSitesTool } from '@/tools/sharepoint/list_sites'
|
||||
import { publishPageTool } from '@/tools/sharepoint/publish_page'
|
||||
import { readPageTool } from '@/tools/sharepoint/read_page'
|
||||
import { updateListItemTool } from '@/tools/sharepoint/update_list'
|
||||
import { updatePageTool } from '@/tools/sharepoint/update_page'
|
||||
import { uploadFileTool } from '@/tools/sharepoint/upload_file'
|
||||
|
||||
export const sharepointAddListItemTool = addListItemTool
|
||||
export const sharepointCreatePageTool = createPageTool
|
||||
export const sharepointCreateListTool = createListTool
|
||||
export const sharepointDeleteFileTool = deleteFileTool
|
||||
export const sharepointDeleteListItemTool = deleteListItemTool
|
||||
export const sharepointDeletePageTool = deletePageTool
|
||||
export const sharepointDownloadFileTool = downloadFileTool
|
||||
export const sharepointGetDriveItemTool = getDriveItemTool
|
||||
export const sharepointGetListTool = getListTool
|
||||
export const sharepointGetListItemTool = getListItemTool
|
||||
export const sharepointListSitesTool = listSitesTool
|
||||
export const sharepointPublishPageTool = publishPageTool
|
||||
export const sharepointReadPageTool = readPageTool
|
||||
export const sharepointUpdateListItemTool = updateListItemTool
|
||||
export const sharepointUpdatePageTool = updatePageTool
|
||||
export const sharepointUploadFileTool = uploadFileTool
|
||||
|
||||
export * from '@/tools/sharepoint/types'
|
||||
@@ -0,0 +1,177 @@
|
||||
import type {
|
||||
SharepointReadSiteResponse,
|
||||
SharepointSite,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { assertGraphNextPageUrl, getGraphNextPageUrl, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
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.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The group ID for accessing a group team site. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full @odata.nextLink URL from a previous Microsoft Graph page response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (params.nextPageUrl) {
|
||||
return assertGraphNextPageUrl(params.nextPageUrl)
|
||||
}
|
||||
|
||||
let baseUrl: string
|
||||
const groupId = optionalTrim(params.groupId)
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector)
|
||||
|
||||
if (groupId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/groups/${encodeURIComponent(groupId)}/sites/root`
|
||||
} else if (siteId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}`
|
||||
} else {
|
||||
baseUrl = 'https://graph.microsoft.com/v1.0/sites?search=*'
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
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) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.value && Array.isArray(data.value)) {
|
||||
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,
|
||||
})),
|
||||
nextPageUrl: getGraphNextPageUrl(data as Record<string, unknown>),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
site: {
|
||||
type: 'object',
|
||||
description: 'Information about the current SharePoint site',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the site' },
|
||||
name: { type: 'string', description: 'The name of the site' },
|
||||
displayName: { type: 'string', description: 'The display name of the site' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the site' },
|
||||
description: { type: 'string', description: 'The description of the site' },
|
||||
createdDateTime: { type: 'string', description: 'When the site was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the site was last modified' },
|
||||
isPersonalSite: { type: 'boolean', description: 'Whether this is a personal site' },
|
||||
root: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Present (as an empty object) only when this site is the root of its site collection',
|
||||
optional: true,
|
||||
},
|
||||
siteCollection: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
hostname: { type: 'string', description: 'Site collection hostname' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sites: {
|
||||
type: 'array',
|
||||
description: 'List of all accessible SharePoint sites',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the site' },
|
||||
name: { type: 'string', description: 'The name of the site' },
|
||||
displayName: { type: 'string', description: 'The display name of the site' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the site' },
|
||||
description: { type: 'string', description: 'The description of the site' },
|
||||
createdDateTime: { type: 'string', description: 'When the site was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the site was last modified' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { SharepointPublishPageResponse, SharepointToolParams } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const publishPageTool: ToolConfig<SharepointToolParams, SharepointPublishPageResponse> = {
|
||||
id: 'sharepoint_publish_page',
|
||||
name: 'Publish SharePoint Page',
|
||||
description: 'Publish the latest version of a SharePoint page, making it available to all users',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to publish. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
published: true,
|
||||
pageId: params?.pageId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
published: { type: 'boolean', description: 'Whether the page was published' },
|
||||
pageId: { type: 'string', description: 'The ID of the published page' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import type {
|
||||
GraphApiResponse,
|
||||
SharepointPageContent,
|
||||
SharepointReadPageResponse,
|
||||
SharepointToolParams,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import {
|
||||
assertGraphNextPageUrl,
|
||||
cleanODataMetadata,
|
||||
escapeODataString,
|
||||
extractTextFromCanvasLayout,
|
||||
getGraphNextPageUrl,
|
||||
optionalTrim,
|
||||
} 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.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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-or-llm',
|
||||
description:
|
||||
'The ID of the page to read. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
pageName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The name of the page to read (alternative to pageId). Example: Home.aspx or About-Us.aspx',
|
||||
},
|
||||
maxPages: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Maximum number of pages to return when listing all pages (default: 10, max: 50)',
|
||||
},
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full @odata.nextLink URL from a previous Microsoft Graph page response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (params.nextPageUrl) {
|
||||
return assertGraphNextPageUrl(params.nextPageUrl)
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
|
||||
let baseUrl: string
|
||||
if (pageId) {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
|
||||
} else {
|
||||
baseUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/microsoft.graph.sitePage`
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
url.searchParams.append(
|
||||
'$select',
|
||||
'id,name,title,webUrl,pageLayout,description,createdDateTime,lastModifiedDateTime'
|
||||
)
|
||||
|
||||
if (params.pageName && !pageId) {
|
||||
const pageName = params.pageName.trim()
|
||||
const pageNameWithAspx = pageName.endsWith('.aspx') ? pageName : `${pageName}.aspx`
|
||||
const escapedPageName = escapeODataString(pageName)
|
||||
const escapedPageNameWithAspx = escapeODataString(pageNameWithAspx)
|
||||
|
||||
url.searchParams.append(
|
||||
'$filter',
|
||||
`name eq '${escapedPageName}' or name eq '${escapedPageNameWithAspx}'`
|
||||
)
|
||||
url.searchParams.append('$top', '10')
|
||||
} else if (!pageId && !params.pageName) {
|
||||
const requestedMaxPages =
|
||||
typeof params.maxPages === 'number' ? params.maxPages : Number(params.maxPages || 10)
|
||||
const maxPages = Math.min(Number.isFinite(requestedMaxPages) ? requestedMaxPages : 10, 50)
|
||||
url.searchParams.append('$top', maxPages.toString())
|
||||
}
|
||||
|
||||
if (pageId) {
|
||||
url.searchParams.append('$expand', 'canvasLayout')
|
||||
}
|
||||
|
||||
const finalUrl = url.toString()
|
||||
|
||||
logger.info('SharePoint API URL', {
|
||||
finalUrl,
|
||||
siteId,
|
||||
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()
|
||||
|
||||
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) {
|
||||
const pageData = data
|
||||
const contentData = {
|
||||
content: extractTextFromCanvasLayout(data.canvasLayout),
|
||||
canvasLayout: data.canvasLayout ?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
page: {
|
||||
id: pageData.id!,
|
||||
name: pageData.name!,
|
||||
title: pageData.title || pageData.name!,
|
||||
webUrl: pageData.webUrl!,
|
||||
pageLayout: pageData.pageLayout,
|
||||
description: pageData.description ?? null,
|
||||
createdDateTime: pageData.createdDateTime,
|
||||
lastModifiedDateTime: pageData.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.value || data.value.length === 0) {
|
||||
logger.info('No pages found', {
|
||||
searchName: params?.pageName,
|
||||
siteId: optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root',
|
||||
totalResults: data.value?.length || 0,
|
||||
})
|
||||
const message = 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.'
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: {
|
||||
content: message,
|
||||
canvasLayout: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Found pages', {
|
||||
searchName: params?.pageName,
|
||||
foundPages: data.value.map((p) => ({ id: p.id, name: p.name, title: p.title })),
|
||||
totalCount: data.value.length,
|
||||
})
|
||||
|
||||
if (params?.pageName) {
|
||||
const pageData = data.value[0]
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(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,
|
||||
description: pageData.description ?? null,
|
||||
createdDateTime: pageData.createdDateTime,
|
||||
lastModifiedDateTime: pageData.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const siteId = optionalTrim(params?.siteId) || optionalTrim(params?.siteSelector) || 'root'
|
||||
const pagesWithContent = []
|
||||
const nextPageUrl = getGraphNextPageUrl(data)
|
||||
|
||||
logger.info('Fetching content for all pages', {
|
||||
totalPages: data.value.length,
|
||||
siteId,
|
||||
})
|
||||
|
||||
const encodedSiteId = encodeURIComponent(siteId)
|
||||
for (const pageInfo of data.value) {
|
||||
const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodedSiteId}/pages/${encodeURIComponent(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,
|
||||
description: pageInfo.description ?? null,
|
||||
createdDateTime: pageInfo.createdDateTime,
|
||||
lastModifiedDateTime: pageInfo.lastModifiedDateTime,
|
||||
},
|
||||
content: contentData,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching content for page', {
|
||||
pageId: pageInfo.id,
|
||||
pageName: pageInfo.name,
|
||||
error: toError(error).message,
|
||||
})
|
||||
|
||||
pagesWithContent.push({
|
||||
page: {
|
||||
id: pageInfo.id,
|
||||
name: pageInfo.name,
|
||||
title: pageInfo.title || pageInfo.name,
|
||||
webUrl: pageInfo.webUrl,
|
||||
pageLayout: pageInfo.pageLayout,
|
||||
description: pageInfo.description ?? null,
|
||||
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,
|
||||
nextPageUrl,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
page: {
|
||||
type: 'object',
|
||||
description: 'Information about the SharePoint page',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
description: { type: 'string', description: 'The description of the page', optional: true },
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
type: 'array',
|
||||
description: 'List of SharePoint pages',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'The description of the page',
|
||||
optional: true,
|
||||
},
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: {
|
||||
type: 'string',
|
||||
description: 'When the page was last modified',
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Extracted text content from the page' },
|
||||
canvasLayout: {
|
||||
type: 'object',
|
||||
description: 'Raw SharePoint canvas layout structure',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'object',
|
||||
description: 'Content of the SharePoint page',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Extracted text content from the page' },
|
||||
canvasLayout: { type: 'object', description: 'Raw SharePoint canvas layout structure' },
|
||||
},
|
||||
},
|
||||
totalPages: { type: 'number', description: 'Total number of pages found' },
|
||||
nextPageUrl: {
|
||||
type: 'string',
|
||||
description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface SharepointSite {
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
webUrl: string
|
||||
description?: string | null
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
}
|
||||
|
||||
export interface SharepointPage {
|
||||
'@odata.type'?: string
|
||||
id?: string
|
||||
name: string
|
||||
title: string
|
||||
description?: string | null
|
||||
webUrl?: string
|
||||
pageLayout?: string
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
publishingState?: {
|
||||
level: string
|
||||
}
|
||||
canvasLayout?: CanvasLayout
|
||||
}
|
||||
|
||||
export interface SharepointPageContent {
|
||||
content: string
|
||||
canvasLayout?: CanvasLayout | null
|
||||
}
|
||||
|
||||
interface SharepointColumn {
|
||||
id?: string
|
||||
name?: string
|
||||
displayName?: string
|
||||
description?: string
|
||||
indexed?: boolean
|
||||
enforcedUniqueValues?: boolean
|
||||
hidden?: boolean
|
||||
readOnly?: boolean
|
||||
required?: boolean
|
||||
columnGroup?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface SharepointListItem {
|
||||
id: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface SharepointList {
|
||||
id: string
|
||||
displayName?: string
|
||||
name?: string
|
||||
webUrl?: string
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
list?: {
|
||||
template?: string
|
||||
}
|
||||
columns?: SharepointColumn[]
|
||||
items?: SharepointListItem[]
|
||||
}
|
||||
|
||||
interface SharepointListSitesResponse extends ToolResponse {
|
||||
output: {
|
||||
sites: SharepointSite[]
|
||||
nextPageUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointCreatePageResponse extends ToolResponse {
|
||||
output: {
|
||||
page: SharepointPage
|
||||
}
|
||||
}
|
||||
|
||||
interface SharepointPageWithContent {
|
||||
page: SharepointPage
|
||||
content: SharepointPageContent
|
||||
}
|
||||
|
||||
export interface SharepointReadPageResponse extends ToolResponse {
|
||||
output: {
|
||||
page?: SharepointPage
|
||||
pages?: SharepointPageWithContent[]
|
||||
content?: SharepointPageContent
|
||||
totalPages?: number
|
||||
nextPageUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointReadSiteResponse extends ToolResponse {
|
||||
output: {
|
||||
site?: {
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
webUrl: string
|
||||
description?: string | null
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
isPersonalSite?: boolean
|
||||
// Graph returns an empty object marker (not a URL) when this site is the root of its site collection
|
||||
root?: Record<string, never>
|
||||
siteCollection?: {
|
||||
hostname: string
|
||||
}
|
||||
}
|
||||
sites?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
webUrl: string
|
||||
description?: string | null
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
}>
|
||||
nextPageUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointToolParams {
|
||||
accessToken: string
|
||||
siteId?: string
|
||||
siteSelector?: string
|
||||
pageId?: string
|
||||
pageName?: string
|
||||
pageContent?: string | unknown[] | { columns?: unknown[] }
|
||||
pageTitle?: string
|
||||
publishingState?: string
|
||||
query?: string
|
||||
pageSize?: number
|
||||
nextPageUrl?: string
|
||||
hostname?: string
|
||||
serverRelativePath?: string
|
||||
groupId?: string
|
||||
maxPages?: number
|
||||
// Lists
|
||||
listId?: string
|
||||
listTitle?: string
|
||||
includeColumns?: boolean
|
||||
includeItems?: boolean
|
||||
// Create List
|
||||
listDisplayName?: string
|
||||
listDescription?: string
|
||||
listTemplate?: string
|
||||
// Update List Item / Delete List Item / Get List Item
|
||||
itemId?: string
|
||||
listItemFields?: Record<string, unknown>
|
||||
// Upload File / Download File / Delete File / Get Drive Item
|
||||
driveId?: string
|
||||
folderPath?: string
|
||||
fileName?: string
|
||||
files?: UserFile[]
|
||||
driveItemId?: string
|
||||
}
|
||||
|
||||
export interface GraphApiResponse {
|
||||
id?: string
|
||||
name?: string
|
||||
title?: string
|
||||
description?: string | null
|
||||
webUrl?: string
|
||||
pageLayout?: string
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
canvasLayout?: CanvasLayout
|
||||
value?: GraphApiPageItem[]
|
||||
error?: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GraphApiPageItem {
|
||||
id: string
|
||||
name: string
|
||||
title?: string
|
||||
description?: string | null
|
||||
webUrl?: string
|
||||
pageLayout?: string
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
}
|
||||
|
||||
export interface 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 type SharepointResponse =
|
||||
| SharepointListSitesResponse
|
||||
| SharepointCreatePageResponse
|
||||
| SharepointReadPageResponse
|
||||
| SharepointReadSiteResponse
|
||||
| SharepointGetListResponse
|
||||
| SharepointCreateListResponse
|
||||
| SharepointUpdateListItemResponse
|
||||
| SharepointAddListItemResponse
|
||||
| SharepointUploadFileResponse
|
||||
| SharepointDeleteListItemResponse
|
||||
| SharepointGetListItemResponse
|
||||
| SharepointDeletePageResponse
|
||||
| SharepointUpdatePageResponse
|
||||
| SharepointPublishPageResponse
|
||||
| SharepointDownloadFileResponse
|
||||
| SharepointDeleteFileResponse
|
||||
| SharepointGetDriveItemResponse
|
||||
|
||||
export interface SharepointGetListResponse extends ToolResponse {
|
||||
output: {
|
||||
list?: SharepointList
|
||||
lists?: SharepointList[]
|
||||
items?: SharepointListItem[]
|
||||
nextPageUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointCreateListResponse extends ToolResponse {
|
||||
output: {
|
||||
list: SharepointList
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointUpdateListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
item: {
|
||||
id: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointAddListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
item: {
|
||||
id: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SharepointUploadedFile {
|
||||
id: string
|
||||
name: string
|
||||
webUrl: string
|
||||
size: number
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
}
|
||||
|
||||
export interface SharepointSkippedFile {
|
||||
name: string
|
||||
size: number
|
||||
limit: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface SharepointUploadError {
|
||||
name: string
|
||||
error: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
export interface SharepointUploadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
uploadedFiles: SharepointUploadedFile[]
|
||||
fileCount: number
|
||||
skippedFiles?: SharepointSkippedFile[]
|
||||
skippedCount?: number
|
||||
errors?: SharepointUploadError[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeleteListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointGetListItemResponse extends ToolResponse {
|
||||
output: {
|
||||
item: {
|
||||
id: string
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeletePageResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
pageId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointUpdatePageResponse extends ToolResponse {
|
||||
output: {
|
||||
page: SharepointPage
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointPublishPageResponse extends ToolResponse {
|
||||
output: {
|
||||
published: boolean
|
||||
pageId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDriveItem {
|
||||
id: string
|
||||
name: string
|
||||
webUrl?: string
|
||||
size?: number
|
||||
createdDateTime?: string
|
||||
lastModifiedDateTime?: string
|
||||
file?: {
|
||||
mimeType?: string
|
||||
} | null
|
||||
folder?: {
|
||||
childCount?: number
|
||||
} | null
|
||||
parentReference?: {
|
||||
id?: string
|
||||
driveId?: string
|
||||
path?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface SharepointDownloadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: Buffer | string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointDeleteFileResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
itemId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharepointGetDriveItemResponse extends ToolResponse {
|
||||
output: {
|
||||
driveItem: SharepointDriveItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
SharepointToolParams,
|
||||
SharepointUpdateListItemResponse,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateListItemTool: ToolConfig<
|
||||
SharepointToolParams,
|
||||
SharepointUpdateListItemResponse
|
||||
> = {
|
||||
id: 'sharepoint_update_list',
|
||||
name: 'Update SharePoint List Item',
|
||||
description: 'Update the properties (fields) on a SharePoint list item',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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)',
|
||||
},
|
||||
listId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
itemId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the list item to update. Example: 1, 42, or 123',
|
||||
},
|
||||
listItemFields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Field values to update on the list item',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const itemId = optionalTrim(params.itemId)
|
||||
const listId = optionalTrim(params.listId)
|
||||
if (!itemId) throw new Error('itemId is required')
|
||||
if (!listId) {
|
||||
throw new Error('listId must be provided')
|
||||
}
|
||||
const listSegment = encodeURIComponent(listId)
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.listItemFields || Object.keys(params.listItemFields).length === 0) {
|
||||
throw new Error('listItemFields must not be empty')
|
||||
}
|
||||
|
||||
return sanitizeListItemFields(params.listItemFields, { action: 'update' })
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
let fields: Record<string, unknown> | undefined
|
||||
if (response.status !== 204) {
|
||||
try {
|
||||
fields = await response.json()
|
||||
} catch {
|
||||
// Fall back to submitted fields if no body is returned
|
||||
fields = params?.listItemFields
|
||||
}
|
||||
} else {
|
||||
fields = params?.listItemFields
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
item: {
|
||||
id: params?.itemId!,
|
||||
fields,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'object',
|
||||
description: 'Updated SharePoint list item',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Item ID' },
|
||||
fields: { type: 'object', description: 'Updated field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
CanvasLayout,
|
||||
SharepointToolParams,
|
||||
SharepointUpdatePageResponse,
|
||||
} from '@/tools/sharepoint/types'
|
||||
import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SharePointUpdatePage')
|
||||
|
||||
export const updatePageTool: ToolConfig<SharepointToolParams, SharepointUpdatePageResponse> = {
|
||||
id: 'sharepoint_update_page',
|
||||
name: 'Update SharePoint Page',
|
||||
description: 'Update the title and/or content of a SharePoint page',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the page to update. Example: a GUID like 12345678-1234-1234-1234-123456789012',
|
||||
},
|
||||
pageTitle: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'The new title of the page',
|
||||
},
|
||||
pageContent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'The new text content of the page. Replaces the entire canvas layout of the page.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root'
|
||||
const pageId = optionalTrim(params.pageId)
|
||||
if (!pageId) throw new Error('pageId must be provided')
|
||||
return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const pageTitle = optionalTrim(params.pageTitle)
|
||||
const pageContent = typeof params.pageContent === 'string' ? params.pageContent : undefined
|
||||
|
||||
if (!pageTitle && !pageContent) {
|
||||
throw new Error('At least one of pageTitle or pageContent must be provided')
|
||||
}
|
||||
|
||||
const pageData: {
|
||||
'@odata.type': string
|
||||
title?: string
|
||||
canvasLayout?: CanvasLayout
|
||||
} = {
|
||||
'@odata.type': '#microsoft.graph.sitePage',
|
||||
}
|
||||
if (pageTitle) pageData.title = pageTitle
|
||||
|
||||
if (pageContent) {
|
||||
pageData.canvasLayout = {
|
||||
horizontalSections: [
|
||||
{
|
||||
layout: 'oneColumn',
|
||||
id: '1',
|
||||
emphasis: 'none',
|
||||
columns: [
|
||||
{
|
||||
id: '1',
|
||||
width: 12,
|
||||
webparts: [
|
||||
{
|
||||
id: '6f9230af-2a98-4952-b205-9ede4f9ef548',
|
||||
innerHtml: `<p>${escapeHtml(pageContent)}</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Updating SharePoint page', {
|
||||
pageId: params.pageId,
|
||||
hasTitle: !!pageTitle,
|
||||
hasContent: !!pageContent,
|
||||
})
|
||||
|
||||
return pageData
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
logger.info('SharePoint page updated 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
page: {
|
||||
type: 'object',
|
||||
description: 'Updated SharePoint page information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the page' },
|
||||
name: { type: 'string', description: 'The name of the page' },
|
||||
title: { type: 'string', description: 'The title of the page' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the page' },
|
||||
pageLayout: { type: 'string', description: 'The layout type of the page' },
|
||||
createdDateTime: { type: 'string', description: 'When the page was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { SharepointToolParams, SharepointUploadFileResponse } from '@/tools/sharepoint/types'
|
||||
import { optionalTrim } from '@/tools/sharepoint/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const uploadFileTool: ToolConfig<SharepointToolParams, SharepointUploadFileResponse> = {
|
||||
id: 'sharepoint_upload_file',
|
||||
name: 'Upload File to SharePoint',
|
||||
description: 'Upload files to a SharePoint document library',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'sharepoint',
|
||||
},
|
||||
|
||||
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',
|
||||
},
|
||||
driveId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the document library (drive). If not provided, uses default drive. Example: b!abc123def456',
|
||||
},
|
||||
folderPath: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Optional folder path within the document library. Example: /Documents/Subfolder or /Shared Documents/Reports',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional: override the uploaded file name. Example: report-2024.pdf',
|
||||
},
|
||||
files: {
|
||||
type: 'file[]',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Files to upload to SharePoint',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/sharepoint/upload',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: SharepointToolParams) => {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
siteId: optionalTrim(params.siteId) || 'root',
|
||||
driveId: optionalTrim(params.driveId) || null,
|
||||
folderPath: optionalTrim(params.folderPath) || null,
|
||||
fileName: optionalTrim(params.fileName) || null,
|
||||
files: params.files || null,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const output = data.output ?? {}
|
||||
return {
|
||||
success: Boolean(data.success),
|
||||
output: {
|
||||
uploadedFiles: output.uploadedFiles ?? [],
|
||||
fileCount: output.fileCount ?? 0,
|
||||
skippedFiles: output.skippedFiles ?? [],
|
||||
skippedCount: output.skippedCount ?? 0,
|
||||
errors: output.errors ?? [],
|
||||
},
|
||||
error: data.success ? undefined : data.error || 'Failed to upload files to SharePoint',
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uploadedFiles: {
|
||||
type: 'array',
|
||||
description: 'Array of uploaded file objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The unique ID of the uploaded file' },
|
||||
name: { type: 'string', description: 'The name of the uploaded file' },
|
||||
webUrl: { type: 'string', description: 'The URL to access the file' },
|
||||
size: { type: 'number', description: 'The size of the file in bytes' },
|
||||
createdDateTime: { type: 'string', description: 'When the file was created' },
|
||||
lastModifiedDateTime: { type: 'string', description: 'When the file was last modified' },
|
||||
},
|
||||
},
|
||||
},
|
||||
fileCount: {
|
||||
type: 'number',
|
||||
description: 'Number of files uploaded',
|
||||
},
|
||||
skippedFiles: {
|
||||
type: 'array',
|
||||
description: 'Files that were skipped before upload',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
limit: { type: 'number', description: 'Upload size limit in bytes' },
|
||||
reason: { type: 'string', description: 'Reason the file was skipped' },
|
||||
},
|
||||
},
|
||||
},
|
||||
skippedCount: {
|
||||
type: 'number',
|
||||
description: 'Number of files skipped',
|
||||
},
|
||||
errors: {
|
||||
type: 'array',
|
||||
description: 'Per-file upload errors',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
status: {
|
||||
type: 'number',
|
||||
description: 'HTTP status from Microsoft Graph',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { CanvasLayout } from '@/tools/sharepoint/types'
|
||||
|
||||
const logger = createLogger('SharepointUtils')
|
||||
|
||||
export function optionalTrim(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null) return undefined
|
||||
const trimmed = String(value).trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
|
||||
export function escapeODataString(value: string): string {
|
||||
return value.replace(/'/g, "''")
|
||||
}
|
||||
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export function getGraphNextPageUrl(data: object): string | undefined {
|
||||
const nextLink = (data as Record<string, unknown>)['@odata.nextLink']
|
||||
return typeof nextLink === 'string' ? nextLink : undefined
|
||||
}
|
||||
|
||||
export function assertGraphNextPageUrl(nextPageUrl: string): string {
|
||||
const trimmed = nextPageUrl.trim()
|
||||
const url = new URL(trimmed)
|
||||
if (url.origin !== 'https://graph.microsoft.com') {
|
||||
throw new Error('nextPageUrl must be a Microsoft Graph @odata.nextLink URL')
|
||||
}
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
function stripHtmlTags(html: string): string {
|
||||
let text = html
|
||||
let previous: string
|
||||
|
||||
do {
|
||||
previous = text
|
||||
text = text.replace(/<[^>]*>/g, '')
|
||||
text = text.replace(/[<>]/g, '')
|
||||
} while (text !== previous)
|
||||
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
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) {
|
||||
const text = stripHtmlTags(webpart.innerHtml)
|
||||
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 = stripHtmlTags(webpart.innerHtml)
|
||||
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
|
||||
}
|
||||
|
||||
/** SharePoint list item fields that are system-managed and cannot be set via the Graph API. */
|
||||
export const READ_ONLY_LIST_ITEM_FIELDS = new Set<string>([
|
||||
'Id',
|
||||
'id',
|
||||
'UniqueId',
|
||||
'GUID',
|
||||
'ContentTypeId',
|
||||
'Created',
|
||||
'Modified',
|
||||
'Author',
|
||||
'Editor',
|
||||
'CreatedBy',
|
||||
'ModifiedBy',
|
||||
'AuthorId',
|
||||
'EditorId',
|
||||
'_UIVersionString',
|
||||
'Attachments',
|
||||
'FileRef',
|
||||
'FileDirRef',
|
||||
'FileLeafRef',
|
||||
])
|
||||
|
||||
/**
|
||||
* Removes read-only/system-managed fields from a SharePoint list item field set, logging any
|
||||
* fields that were stripped. Throws if no updatable fields remain.
|
||||
*/
|
||||
export function sanitizeListItemFields(
|
||||
fields: Record<string, unknown>,
|
||||
context: { action: 'update' | 'create' }
|
||||
): Record<string, unknown> {
|
||||
const entries = Object.entries(fields)
|
||||
const updatableEntries = entries.filter(([key]) => !READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
||||
|
||||
if (updatableEntries.length !== entries.length) {
|
||||
const removed = entries
|
||||
.filter(([key]) => READ_ONLY_LIST_ITEM_FIELDS.has(key))
|
||||
.map(([key]) => key)
|
||||
logger.warn(`Removed read-only SharePoint fields from ${context.action}`, { removed })
|
||||
}
|
||||
|
||||
if (updatableEntries.length === 0) {
|
||||
const requestedKeys = Object.keys(fields)
|
||||
const verb = context.action === 'update' ? 'updated' : 'set'
|
||||
throw new Error(
|
||||
`All provided fields are read-only and cannot be ${verb}: ${requestedKeys.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return Object.fromEntries(updatableEntries)
|
||||
}
|
||||
|
||||
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>)) {
|
||||
if (key.includes('@odata')) continue
|
||||
|
||||
cleaned[key] = cleanODataMetadata(value)
|
||||
}
|
||||
|
||||
return cleaned as T
|
||||
}
|
||||
Reference in New Issue
Block a user