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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,134 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelClearRangeParams,
MicrosoftExcelClearRangeResponse,
} from '@/tools/microsoft_excel/types'
import {
buildWorksheetRangeUrl,
getItemBasePath,
getSpreadsheetWebUrl,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Clears the contents and/or formatting of a worksheet range.
* Uses Microsoft Graph: POST /workbook/worksheets/{name}/range(address='...')/clear
*/
export const clearRangeTool: ToolConfig<
MicrosoftExcelClearRangeParams,
MicrosoftExcelClearRangeResponse
> = {
id: 'microsoft_excel_clear_range',
name: 'Clear Microsoft Excel Range',
description: 'Clear the values and/or formatting of a range in a Microsoft Excel worksheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
sheetName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The name of the worksheet (e.g., "Sheet1"). If omitted, the range must use the combined "Sheet1!A1:B2" format.',
},
range: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The cell range to clear (e.g., "A1:D10" or "Sheet1!A1:D10")',
},
applyTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'What to clear: "All", "Formats", or "Contents". Defaults to "All".',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
return `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/clear`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => ({
applyTo: params.applyTo || 'All',
}),
},
transformResponse: async (_response: Response, params?: MicrosoftExcelClearRangeParams) => {
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
cleared: true,
range: params?.range ?? '',
applyTo: params?.applyTo || 'All',
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
cleared: { type: 'boolean', description: 'Whether the range was cleared' },
range: { type: 'string', description: 'The range that was cleared' },
applyTo: { type: 'string', description: 'What was cleared (All, Formats, or Contents)' },
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
@@ -0,0 +1,145 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelCreateTableParams,
MicrosoftExcelCreateTableResponse,
} from '@/tools/microsoft_excel/types'
import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Creates a new table over a range of cells.
* Uses Microsoft Graph: POST /workbook/tables/add with { address, hasHeaders }.
*/
export const createTableTool: ToolConfig<
MicrosoftExcelCreateTableParams,
MicrosoftExcelCreateTableResponse
> = {
id: 'microsoft_excel_create_table',
name: 'Create Microsoft Excel Table',
description: 'Create a new table over a range of cells in a Microsoft Excel workbook',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
address: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The range address for the table data source (e.g., "Sheet1!A1:D5"). If no sheet name is included, the active sheet is used.',
},
hasHeaders: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the first row of the range contains column headers. Defaults to true.',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
return `${basePath}/workbook/tables/add`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const address = params.address?.trim()
if (!address) {
throw new Error('A range address is required (e.g., "Sheet1!A1:D5")')
}
return {
address,
hasHeaders: params.hasHeaders ?? true,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftExcelCreateTableParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
table: {
id: data.id ?? '',
name: data.name ?? '',
showHeaders: data.showHeaders ?? true,
showTotals: data.showTotals ?? false,
style: data.style ?? null,
},
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
table: {
type: 'object',
description: 'Details of the newly created table',
properties: {
id: { type: 'string', description: 'The unique ID of the table' },
name: { type: 'string', description: 'The name of the table' },
showHeaders: { type: 'boolean', description: 'Whether the header row is shown' },
showTotals: { type: 'boolean', description: 'Whether the totals row is shown' },
style: { type: 'string', description: 'The table style name' },
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
@@ -0,0 +1,119 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelDeleteWorksheetParams,
MicrosoftExcelDeleteWorksheetResponse,
} from '@/tools/microsoft_excel/types'
import {
escapeODataString,
getItemBasePath,
getSpreadsheetWebUrl,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Deletes a worksheet from a workbook.
* Uses Microsoft Graph: DELETE /workbook/worksheets/{name}
*/
export const deleteWorksheetTool: ToolConfig<
MicrosoftExcelDeleteWorksheetParams,
MicrosoftExcelDeleteWorksheetResponse
> = {
id: 'microsoft_excel_delete_worksheet',
name: 'Delete Microsoft Excel Worksheet',
description: 'Delete a worksheet (sheet) from a Microsoft Excel workbook',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
worksheetName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the worksheet to delete (e.g., "Sheet1", "Old Data")',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const worksheetName = params.worksheetName?.trim()
if (!worksheetName) {
throw new Error('Worksheet name is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
return `${basePath}/workbook/worksheets('${encodeURIComponent(escapeODataString(worksheetName))}')`
},
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (_response: Response, params?: MicrosoftExcelDeleteWorksheetParams) => {
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
deleted: true,
worksheetName: params?.worksheetName?.trim() ?? '',
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the worksheet was deleted' },
worksheetName: { type: 'string', description: 'The name of the deleted worksheet' },
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
@@ -0,0 +1,254 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelFormatRangeParams,
MicrosoftExcelFormatRangeResponse,
} from '@/tools/microsoft_excel/types'
import {
buildWorksheetRangeUrl,
getItemBasePath,
getSpreadsheetWebUrl,
parseGraphErrorMessage,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Builds the font PATCH body from the provided font params, omitting any unset fields.
* Returns null when no font property was supplied.
*/
function buildFontBody(params: MicrosoftExcelFormatRangeParams): Record<string, unknown> | null {
const body: Record<string, unknown> = {}
if (params.fontBold !== undefined) body.bold = params.fontBold
if (params.fontItalic !== undefined) body.italic = params.fontItalic
if (params.fontColor) body.color = params.fontColor
if (params.fontSize !== undefined) body.size = params.fontSize
if (params.fontName) body.name = params.fontName
return Object.keys(body).length > 0 ? body : null
}
/**
* Formats a worksheet range by applying fill color and/or font properties.
* Uses Microsoft Graph PATCH on range(...)/format/fill and range(...)/format/font.
* The font update is the primary request; a fill update (when also requested) runs
* as a follow-up call so a single tool invocation can set both.
*/
export const formatRangeTool: ToolConfig<
MicrosoftExcelFormatRangeParams,
MicrosoftExcelFormatRangeResponse
> = {
id: 'microsoft_excel_format_range',
name: 'Format Microsoft Excel Range',
description: 'Apply fill color and/or font formatting to a range in a Microsoft Excel worksheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
sheetName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The name of the worksheet (e.g., "Sheet1"). If omitted, the range must use the combined "Sheet1!A1:B2" format.',
},
range: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The cell range to format (e.g., "A1:D10" or "Sheet1!A1:D10")',
},
fillColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Background fill color as an HTML hex code (e.g., "#FFFF00").',
},
fontBold: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the font is bold.',
},
fontItalic: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the font is italic.',
},
fontColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Font color as an HTML hex code (e.g., "#FF0000").',
},
fontSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Font size in points (e.g., 12).',
},
fontName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Font name (e.g., "Calibri").',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const fontBody = buildFontBody(params)
const hasFill = Boolean(params.fillColor)
if (!fontBody && !hasFill) {
throw new Error('Provide at least a fill color or a font property to format the range')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
const rangeUrl = buildWorksheetRangeUrl(basePath, params.range, params.sheetName)
return fontBody ? `${rangeUrl}/format/font` : `${rangeUrl}/format/fill`
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const fontBody = buildFontBody(params)
if (fontBody) return fontBody
return { color: params.fillColor }
},
},
transformResponse: async (response: Response, params?: MicrosoftExcelFormatRangeParams) => {
if (!params) {
throw new Error('Format parameters are required')
}
const accessToken = params.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const spreadsheetId = params.spreadsheetId?.trim() || ''
const driveId = params.driveId
const fontBody = buildFontBody(params)
const hasFill = Boolean(params.fillColor)
let fontResult: Record<string, unknown> | null = null
let fillApplied = false
if (fontBody) {
fontResult = await response.json().catch(() => null)
if (hasFill) {
const basePath = getItemBasePath(spreadsheetId, driveId)
const fillUrl = `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/format/fill`
const fillResp = await fetch(fillUrl, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ color: params.fillColor }),
})
if (!fillResp.ok) {
const errorText = await fillResp.text().catch(() => '')
const detail = parseGraphErrorMessage(fillResp.status, fillResp.statusText, errorText)
throw new Error(
`Font formatting was applied, but the fill color update failed: ${detail}. Re-run with only the fill color to finish.`
)
}
fillApplied = true
}
} else if (hasFill) {
fillApplied = true
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
formatted: true,
range: params.range ?? '',
fill: fillApplied ? { color: params.fillColor ?? null } : null,
font: fontBody
? {
bold: (fontResult?.bold as boolean | undefined) ?? params.fontBold ?? null,
italic: (fontResult?.italic as boolean | undefined) ?? params.fontItalic ?? null,
color: (fontResult?.color as string | undefined) ?? params.fontColor ?? null,
name: (fontResult?.name as string | undefined) ?? params.fontName ?? null,
size: (fontResult?.size as number | undefined) ?? params.fontSize ?? null,
}
: null,
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
formatted: { type: 'boolean', description: 'Whether the formatting was applied' },
range: { type: 'string', description: 'The range that was formatted' },
fill: {
type: 'object',
description: 'The applied fill, or null if no fill was set',
properties: {
color: { type: 'string', description: 'The applied fill color' },
},
},
font: {
type: 'object',
description: 'The applied font properties, or null if no font was set',
properties: {
bold: { type: 'boolean', description: 'Whether the font is bold' },
italic: { type: 'boolean', description: 'Whether the font is italic' },
color: { type: 'string', description: 'The font color' },
name: { type: 'string', description: 'The font name' },
size: { type: 'number', description: 'The font size in points' },
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
+28
View File
@@ -0,0 +1,28 @@
import { clearRangeTool } from '@/tools/microsoft_excel/clear_range'
import { createTableTool } from '@/tools/microsoft_excel/create_table'
import { deleteWorksheetTool } from '@/tools/microsoft_excel/delete_worksheet'
import { formatRangeTool } from '@/tools/microsoft_excel/format_range'
import { readTool, readV2Tool } from '@/tools/microsoft_excel/read'
import { sortRangeTool } from '@/tools/microsoft_excel/sort_range'
import { tableAddTool } from '@/tools/microsoft_excel/table_add'
import { worksheetAddTool } from '@/tools/microsoft_excel/worksheet_add'
import { writeTool, writeV2Tool } from '@/tools/microsoft_excel/write'
// V1 exports
export const microsoftExcelReadTool = readTool
export const microsoftExcelTableAddTool = tableAddTool
export const microsoftExcelWorksheetAddTool = worksheetAddTool
export const microsoftExcelWriteTool = writeTool
// V2 exports
export const microsoftExcelReadV2Tool = readV2Tool
export const microsoftExcelWriteV2Tool = writeV2Tool
// Workbook operations
export const microsoftExcelClearRangeTool = clearRangeTool
export const microsoftExcelCreateTableTool = createTableTool
export const microsoftExcelDeleteWorksheetTool = deleteWorksheetTool
export const microsoftExcelFormatRangeTool = formatRangeTool
export const microsoftExcelSortRangeTool = sortRangeTool
export * from './types'
+351
View File
@@ -0,0 +1,351 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
ExcelCellValue,
MicrosoftExcelReadResponse,
MicrosoftExcelToolParams,
MicrosoftExcelV2ReadResponse,
MicrosoftExcelV2ToolParams,
} from '@/tools/microsoft_excel/types'
import {
escapeODataString,
getItemBasePath,
getSpreadsheetWebUrl,
parseGraphErrorMessage,
trimTrailingEmptyRowsAndColumns,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
const EXCEL_RETRY_CONFIG = {
enabled: true,
maxRetries: 3,
initialDelayMs: 500,
maxDelayMs: 30000,
retryIdempotentOnly: true,
} as const
export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadResponse> = {
id: 'microsoft_excel_read',
name: 'Read from Microsoft Excel',
description: 'Read data from a Microsoft Excel spreadsheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook to read from (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
range: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet.',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
if (!params.range) {
return `${basePath}/workbook/worksheets?$select=name&$orderby=position&$top=1`
}
const rangeInput = params.range.trim()
if (!rangeInput.includes('!')) {
const sheetOnly = encodeURIComponent(escapeODataString(rangeInput))
return `${basePath}/workbook/worksheets('${sheetOnly}')/usedRange(valuesOnly=true)`
}
const match = rangeInput.match(/^([^!]+)!(.+)$/)
if (!match) {
throw new Error(
`Invalid range format: "${params.range}". Use "Sheet1!A1:B2" or just "Sheet1" to read the whole sheet`
)
}
const sheetName = encodeURIComponent(escapeODataString(match[1]))
const address = encodeURIComponent(match[2])
return `${basePath}/workbook/worksheets('${sheetName}')/range(address='${address}')`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
retry: EXCEL_RETRY_CONFIG,
},
transformResponse: async (response: Response, params?: MicrosoftExcelToolParams) => {
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
// If we came from the worksheets listing (no range provided), resolve first sheet name then fetch range
if (response.url.includes('/workbook/worksheets?')) {
const listData = await response.json()
const firstSheetName: string | undefined = listData?.value?.[0]?.name
if (!firstSheetName) {
throw new Error('No worksheets found in the Excel workbook')
}
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required to read Excel range')
}
const basePath = getItemBasePath(spreadsheetId, driveId)
const rangeUrl = `${basePath}/workbook/worksheets('${encodeURIComponent(escapeODataString(firstSheetName))}')/usedRange(valuesOnly=true)`
const rangeResp = await fetch(rangeUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
})
if (!rangeResp.ok) {
const errorText = await rangeResp.text().catch(() => '')
const detail = parseGraphErrorMessage(rangeResp.status, rangeResp.statusText, errorText)
throw new Error(
`Failed to read worksheet "${firstSheetName}": ${detail}. Provide a range like "Sheet1!A1:B2" or just the sheet name to read the whole sheet.`
)
}
const data = await rangeResp.json()
const address: string = data.address || data.addressLocal || `${firstSheetName}!A1`
const rawValues: ExcelCellValue[][] = data.values || []
const values = trimTrailingEmptyRowsAndColumns(rawValues)
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
const result: MicrosoftExcelReadResponse = {
success: true,
output: {
data: {
range: address,
values,
},
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
return result
}
// Normal path: caller supplied a range; just return the parsed result
const data = await response.json()
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
const address: string = data.address || data.addressLocal || data.range || ''
const rawValues: ExcelCellValue[][] = data.values || []
const values = trimTrailingEmptyRowsAndColumns(rawValues)
const result: MicrosoftExcelReadResponse = {
success: true,
output: {
data: {
range: address,
values,
},
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
return result
},
outputs: {
data: {
type: 'object',
description: 'Range data from the spreadsheet',
properties: {
range: { type: 'string', description: 'The range that was read' },
values: { type: 'array', description: 'Array of rows containing cell values' },
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
export const readV2Tool: ToolConfig<MicrosoftExcelV2ToolParams, MicrosoftExcelV2ReadResponse> = {
id: 'microsoft_excel_read_v2',
name: 'Read from Microsoft Excel V2',
description: 'Read data from a specific sheet in a Microsoft Excel spreadsheet',
version: '2.0.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook to read from (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
sheetName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the sheet/tab to read from (e.g., "Sheet1", "Sales Data")',
},
cellRange: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The cell range to read (e.g., "A1:D10"). If not specified, reads the entire used range.',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const sheetName = params.sheetName?.trim()
if (!sheetName) {
throw new Error('Sheet name is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
const encodedSheetName = encodeURIComponent(escapeODataString(sheetName))
if (!params.cellRange) {
return `${basePath}/workbook/worksheets('${encodedSheetName}')/usedRange(valuesOnly=true)`
}
const cellRange = params.cellRange.trim()
const encodedAddress = encodeURIComponent(cellRange)
return `${basePath}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
retry: EXCEL_RETRY_CONFIG,
},
transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
const address: string = data.address || data.addressLocal || ''
const rawValues: ExcelCellValue[][] = data.values || []
const values = trimTrailingEmptyRowsAndColumns(rawValues)
const sheetName = params?.sheetName || address.split('!')[0] || ''
return {
success: true,
output: {
sheetName,
range: address,
values,
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
sheetName: { type: 'string', description: 'Name of the sheet that was read' },
range: { type: 'string', description: 'The range that was read' },
values: { type: 'array', description: 'Array of rows containing cell values' },
metadata: {
type: 'json',
description: 'Spreadsheet metadata including ID and URL',
properties: {
spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' },
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
},
},
},
}
@@ -0,0 +1,197 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelSortRangeParams,
MicrosoftExcelSortRangeResponse,
} from '@/tools/microsoft_excel/types'
import {
buildWorksheetRangeUrl,
escapeODataString,
getItemBasePath,
getSpreadsheetWebUrl,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Sorts a worksheet range or a table by a single column.
* Uses Microsoft Graph:
* - Range: POST /workbook/worksheets/{name}/range(address='...')/sort/apply
* - Table: POST /workbook/tables('{name}')/sort/apply
*/
export const sortRangeTool: ToolConfig<
MicrosoftExcelSortRangeParams,
MicrosoftExcelSortRangeResponse
> = {
id: 'microsoft_excel_sort_range',
name: 'Sort Microsoft Excel Range',
description: 'Sort a range or table by a column in a Microsoft Excel worksheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
tableName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The name of the table to sort. When provided, the table is sorted and range/sheetName are ignored.',
},
sheetName: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The name of the worksheet (e.g., "Sheet1"). Used for range sorts when the range does not include a sheet name.',
},
range: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The cell range to sort (e.g., "A1:D10" or "Sheet1!A1:D10"). Required when no table name is provided.',
},
sortColumn: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description:
'The zero-based column index within the range or table to sort on (0 = first column).',
},
sortAscending: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to sort in ascending order. Defaults to true.',
},
hasHeaders: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description:
'Whether the range has a header row that should be excluded from sorting. Only applies to range sorts. Defaults to false.',
},
matchCase: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether casing affects string ordering. Defaults to false.',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
const tableName = params.tableName?.trim()
if (tableName) {
return `${basePath}/workbook/tables('${encodeURIComponent(escapeODataString(tableName))}')/sort/apply`
}
return `${buildWorksheetRangeUrl(basePath, params.range, params.sheetName)}/sort/apply`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const key =
typeof params.sortColumn === 'number' ? params.sortColumn : Number(params.sortColumn)
if (!Number.isInteger(key) || key < 0) {
throw new Error('sortColumn must be a non-negative integer column index')
}
const body: Record<string, unknown> = {
fields: [
{
key,
ascending: params.sortAscending ?? true,
sortOn: 'Value',
},
],
matchCase: params.matchCase ?? false,
}
if (!params.tableName?.trim()) {
body.hasHeaders = params.hasHeaders ?? false
body.orientation = 'Rows'
}
return body
},
},
transformResponse: async (_response: Response, params?: MicrosoftExcelSortRangeParams) => {
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
const target = params?.tableName?.trim() || params?.range || ''
return {
success: true,
output: {
sorted: true,
target,
sortColumn: params?.sortColumn ?? 0,
ascending: params?.sortAscending ?? true,
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
sorted: { type: 'boolean', description: 'Whether the sort was applied' },
target: { type: 'string', description: 'The range or table name that was sorted' },
sortColumn: { type: 'number', description: 'The zero-based column index that was sorted on' },
ascending: { type: 'boolean', description: 'Whether the sort was ascending' },
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
+169
View File
@@ -0,0 +1,169 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelTableAddResponse,
MicrosoftExcelTableToolParams,
} from '@/tools/microsoft_excel/types'
import {
escapeODataString,
getItemBasePath,
getSpreadsheetWebUrl,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
export const tableAddTool: ToolConfig<
MicrosoftExcelTableToolParams,
MicrosoftExcelTableAddResponse
> = {
id: 'microsoft_excel_table_add',
name: 'Add to Microsoft Excel Table',
description: 'Add new rows to a Microsoft Excel table',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The ID of the spreadsheet/workbook containing the table (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
tableName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the table to add rows to (e.g., "Table1", "SalesTable")',
},
values: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description:
'The data to add as a 2D array (e.g., [["Alice", 30], ["Bob", 25]]) or array of objects',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const tableName = params.tableName?.trim()
if (!tableName) {
throw new Error('Table name is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
return `${basePath}/workbook/tables('${encodeURIComponent(escapeODataString(tableName))}')/rows/add`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
let processedValues: any = params.values || []
if (
Array.isArray(processedValues) &&
processedValues.length > 0 &&
typeof processedValues[0] === 'object' &&
!Array.isArray(processedValues[0])
) {
const allKeys = new Set<string>()
processedValues.forEach((obj: any) => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach((key) => allKeys.add(key))
}
})
const headers = Array.from(allKeys)
processedValues = processedValues.map((obj: any) => {
if (!obj || typeof obj !== 'object') {
return Array(headers.length).fill('')
}
return headers.map((key) => {
const value = obj[key]
if (value !== null && typeof value === 'object') {
return JSON.stringify(value)
}
return value === undefined ? '' : value
})
})
}
if (!Array.isArray(processedValues) || processedValues.length === 0) {
throw new Error('Values must be a non-empty array')
}
if (!Array.isArray(processedValues[0])) {
processedValues = [processedValues]
}
return {
values: processedValues,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftExcelTableToolParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
index: data.index || 0,
values: data.values || [],
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
index: { type: 'number', description: 'Index of the first row that was added' },
values: { type: 'array', description: 'Array of rows that were added to the table' },
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
+233
View File
@@ -0,0 +1,233 @@
import type { ToolResponse } from '@/tools/types'
// Type for Excel cell values - covers all valid data types that Excel supports
export type ExcelCellValue = string | number | boolean | null
interface MicrosoftExcelRange {
range: string
values: ExcelCellValue[][]
}
interface MicrosoftExcelMetadata {
spreadsheetId: string
spreadsheetUrl?: string
}
export interface MicrosoftExcelReadResponse extends ToolResponse {
output: {
data: MicrosoftExcelRange
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelWriteResponse extends ToolResponse {
output: {
updatedRange: string
updatedRows: number
updatedColumns: number
updatedCells: number
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelTableAddResponse extends ToolResponse {
output: {
index: number
values: ExcelCellValue[][]
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelWorksheetAddResponse extends ToolResponse {
output: {
worksheet: {
id: string
name: string
position: number
visibility: string
}
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelToolParams {
accessToken: string
spreadsheetId: string
driveId?: string
range?: string
values?: ExcelCellValue[][]
valueInputOption?: 'RAW' | 'USER_ENTERED'
includeValuesInResponse?: boolean
}
export interface MicrosoftExcelTableToolParams {
accessToken: string
spreadsheetId: string
driveId?: string
tableName: string
values: ExcelCellValue[][]
}
export interface MicrosoftExcelWorksheetToolParams {
accessToken: string
spreadsheetId: string
driveId?: string
worksheetName: string
}
export interface MicrosoftExcelClearRangeParams {
accessToken: string
spreadsheetId: string
driveId?: string
sheetName?: string
range: string
applyTo?: 'All' | 'Formats' | 'Contents'
}
export interface MicrosoftExcelClearRangeResponse extends ToolResponse {
output: {
cleared: boolean
range: string
applyTo: string
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelFormatRangeParams {
accessToken: string
spreadsheetId: string
driveId?: string
sheetName?: string
range: string
fillColor?: string
fontBold?: boolean
fontItalic?: boolean
fontColor?: string
fontSize?: number
fontName?: string
}
export interface MicrosoftExcelFormatRangeResponse extends ToolResponse {
output: {
formatted: boolean
range: string
fill: { color: string | null } | null
font: {
bold: boolean | null
italic: boolean | null
color: string | null
name: string | null
size: number | null
} | null
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelCreateTableParams {
accessToken: string
spreadsheetId: string
driveId?: string
address: string
hasHeaders?: boolean
}
export interface MicrosoftExcelCreateTableResponse extends ToolResponse {
output: {
table: {
id: string
name: string
showHeaders: boolean
showTotals: boolean
style: string | null
}
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelDeleteWorksheetParams {
accessToken: string
spreadsheetId: string
driveId?: string
worksheetName: string
}
export interface MicrosoftExcelDeleteWorksheetResponse extends ToolResponse {
output: {
deleted: boolean
worksheetName: string
metadata: MicrosoftExcelMetadata
}
}
export interface MicrosoftExcelSortRangeParams {
accessToken: string
spreadsheetId: string
driveId?: string
sheetName?: string
range?: string
tableName?: string
sortColumn: number
sortAscending?: boolean
hasHeaders?: boolean
matchCase?: boolean
}
export interface MicrosoftExcelSortRangeResponse extends ToolResponse {
output: {
sorted: boolean
target: string
sortColumn: number
ascending: boolean
metadata: MicrosoftExcelMetadata
}
}
export type MicrosoftExcelResponse =
| MicrosoftExcelReadResponse
| MicrosoftExcelWriteResponse
| MicrosoftExcelTableAddResponse
| MicrosoftExcelWorksheetAddResponse
| MicrosoftExcelClearRangeResponse
| MicrosoftExcelFormatRangeResponse
| MicrosoftExcelCreateTableResponse
| MicrosoftExcelDeleteWorksheetResponse
| MicrosoftExcelSortRangeResponse
// V2 Types - with separate sheetName param
export interface MicrosoftExcelV2ToolParams {
accessToken: string
spreadsheetId: string
driveId?: string
sheetName: string
cellRange?: string
values?: ExcelCellValue[][]
valueInputOption?: 'RAW' | 'USER_ENTERED'
includeValuesInResponse?: boolean
}
export interface MicrosoftExcelV2ReadResponse extends ToolResponse {
output: {
sheetName: string
range: string
values: ExcelCellValue[][]
metadata: {
spreadsheetId: string
spreadsheetUrl: string
}
}
}
export interface MicrosoftExcelV2WriteResponse extends ToolResponse {
output: {
updatedRange: string | null
updatedRows: number
updatedColumns: number
updatedCells: number
metadata: {
spreadsheetId: string
spreadsheetUrl: string
}
}
}
export type MicrosoftExcelV2Response = MicrosoftExcelV2ReadResponse | MicrosoftExcelV2WriteResponse
@@ -0,0 +1,90 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { parseGraphErrorMessage } from '@/tools/microsoft_excel/utils'
describe('parseGraphErrorMessage', () => {
it('extracts top-level error.message', () => {
const body = JSON.stringify({
error: { code: 'badRequest', message: 'Uploaded fragment overlaps with existing data.' },
})
expect(parseGraphErrorMessage(400, 'Bad Request', body)).toBe(
'Uploaded fragment overlaps with existing data.'
)
})
it('combines top-level and innerError messages with em-dash separator', () => {
const body = JSON.stringify({
error: {
code: 'invalidRequest',
message: 'The request is invalid.',
innerError: { code: 'invalidRange', message: 'Range A1:Z9999 is out of bounds.' },
},
})
expect(parseGraphErrorMessage(400, 'Bad Request', body)).toBe(
'The request is invalid. — Range A1:Z9999 is out of bounds.'
)
})
it('walks nested innerError chain (lowercase spec form)', () => {
const body = JSON.stringify({
error: {
message: 'Outer message.',
innererror: {
message: 'Middle message.',
innererror: { message: 'Innermost message.' },
},
},
})
expect(parseGraphErrorMessage(500, 'Internal Server Error', body)).toBe(
'Outer message. — Middle message. — Innermost message.'
)
})
it('appends details[].message entries', () => {
const body = JSON.stringify({
error: {
message: 'Multiple problems.',
details: [{ message: 'Cell A1 invalid.' }, { message: 'Cell B2 invalid.' }],
},
})
expect(parseGraphErrorMessage(400, 'Bad Request', body)).toBe(
'Multiple problems. — Cell A1 invalid. — Cell B2 invalid.'
)
})
it('falls back to error.code when no messages present', () => {
const body = JSON.stringify({ error: { code: 'itemNotFound' } })
expect(parseGraphErrorMessage(404, 'Not Found', body)).toBe('itemNotFound (404 Not Found)')
})
it('returns raw text when body is not JSON', () => {
expect(parseGraphErrorMessage(502, 'Bad Gateway', 'upstream timeout')).toBe('upstream timeout')
})
it('falls back to status text when body is empty', () => {
expect(parseGraphErrorMessage(503, 'Service Unavailable', '')).toBe('503 Service Unavailable')
})
it('handles deeply nested chain without infinite loop', () => {
let nested: Record<string, unknown> = { message: 'leaf' }
for (let i = 0; i < 50; i++) {
nested = { message: `level-${i}`, innerError: nested }
}
const body = JSON.stringify({ error: nested })
const result = parseGraphErrorMessage(500, 'Internal Server Error', body)
// Should include outer plus capped nested messages, not blow up.
expect(result.startsWith('level-49')).toBe(true)
})
it('deduplicates identical inner messages', () => {
const body = JSON.stringify({
error: {
message: 'Same message.',
innerError: { message: 'Same message.' },
},
})
expect(parseGraphErrorMessage(400, 'Bad Request', body)).toBe('Same message.')
})
})
+272
View File
@@ -0,0 +1,272 @@
import { createLogger } from '@sim/logger'
import { validatePathSegment } from '@/lib/core/security/input-validation'
import type { ExcelCellValue } from '@/tools/microsoft_excel/types'
const logger = createLogger('MicrosoftExcelUtils')
/**
* Extract a developer-readable message from a parsed Microsoft Graph error body.
* Graph errors follow the documented shape:
* { error: { code, message, innerError: { code, message, ... }, details: [...] } }
* See https://learn.microsoft.com/en-us/graph/errors
*
* Walks the nested innerError chain (capped at depth 5) and appends details[].message.
* Returns undefined when no message-like field is present so callers can fall back.
*/
export function parseGraphErrorFromData(data: unknown): string | undefined {
if (!data || typeof data !== 'object') return undefined
const root = (
data as {
error?: {
code?: unknown
message?: unknown
innerError?: unknown
innererror?: unknown
details?: unknown
}
}
).error
if (root && typeof root === 'object') {
const messages: string[] = []
if (typeof root.message === 'string' && root.message.trim()) {
messages.push(root.message.trim())
}
// Walk the (possibly nested) innerError chain. Spec uses `innererror`
// but Graph commonly returns `innerError` — accept both.
let inner: any = (root as any).innererror ?? (root as any).innerError
let depth = 0
while (inner && depth < 5) {
if (typeof inner.message === 'string' && inner.message.trim()) {
const msg = inner.message.trim()
if (!messages.includes(msg)) messages.push(msg)
}
inner = inner.innererror ?? inner.innerError
depth++
}
if (Array.isArray((root as any).details)) {
for (const detail of (root as any).details) {
if (detail && typeof detail.message === 'string' && detail.message.trim()) {
const msg = detail.message.trim()
if (!messages.includes(msg)) messages.push(msg)
}
}
}
if (messages.length > 0) return messages.join(' — ')
if (typeof root.code === 'string' && root.code.trim()) {
return root.code.trim()
}
}
const topMessage = (data as { message?: unknown }).message
if (typeof topMessage === 'string' && topMessage.trim()) {
return topMessage.trim()
}
return undefined
}
/**
* Parse a Microsoft Graph error response body into a string message.
* Used by API routes that have a Response object rather than parsed data.
*/
export function parseGraphErrorMessage(
status: number,
statusText: string,
errorText: string
): string {
try {
const data = JSON.parse(errorText)
const message = parseGraphErrorFromData(data)
if (message) {
// If the only thing we found was the bare error code, append status for context.
const root = data?.error
if (
root &&
message === root.code?.trim?.() &&
!(typeof root.message === 'string' && root.message.trim())
) {
return `${message} (${status} ${statusText})`
}
return message
}
} catch {
if (errorText?.trim()) return errorText.trim()
}
return statusText ? `${status} ${statusText}` : `Microsoft Graph request failed (${status})`
}
/**
* Read an error response body and produce a developer-readable message.
* Safely handles non-JSON bodies and read failures. Used by internal API routes.
*/
export async function extractGraphError(response: Response): Promise<string> {
const errorText = await response.text().catch(() => '')
return parseGraphErrorMessage(response.status, response.statusText, errorText)
}
/**
* Escape a string for use inside an OData single-quoted literal (e.g. a
* `worksheets('...')` or `tables('...')` key). OData escapes an embedded single
* quote by doubling it, so a name like `O'Brien` becomes `O''Brien`; without this
* the apostrophe terminates the literal early and breaks the request URL.
* `encodeURIComponent` leaves apostrophes untouched, so the doubling must happen here.
*/
export function escapeODataString(value: string): string {
return value.replace(/'/g, "''")
}
/** Pattern for Microsoft Graph item/drive IDs: alphanumeric, hyphens, underscores, and ! (for SharePoint b!<base64> format) */
export const GRAPH_ID_PATTERN = /^[a-zA-Z0-9!_-]+$/
/**
* Returns the Graph API base path for an Excel item.
* When driveId is provided, uses /drives/{driveId}/items/{itemId} (SharePoint/shared drives).
* When driveId is omitted, uses /me/drive/items/{itemId} (personal OneDrive).
*/
export function getItemBasePath(spreadsheetId: string, driveId?: string): string {
const spreadsheetValidation = validatePathSegment(spreadsheetId, {
paramName: 'spreadsheetId',
customPattern: GRAPH_ID_PATTERN,
})
if (!spreadsheetValidation.isValid) {
throw new Error(spreadsheetValidation.error)
}
if (driveId) {
const driveValidation = validatePathSegment(driveId, {
paramName: 'driveId',
customPattern: GRAPH_ID_PATTERN,
})
if (!driveValidation.isValid) {
throw new Error(driveValidation.error)
}
return `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}`
}
return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}`
}
/**
* Resolves a worksheet name and cell address from either an explicit sheet name
* plus address, or a combined "Sheet1!A1:B2" range string.
*
* - When `sheetName` is provided, `address` is treated as a bare cell address (e.g. "A1:B2").
* - When `sheetName` is omitted, `address` must be a combined "Sheet1!A1:B2" range.
*
* Throws when a worksheet name cannot be determined or the combined format is invalid.
*/
export function resolveSheetAndAddress(
address: string | undefined,
sheetName?: string
): { sheetName: string; address: string } {
const trimmedAddress = address?.trim()
if (!trimmedAddress) {
throw new Error('A cell range is required (e.g., "A1:B2" or "Sheet1!A1:B2")')
}
const trimmedSheet = sheetName?.trim()
if (trimmedSheet) {
return { sheetName: trimmedSheet, address: trimmedAddress }
}
const match = trimmedAddress.match(/^([^!]+)!(.+)$/)
if (!match) {
throw new Error(
`Invalid range format: "${address}". Provide a sheet name, or use the combined format "Sheet1!A1:B2"`
)
}
return { sheetName: match[1], address: match[2] }
}
/**
* Builds the Graph API URL for a worksheet range object, resolving the sheet name
* from either an explicit `sheetName` param or a combined "Sheet1!A1:B2" address.
* The returned URL has no trailing segment, so callers append `/clear`, `/sort/apply`,
* `/format/fill`, `/format/font`, or nothing for the range object itself.
*/
export function buildWorksheetRangeUrl(
basePath: string,
address: string | undefined,
sheetName?: string
): string {
const resolved = resolveSheetAndAddress(address, sheetName)
const encodedSheet = encodeURIComponent(escapeODataString(resolved.sheetName))
const encodedAddress = encodeURIComponent(resolved.address)
return `${basePath}/workbook/worksheets('${encodedSheet}')/range(address='${encodedAddress}')`
}
export function trimTrailingEmptyRowsAndColumns(matrix: ExcelCellValue[][]): ExcelCellValue[][] {
if (!Array.isArray(matrix) || matrix.length === 0) return []
const isEmptyValue = (v: ExcelCellValue) => v === null || v === ''
// Determine last non-empty row
let lastNonEmptyRowIndex = -1
for (let r = 0; r < matrix.length; r++) {
const row = matrix[r] || []
const hasData = row.some((cell: ExcelCellValue) => !isEmptyValue(cell))
if (hasData) lastNonEmptyRowIndex = r
}
if (lastNonEmptyRowIndex === -1) return []
const trimmedRows = matrix.slice(0, lastNonEmptyRowIndex + 1)
// Determine last non-empty column across trimmed rows
let lastNonEmptyColIndex = -1
for (let r = 0; r < trimmedRows.length; r++) {
const row = trimmedRows[r] || []
for (let c = 0; c < row.length; c++) {
if (!isEmptyValue(row[c])) {
if (c > lastNonEmptyColIndex) lastNonEmptyColIndex = c
}
}
}
if (lastNonEmptyColIndex === -1) return []
return trimmedRows.map((row) => (row || []).slice(0, lastNonEmptyColIndex + 1))
}
/**
* Fetches the browser-accessible web URL for an Excel spreadsheet.
* This URL can be opened in a browser if the user is logged into OneDrive/Microsoft,
* unlike the Graph API URL which requires an access token.
*/
export async function getSpreadsheetWebUrl(
spreadsheetId: string,
accessToken: string,
driveId?: string
): Promise<string> {
const basePath = getItemBasePath(spreadsheetId, driveId)
try {
const response = await fetch(`${basePath}?$select=id,webUrl`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
logger.warn('Failed to fetch spreadsheet webUrl, using Graph API URL as fallback', {
spreadsheetId,
status: response.status,
})
return basePath
}
const data = await response.json()
return data.webUrl || basePath
} catch (error) {
logger.warn('Error fetching spreadsheet webUrl, using Graph API URL as fallback', {
spreadsheetId,
error,
})
return basePath
}
}
@@ -0,0 +1,157 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelWorksheetAddResponse,
MicrosoftExcelWorksheetToolParams,
} from '@/tools/microsoft_excel/types'
import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Tool for adding a new worksheet to a Microsoft Excel workbook
* Uses Microsoft Graph API endpoint: POST /me/drive/items/{id}/workbook/worksheets/add
*/
export const worksheetAddTool: ToolConfig<
MicrosoftExcelWorksheetToolParams,
MicrosoftExcelWorksheetAddResponse
> = {
id: 'microsoft_excel_worksheet_add',
name: 'Add Worksheet to Microsoft Excel',
description: 'Create a new worksheet (sheet) in a Microsoft Excel workbook',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the Excel workbook to add the worksheet to (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
worksheetName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The name of the new worksheet (e.g., "Sales Q1", "Data"). Must be unique within the workbook and cannot exceed 31 characters',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const basePath = getItemBasePath(spreadsheetId, params.driveId)
return `${basePath}/workbook/worksheets/add`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const worksheetName = params.worksheetName?.trim()
if (!worksheetName) {
throw new Error('Worksheet name is required')
}
// Validate worksheet name length (Excel limitation)
if (worksheetName.length > 31) {
throw new Error('Worksheet name cannot exceed 31 characters. Please provide a shorter name')
}
// Validate worksheet name doesn't contain invalid characters
const invalidChars = ['\\', '/', '?', '*', '[', ']', ':']
for (const char of invalidChars) {
if (worksheetName.includes(char)) {
throw new Error(`Worksheet name cannot contain the following characters: \\ / ? * [ ] :`)
}
}
return {
name: worksheetName,
}
},
},
transformResponse: async (response: Response, params?: MicrosoftExcelWorksheetToolParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
const result: MicrosoftExcelWorksheetAddResponse = {
success: true,
output: {
worksheet: {
id: data.id || '',
name: data.name || '',
position: data.position ?? 0,
visibility: data.visibility || 'Visible',
},
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
return result
},
outputs: {
worksheet: {
type: 'object',
description: 'Details of the newly created worksheet',
properties: {
id: { type: 'string', description: 'The unique ID of the worksheet' },
name: { type: 'string', description: 'The name of the worksheet' },
position: { type: 'number', description: 'The zero-based position of the worksheet' },
visibility: {
type: 'string',
description: 'The visibility state of the worksheet (Visible/Hidden/VeryHidden)',
},
},
},
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
+399
View File
@@ -0,0 +1,399 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelToolParams,
MicrosoftExcelV2ToolParams,
MicrosoftExcelV2WriteResponse,
MicrosoftExcelWriteResponse,
} from '@/tools/microsoft_excel/types'
import {
escapeODataString,
getItemBasePath,
getSpreadsheetWebUrl,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Range writes (PATCH /workbook/.../range) are semantically idempotent —
* the same payload produces the same result — so we permit retries on
* transient 429/5xx even though PATCH is not in the default idempotent set.
*/
const EXCEL_RETRY_CONFIG = {
enabled: true,
maxRetries: 3,
initialDelayMs: 500,
maxDelayMs: 30000,
retryIdempotentOnly: false,
} as const
export const writeTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelWriteResponse> = {
id: 'microsoft_excel_write',
name: 'Write to Microsoft Excel',
description: 'Write data to a Microsoft Excel spreadsheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook to write to (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
range: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The range of cells to write to (e.g., "Sheet1!A1:B2")',
},
values: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description:
'The data to write as a 2D array (e.g., [["Name", "Age"], ["Alice", 30]]) or array of objects',
},
valueInputOption: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The format of the data to write',
},
includeValuesInResponse: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Whether to include the written values in the response',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const rangeInput = params.range?.trim()
const match = rangeInput?.match(/^([^!]+)!(.+)$/)
if (!match) {
throw new Error(`Invalid range format: "${params.range}". Use the format "Sheet1!A1:B2"`)
}
const sheetName = encodeURIComponent(escapeODataString(match[1]))
const address = encodeURIComponent(match[2])
const basePath = getItemBasePath(spreadsheetId, params.driveId)
const url = new URL(
`${basePath}/workbook/worksheets('${sheetName}')/range(address='${address}')`
)
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
url.searchParams.append('valueInputOption', valueInputOption)
if (params.includeValuesInResponse) {
url.searchParams.append('includeValuesInResponse', 'true')
}
return url.toString()
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let processedValues: any = params.values || []
if (
Array.isArray(processedValues) &&
processedValues.length > 0 &&
typeof processedValues[0] === 'object' &&
!Array.isArray(processedValues[0])
) {
const allKeys = new Set<string>()
processedValues.forEach((obj: any) => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach((key) => allKeys.add(key))
}
})
const headers = Array.from(allKeys)
const rows = processedValues.map((obj: any) => {
if (!obj || typeof obj !== 'object') {
return Array(headers.length).fill('')
}
return headers.map((key) => {
const value = obj[key]
if (value !== null && typeof value === 'object') {
return JSON.stringify(value)
}
return value === undefined ? '' : value
})
})
processedValues = [headers, ...rows]
}
const body: Record<string, any> = {
majorDimension: 'ROWS',
values: processedValues,
}
if (params.range) {
body.range = params.range
}
return body
},
retry: EXCEL_RETRY_CONFIG,
},
transformResponse: async (response: Response, params?: MicrosoftExcelToolParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
updatedRange: data.address ?? '',
updatedRows: data.rowCount ?? 0,
updatedColumns: data.columnCount ?? 0,
updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0),
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
updatedRange: { type: 'string', description: 'The range that was updated' },
updatedRows: { type: 'number', description: 'Number of rows that were updated' },
updatedColumns: { type: 'number', description: 'Number of columns that were updated' },
updatedCells: { type: 'number', description: 'Number of cells that were updated' },
metadata: {
type: 'object',
description: 'Spreadsheet metadata',
properties: {
spreadsheetId: { type: 'string', description: 'The ID of the spreadsheet' },
spreadsheetUrl: { type: 'string', description: 'URL to access the spreadsheet' },
},
},
},
}
export const writeV2Tool: ToolConfig<MicrosoftExcelV2ToolParams, MicrosoftExcelV2WriteResponse> = {
id: 'microsoft_excel_write_v2',
name: 'Write to Microsoft Excel V2',
description: 'Write data to a specific sheet in a Microsoft Excel spreadsheet',
version: '2.0.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,
oauth: {
required: true,
provider: 'microsoft-excel',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Microsoft Excel API',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the spreadsheet/workbook to write to (e.g., "01ABC123DEF456")',
},
driveId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.',
},
sheetName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the sheet/tab to write to (e.g., "Sheet1", "Sales Data")',
},
cellRange: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The cell range to write to (e.g., "A1:D10", "A1"). Defaults to "A1" if not specified.',
},
values: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description:
'The data to write as a 2D array (e.g. [["Name", "Age"], ["Alice", 30], ["Bob", 25]]) or array of objects.',
},
valueInputOption: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The format of the data to write',
},
includeValuesInResponse: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Whether to include the written values in the response',
},
},
request: {
url: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) {
throw new Error('Spreadsheet ID is required')
}
const sheetName = params.sheetName?.trim()
if (!sheetName) {
throw new Error('Sheet name is required')
}
const cellRange = params.cellRange?.trim() || 'A1'
const encodedSheetName = encodeURIComponent(escapeODataString(sheetName))
const encodedAddress = encodeURIComponent(cellRange)
const basePath = getItemBasePath(spreadsheetId, params.driveId)
const url = new URL(
`${basePath}/workbook/worksheets('${encodedSheetName}')/range(address='${encodedAddress}')`
)
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
url.searchParams.append('valueInputOption', valueInputOption)
if (params.includeValuesInResponse) {
url.searchParams.append('includeValuesInResponse', 'true')
}
return url.toString()
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
let processedValues: any = params.values || []
// Handle array of objects
if (
Array.isArray(processedValues) &&
processedValues.length > 0 &&
typeof processedValues[0] === 'object' &&
!Array.isArray(processedValues[0])
) {
const allKeys = new Set<string>()
processedValues.forEach((obj: any) => {
if (obj && typeof obj === 'object') {
Object.keys(obj).forEach((key) => allKeys.add(key))
}
})
const headers = Array.from(allKeys)
const rows = processedValues.map((obj: any) => {
if (!obj || typeof obj !== 'object') {
return Array(headers.length).fill('')
}
return headers.map((key) => {
const value = obj[key]
if (value !== null && typeof value === 'object') {
return JSON.stringify(value)
}
return value === undefined ? '' : value
})
})
processedValues = [headers, ...rows]
}
const body: Record<string, any> = {
majorDimension: 'ROWS',
values: processedValues,
}
return body
},
retry: EXCEL_RETRY_CONFIG,
},
transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => {
const data = await response.json()
const spreadsheetId = params?.spreadsheetId?.trim() || ''
const driveId = params?.driveId
const accessToken = params?.accessToken
if (!accessToken) {
throw new Error('Access token is required')
}
const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId)
return {
success: true,
output: {
updatedRange: data.address ?? null,
updatedRows: data.rowCount ?? 0,
updatedColumns: data.columnCount ?? 0,
updatedCells: (data.rowCount ?? 0) * (data.columnCount ?? 0),
metadata: {
spreadsheetId,
spreadsheetUrl: webUrl,
},
},
}
},
outputs: {
updatedRange: { type: 'string', description: 'Range of cells that were updated' },
updatedRows: { type: 'number', description: 'Number of rows updated' },
updatedColumns: { type: 'number', description: 'Number of columns updated' },
updatedCells: { type: 'number', description: 'Number of cells updated' },
metadata: {
type: 'json',
description: 'Spreadsheet metadata including ID and URL',
properties: {
spreadsheetId: { type: 'string', description: 'Microsoft Excel spreadsheet ID' },
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
},
},
},
}