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,424 @@
|
||||
import type {
|
||||
GoogleSheetsAppendResponse,
|
||||
GoogleSheetsToolParams,
|
||||
GoogleSheetsV2AppendResponse,
|
||||
GoogleSheetsV2ToolParams,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendResponse> = {
|
||||
id: 'google_sheets_append',
|
||||
name: 'Append to Google Sheets',
|
||||
description: 'Append data to the end of a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the spreadsheet to append to',
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The A1 notation range to append after (e.g. "Sheet1", "Sheet1!A:D")',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.',
|
||||
},
|
||||
valueInputOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The format of the data to append',
|
||||
},
|
||||
insertDataOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'How to insert the data (OVERWRITE or INSERT_ROWS)',
|
||||
},
|
||||
includeValuesInResponse: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Whether to include the appended values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// If range is not provided, use a default range for the first sheet
|
||||
const range = params.range || 'Sheet1'
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}:append`
|
||||
)
|
||||
|
||||
// Default to USER_ENTERED if not specified
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
// Only send insertDataOption when the user provides it; the API defaults to OVERWRITE
|
||||
if (params.insertDataOption) {
|
||||
url.searchParams.append('insertDataOption', params.insertDataOption)
|
||||
}
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let processedValues: any = params.values || []
|
||||
|
||||
// Handle case where values might be a string (potentially JSON string)
|
||||
if (typeof processedValues === 'string') {
|
||||
try {
|
||||
// Try to parse it as JSON
|
||||
processedValues = JSON.parse(processedValues)
|
||||
} catch (_error) {
|
||||
// If the input contains literal newlines causing JSON parse to fail,
|
||||
// try a more robust approach
|
||||
try {
|
||||
// Replace literal newlines with escaped newlines for JSON parsing
|
||||
const sanitizedInput = (processedValues as string)
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
|
||||
// Try to parse again with sanitized input
|
||||
processedValues = JSON.parse(sanitizedInput)
|
||||
} catch (_secondError) {
|
||||
// If all parsing attempts fail, wrap as a single cell value
|
||||
processedValues = [[processedValues]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New logic to handle array of objects
|
||||
if (
|
||||
Array.isArray(processedValues) &&
|
||||
processedValues.length > 0 &&
|
||||
typeof processedValues[0] === 'object' &&
|
||||
!Array.isArray(processedValues[0])
|
||||
) {
|
||||
// It's an array of objects
|
||||
|
||||
// First, extract all unique keys from all objects to create headers
|
||||
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)
|
||||
|
||||
// Then create rows with object values in the order of headers
|
||||
const rows = processedValues.map((obj: any) => {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
// Handle non-object items by creating an array with empty values
|
||||
return Array(headers.length).fill('')
|
||||
}
|
||||
return headers.map((key) => {
|
||||
const value = obj[key]
|
||||
// Handle nested objects/arrays by converting to JSON string
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return value === undefined ? '' : value
|
||||
})
|
||||
})
|
||||
|
||||
// Add headers as the first row, then add data rows
|
||||
processedValues = [headers, ...rows]
|
||||
}
|
||||
// Continue with existing logic for other array types
|
||||
else if (!Array.isArray(processedValues)) {
|
||||
processedValues = [[String(processedValues)]]
|
||||
} else if (!processedValues.every((item: any) => Array.isArray(item))) {
|
||||
// If it's an array but not all elements are arrays, wrap each element
|
||||
processedValues = (processedValues as any[]).map((row: any) =>
|
||||
Array.isArray(row) ? row : [String(row)]
|
||||
)
|
||||
}
|
||||
|
||||
const body: Record<string, any> = {
|
||||
majorDimension: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
|
||||
// Only include range if it's provided
|
||||
if (params.range) {
|
||||
body.range = params.range
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL (guard if url is missing)
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
// Create a simple metadata object with just the ID and URL
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
properties: {},
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
output: {
|
||||
tableRange: data.tableRange || '',
|
||||
updatedRange: data.updates?.updatedRange || '',
|
||||
updatedRows: data.updates?.updatedRows || 0,
|
||||
updatedColumns: data.updates?.updatedColumns || 0,
|
||||
updatedCells: data.updates?.updatedCells || 0,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const appendV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2AppendResponse> = {
|
||||
id: 'google_sheets_append_v2',
|
||||
name: 'Append to Google Sheets V2',
|
||||
description: 'Append data to the end of a specific sheet in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the sheet/tab to append to',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The data to append as a 2D array (e.g. [["Alice", 30], ["Bob", 25]]) or array of objects.',
|
||||
},
|
||||
valueInputOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The format of the data to append',
|
||||
},
|
||||
insertDataOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'How to insert the data (OVERWRITE or INSERT_ROWS)',
|
||||
},
|
||||
includeValuesInResponse: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Whether to include the appended values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const sheetName = params.sheetName?.trim()
|
||||
if (!sheetName) {
|
||||
throw new Error('Sheet name is required')
|
||||
}
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(sheetName)}:append`
|
||||
)
|
||||
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
if (params.insertDataOption) {
|
||||
url.searchParams.append('insertDataOption', params.insertDataOption)
|
||||
}
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let processedValues: any = params.values || []
|
||||
|
||||
if (typeof processedValues === 'string') {
|
||||
try {
|
||||
processedValues = JSON.parse(processedValues)
|
||||
} catch (_error) {
|
||||
try {
|
||||
const sanitizedInput = (processedValues as string)
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
processedValues = JSON.parse(sanitizedInput)
|
||||
} catch (_secondError) {
|
||||
processedValues = [[processedValues]]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
} else if (!Array.isArray(processedValues)) {
|
||||
processedValues = [[String(processedValues)]]
|
||||
} else if (!processedValues.every((item: any) => Array.isArray(item))) {
|
||||
processedValues = (processedValues as any[]).map((row: any) =>
|
||||
Array.isArray(row) ? row : [String(row)]
|
||||
)
|
||||
}
|
||||
|
||||
const body: Record<string, any> = {
|
||||
majorDimension: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tableRange: data.tableRange ?? '',
|
||||
updatedRange: data.updates?.updatedRange ?? '',
|
||||
updatedRows: data.updates?.updatedRows ?? 0,
|
||||
updatedColumns: data.updates?.updatedColumns ?? 0,
|
||||
updatedCells: data.updates?.updatedCells ?? 0,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tableRange: { type: 'string', description: 'Range of the table where data was appended' },
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
GoogleSheetsV2BatchClearParams,
|
||||
GoogleSheetsV2BatchClearResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const batchClearV2Tool: ToolConfig<
|
||||
GoogleSheetsV2BatchClearParams,
|
||||
GoogleSheetsV2BatchClearResponse
|
||||
> = {
|
||||
id: 'google_sheets_batch_clear_v2',
|
||||
name: 'Batch Clear Google Sheets V2',
|
||||
description: 'Clear multiple ranges in a Google Sheets spreadsheet in a single request',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
ranges: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of ranges to clear (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Each range should include sheet name.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchClear`
|
||||
},
|
||||
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 ranges = params.ranges
|
||||
if (!ranges || !Array.isArray(ranges) || ranges.length === 0) {
|
||||
throw new Error('At least one range is required')
|
||||
}
|
||||
|
||||
return {
|
||||
ranges,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const spreadsheetId = data.spreadsheetId ?? ''
|
||||
const clearedRanges = data.clearedRanges ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
clearedRanges,
|
||||
metadata: {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The spreadsheet ID' },
|
||||
clearedRanges: {
|
||||
type: 'array',
|
||||
description: 'Array of ranges that were cleared',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import type {
|
||||
GoogleSheetsV2BatchGetParams,
|
||||
GoogleSheetsV2BatchGetResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const batchGetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2BatchGetParams,
|
||||
GoogleSheetsV2BatchGetResponse
|
||||
> = {
|
||||
id: 'google_sheets_batch_get_v2',
|
||||
name: 'Batch Read Google Sheets V2',
|
||||
description: 'Read multiple ranges from a Google Sheets spreadsheet in a single request',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
ranges: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of ranges to read (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Each range should include sheet name.',
|
||||
},
|
||||
majorDimension: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The major dimension of values: "ROWS" (default) or "COLUMNS"',
|
||||
},
|
||||
valueRenderOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'How values should be rendered: "FORMATTED_VALUE" (default), "UNFORMATTED_VALUE", or "FORMULA"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
const ranges = params.ranges
|
||||
if (!ranges || !Array.isArray(ranges) || ranges.length === 0) {
|
||||
throw new Error('At least one range is required')
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams()
|
||||
ranges.forEach((range: string) => {
|
||||
queryParams.append('ranges', range)
|
||||
})
|
||||
|
||||
if (params.majorDimension) {
|
||||
queryParams.append('majorDimension', params.majorDimension)
|
||||
}
|
||||
|
||||
if (params.valueRenderOption) {
|
||||
queryParams.append('valueRenderOption', params.valueRenderOption)
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchGet?${queryParams.toString()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const valueRanges =
|
||||
data.valueRanges?.map((vr: any) => ({
|
||||
range: vr.range ?? '',
|
||||
majorDimension: vr.majorDimension ?? 'ROWS',
|
||||
values: vr.values ?? [],
|
||||
})) ?? []
|
||||
|
||||
const spreadsheetId = data.spreadsheetId ?? ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
valueRanges,
|
||||
metadata: {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The spreadsheet ID' },
|
||||
valueRanges: {
|
||||
type: 'array',
|
||||
description: 'Array of value ranges read from the spreadsheet',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
range: { type: 'string', description: 'The range that was read' },
|
||||
majorDimension: { type: 'string', description: 'Major dimension (ROWS or COLUMNS)' },
|
||||
values: { type: 'array', description: 'The cell values as a 2D array' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type {
|
||||
GoogleSheetsV2BatchUpdateParams,
|
||||
GoogleSheetsV2BatchUpdateResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const batchUpdateV2Tool: ToolConfig<
|
||||
GoogleSheetsV2BatchUpdateParams,
|
||||
GoogleSheetsV2BatchUpdateResponse
|
||||
> = {
|
||||
id: 'google_sheets_batch_update_v2',
|
||||
name: 'Batch Update Google Sheets V2',
|
||||
description: 'Update multiple ranges in a Google Sheets spreadsheet in a single request',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
data: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of value ranges to update. Each item should have "range" (e.g., "Sheet1!A1:D10") and "values" (2D array).',
|
||||
},
|
||||
valueInputOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'How input data should be interpreted: "RAW" or "USER_ENTERED" (default). USER_ENTERED parses formulas.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values:batchUpdate`
|
||||
},
|
||||
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 data = params.data
|
||||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||
throw new Error('At least one data range is required')
|
||||
}
|
||||
|
||||
return {
|
||||
valueInputOption: params.valueInputOption ?? 'USER_ENTERED',
|
||||
data: data.map((item: any) => ({
|
||||
range: item.range,
|
||||
values: item.values,
|
||||
})),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const responses =
|
||||
data.responses?.map((r: any) => ({
|
||||
spreadsheetId: r.spreadsheetId ?? '',
|
||||
updatedRange: r.updatedRange ?? '',
|
||||
updatedRows: r.updatedRows ?? 0,
|
||||
updatedColumns: r.updatedColumns ?? 0,
|
||||
updatedCells: r.updatedCells ?? 0,
|
||||
})) ?? []
|
||||
|
||||
const spreadsheetId = data.spreadsheetId ?? ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
totalUpdatedRows: data.totalUpdatedRows ?? 0,
|
||||
totalUpdatedColumns: data.totalUpdatedColumns ?? 0,
|
||||
totalUpdatedCells: data.totalUpdatedCells ?? 0,
|
||||
totalUpdatedSheets: data.totalUpdatedSheets ?? 0,
|
||||
responses,
|
||||
metadata: {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The spreadsheet ID' },
|
||||
totalUpdatedRows: { type: 'number', description: 'Total number of rows updated' },
|
||||
totalUpdatedColumns: { type: 'number', description: 'Total number of columns updated' },
|
||||
totalUpdatedCells: { type: 'number', description: 'Total number of cells updated' },
|
||||
totalUpdatedSheets: { type: 'number', description: 'Total number of sheets updated' },
|
||||
responses: {
|
||||
type: 'array',
|
||||
description: 'Array of update responses for each range',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'The spreadsheet ID' },
|
||||
updatedRange: { type: 'string', description: 'The range that was updated' },
|
||||
updatedRows: { type: 'number', description: 'Number of rows updated in this range' },
|
||||
updatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Number of columns updated in this range',
|
||||
},
|
||||
updatedCells: { type: 'number', description: 'Number of cells updated in this range' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
GoogleSheetsV2ClearParams,
|
||||
GoogleSheetsV2ClearResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const clearV2Tool: ToolConfig<GoogleSheetsV2ClearParams, GoogleSheetsV2ClearResponse> = {
|
||||
id: 'google_sheets_clear_v2',
|
||||
name: 'Clear Google Sheets Range V2',
|
||||
description: 'Clear values from a specific range in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the sheet/tab to clear',
|
||||
},
|
||||
cellRange: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The cell range to clear (e.g. "A1:D10"). Clears entire sheet if not specified.',
|
||||
},
|
||||
},
|
||||
|
||||
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()
|
||||
const fullRange = cellRange ? `${sheetName}!${cellRange}` : sheetName
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(fullRange)}: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: () => ({}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2ClearParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
const spreadsheetId = params?.spreadsheetId ?? ''
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
clearedRange: data.clearedRange ?? '',
|
||||
sheetName: params?.sheetName ?? '',
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
clearedRange: { type: 'string', description: 'The range that was cleared' },
|
||||
sheetName: { type: 'string', description: 'Name of the sheet that was cleared' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
GoogleSheetsV2CopySheetParams,
|
||||
GoogleSheetsV2CopySheetResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const copySheetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2CopySheetParams,
|
||||
GoogleSheetsV2CopySheetResponse
|
||||
> = {
|
||||
id: 'google_sheets_copy_sheet_v2',
|
||||
name: 'Copy Sheet V2',
|
||||
description: 'Copy a sheet from one spreadsheet to another',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
sourceSpreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the sheet to copy (numeric ID, not the sheet name). Use Get Spreadsheet to find sheet IDs.',
|
||||
},
|
||||
destinationSpreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the destination spreadsheet where the sheet will be copied',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const sourceSpreadsheetId = params.sourceSpreadsheetId?.trim()
|
||||
if (!sourceSpreadsheetId) {
|
||||
throw new Error('Source spreadsheet ID is required')
|
||||
}
|
||||
|
||||
const sheetId = params.sheetId
|
||||
if (sheetId === undefined || sheetId === null) {
|
||||
throw new Error('Sheet ID is required')
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${sourceSpreadsheetId}/sheets/${sheetId}:copyTo`
|
||||
},
|
||||
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 destinationSpreadsheetId = params.destinationSpreadsheetId?.trim()
|
||||
if (!destinationSpreadsheetId) {
|
||||
throw new Error('Destination spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
destinationSpreadsheetId,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2CopySheetParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
sheetId: data.sheetId ?? 0,
|
||||
title: data.title ?? '',
|
||||
index: data.index ?? 0,
|
||||
sheetType: data.sheetType ?? 'GRID',
|
||||
destinationSpreadsheetId: params?.destinationSpreadsheetId ?? '',
|
||||
destinationSpreadsheetUrl: `https://docs.google.com/spreadsheets/d/${params?.destinationSpreadsheetId ?? ''}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
sheetId: {
|
||||
type: 'number',
|
||||
description: 'The ID of the newly created sheet in the destination',
|
||||
},
|
||||
title: { type: 'string', description: 'The title of the copied sheet' },
|
||||
index: { type: 'number', description: 'The index (position) of the copied sheet' },
|
||||
sheetType: { type: 'string', description: 'The type of the sheet (GRID, CHART, etc.)' },
|
||||
destinationSpreadsheetId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the destination spreadsheet',
|
||||
},
|
||||
destinationSpreadsheetUrl: {
|
||||
type: 'string',
|
||||
description: 'URL to the destination spreadsheet',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type {
|
||||
GoogleSheetsV2CreateSpreadsheetParams,
|
||||
GoogleSheetsV2CreateSpreadsheetResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createSpreadsheetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2CreateSpreadsheetParams,
|
||||
GoogleSheetsV2CreateSpreadsheetResponse
|
||||
> = {
|
||||
id: 'google_sheets_create_spreadsheet_v2',
|
||||
name: 'Create Spreadsheet V2',
|
||||
description: 'Create a new Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The title of the new spreadsheet',
|
||||
},
|
||||
sheetTitles: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of sheet names to create (e.g., ["Sheet1", "Data", "Summary"]). Defaults to a single "Sheet1".',
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The locale of the spreadsheet (e.g., "en_US")',
|
||||
},
|
||||
timeZone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The time zone of the spreadsheet (e.g., "America/New_York")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://sheets.googleapis.com/v4/spreadsheets',
|
||||
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 title = params.title?.trim()
|
||||
if (!title) {
|
||||
throw new Error('Spreadsheet title is required')
|
||||
}
|
||||
|
||||
const sheetTitles = params.sheetTitles ?? ['Sheet1']
|
||||
const sheets = sheetTitles.map((sheetTitle: string, index: number) => ({
|
||||
properties: {
|
||||
title: sheetTitle,
|
||||
index,
|
||||
},
|
||||
}))
|
||||
|
||||
const body: any = {
|
||||
properties: {
|
||||
title,
|
||||
},
|
||||
sheets,
|
||||
}
|
||||
|
||||
if (params.locale) {
|
||||
body.properties.locale = params.locale
|
||||
}
|
||||
|
||||
if (params.timeZone) {
|
||||
body.properties.timeZone = params.timeZone
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const sheets =
|
||||
data.sheets?.map((sheet: any) => ({
|
||||
sheetId: sheet.properties?.sheetId ?? 0,
|
||||
title: sheet.properties?.title ?? '',
|
||||
index: sheet.properties?.index ?? 0,
|
||||
})) ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId: data.spreadsheetId ?? '',
|
||||
title: data.properties?.title ?? '',
|
||||
spreadsheetUrl: data.spreadsheetUrl ?? '',
|
||||
sheets,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The ID of the created spreadsheet' },
|
||||
title: { type: 'string', description: 'The title of the created spreadsheet' },
|
||||
spreadsheetUrl: { type: 'string', description: 'URL to the created spreadsheet' },
|
||||
sheets: {
|
||||
type: 'array',
|
||||
description: 'List of sheets created in the spreadsheet',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sheetId: { type: 'number', description: 'The sheet ID' },
|
||||
title: { type: 'string', description: 'The sheet title/name' },
|
||||
index: { type: 'number', description: 'The sheet index (position)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface GoogleSheetsV2DeleteRowsParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
sheetId: number
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2DeleteRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
sheetId: number
|
||||
deletedRowRange: string
|
||||
metadata: {
|
||||
spreadsheetId: string
|
||||
spreadsheetUrl: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteRowsV2Tool: ToolConfig<
|
||||
GoogleSheetsV2DeleteRowsParams,
|
||||
GoogleSheetsV2DeleteRowsResponse
|
||||
> = {
|
||||
id: 'google_sheets_delete_rows_v2',
|
||||
name: 'Delete Rows from Google Sheets V2',
|
||||
description: 'Delete rows from a sheet in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The numeric ID of the sheet/tab (not the sheet name). Use Get Spreadsheet to find sheet IDs.',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The start row index (0-based, inclusive) of the rows to delete',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end row index (0-based, exclusive) of the rows to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (params.sheetId === undefined || params.sheetId === null) {
|
||||
throw new Error('Sheet ID is required')
|
||||
}
|
||||
if (params.startIndex === undefined || params.startIndex === null) {
|
||||
throw new Error('Start index is required')
|
||||
}
|
||||
if (params.endIndex === undefined || params.endIndex === null) {
|
||||
throw new Error('End index is required')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: params.sheetId,
|
||||
dimension: 'ROWS',
|
||||
startIndex: params.startIndex,
|
||||
endIndex: params.endIndex,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteRowsParams) => {
|
||||
await response.json()
|
||||
|
||||
const spreadsheetId = params?.spreadsheetId ?? ''
|
||||
const startIndex = params?.startIndex ?? 0
|
||||
const endIndex = params?.endIndex ?? 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
sheetId: params?.sheetId ?? 0,
|
||||
deletedRowRange: `rows ${startIndex} to ${endIndex}`,
|
||||
metadata: {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
sheetId: { type: 'number', description: 'The numeric ID of the sheet' },
|
||||
deletedRowRange: {
|
||||
type: 'string',
|
||||
description: 'Description of the deleted row range',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface GoogleSheetsV2DeleteSheetParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
sheetId: number
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2DeleteSheetResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
deletedSheetId: number
|
||||
metadata: {
|
||||
spreadsheetId: string
|
||||
spreadsheetUrl: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteSheetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2DeleteSheetParams,
|
||||
GoogleSheetsV2DeleteSheetResponse
|
||||
> = {
|
||||
id: 'google_sheets_delete_sheet_v2',
|
||||
name: 'Delete Sheet V2',
|
||||
description: 'Delete a sheet/tab from a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetId: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The numeric ID of the sheet/tab to delete (not the sheet name). Use Get Spreadsheet to find sheet IDs.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}:batchUpdate`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (params.sheetId === undefined || params.sheetId === null) {
|
||||
throw new Error('Sheet ID is required')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
deleteSheet: {
|
||||
sheetId: params.sheetId,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSheetParams) => {
|
||||
await response.json()
|
||||
|
||||
const spreadsheetId = params?.spreadsheetId ?? ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
deletedSheetId: params?.sheetId ?? 0,
|
||||
metadata: {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
deletedSheetId: { type: 'number', description: 'The numeric ID of the deleted sheet' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface GoogleSheetsV2DeleteSpreadsheetParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2DeleteSpreadsheetResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
deleted: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteSpreadsheetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2DeleteSpreadsheetParams,
|
||||
GoogleSheetsV2DeleteSpreadsheetResponse
|
||||
> = {
|
||||
id: 'google_sheets_delete_spreadsheet_v2',
|
||||
name: 'Delete Spreadsheet V2',
|
||||
description: 'Permanently delete a Google Sheets spreadsheet using the Google Drive API',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the Google Sheets spreadsheet to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
return `https://www.googleapis.com/drive/v3/files/${spreadsheetId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2DeleteSpreadsheetParams) => {
|
||||
const spreadsheetId = params?.spreadsheetId ?? ''
|
||||
|
||||
if (response.status === 204 || response.ok) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId,
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
throw new Error(data.error?.message ?? 'Failed to delete spreadsheet')
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The ID of the deleted spreadsheet' },
|
||||
deleted: { type: 'boolean', description: 'Whether the spreadsheet was successfully deleted' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { filterSheetRows } from '@/tools/google_sheets/filter'
|
||||
|
||||
const VALUES: unknown[][] = [
|
||||
['Name', 'Email', 'Status', 'Score'],
|
||||
['Alice', 'alice@example.com', 'Active', '90'],
|
||||
['Bob', 'bob@test.com', 'Closed', '40'],
|
||||
['Carol', 'carol@example.com', 'Active', '7'],
|
||||
]
|
||||
|
||||
describe('filterSheetRows', () => {
|
||||
it('passes values through unchanged when no filter column is provided', () => {
|
||||
const result = filterSheetRows(VALUES, {})
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.values).toBe(VALUES)
|
||||
expect(result.totalRows).toBe(3)
|
||||
})
|
||||
|
||||
it('passes through when filterValue is empty', () => {
|
||||
const result = filterSheetRows(VALUES, { filterColumn: 'Status', filterValue: '' })
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.values).toBe(VALUES)
|
||||
})
|
||||
|
||||
it('defaults to case-insensitive contains', () => {
|
||||
const result = filterSheetRows(VALUES, { filterColumn: 'status', filterValue: 'active' })
|
||||
expect(result.applied).toBe(true)
|
||||
expect(result.columnFound).toBe(true)
|
||||
expect(result.values).toEqual([VALUES[0], VALUES[1], VALUES[3]])
|
||||
expect(result.matchedRows).toBe(2)
|
||||
})
|
||||
|
||||
it('matches column names case-insensitively and trims whitespace', () => {
|
||||
const result = filterSheetRows(VALUES, {
|
||||
filterColumn: ' EMAIL ',
|
||||
filterValue: 'example.com',
|
||||
})
|
||||
expect(result.columnFound).toBe(true)
|
||||
expect(result.matchedRows).toBe(2)
|
||||
})
|
||||
|
||||
it('supports exact and not_equals', () => {
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'Active',
|
||||
filterMatchType: 'exact',
|
||||
}).matchedRows
|
||||
).toBe(2)
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'Active',
|
||||
filterMatchType: 'not_equals',
|
||||
}).matchedRows
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('supports starts_with, ends_with, and not_contains', () => {
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Email',
|
||||
filterValue: 'bob',
|
||||
filterMatchType: 'starts_with',
|
||||
}).matchedRows
|
||||
).toBe(1)
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Email',
|
||||
filterValue: '.com',
|
||||
filterMatchType: 'ends_with',
|
||||
}).matchedRows
|
||||
).toBe(3)
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Email',
|
||||
filterValue: 'example.com',
|
||||
filterMatchType: 'not_contains',
|
||||
}).matchedRows
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('compares numerically for ordering operators (not substring)', () => {
|
||||
const gt = filterSheetRows(VALUES, {
|
||||
filterColumn: 'Score',
|
||||
filterValue: '50',
|
||||
filterMatchType: 'gt',
|
||||
})
|
||||
expect(gt.matchedRows).toBe(1)
|
||||
expect(gt.values).toEqual([VALUES[0], VALUES[1]])
|
||||
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Score',
|
||||
filterValue: '40',
|
||||
filterMatchType: 'gte',
|
||||
}).matchedRows
|
||||
).toBe(2)
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Score',
|
||||
filterValue: '40',
|
||||
filterMatchType: 'lt',
|
||||
}).matchedRows
|
||||
).toBe(1)
|
||||
expect(
|
||||
filterSheetRows(VALUES, {
|
||||
filterColumn: 'Score',
|
||||
filterValue: '40',
|
||||
filterMatchType: 'lte',
|
||||
}).matchedRows
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
it('orders negative numbers correctly', () => {
|
||||
const temps: unknown[][] = [
|
||||
['City', 'Temp'],
|
||||
['A', '-5'],
|
||||
['B', '0'],
|
||||
['C', '-12'],
|
||||
['D', '3'],
|
||||
]
|
||||
expect(
|
||||
filterSheetRows(temps, { filterColumn: 'Temp', filterValue: '-5', filterMatchType: 'gte' })
|
||||
.matchedRows
|
||||
).toBe(3)
|
||||
expect(
|
||||
filterSheetRows(temps, { filterColumn: 'Temp', filterValue: '0', filterMatchType: 'lt' })
|
||||
.matchedRows
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
it('excludes blank and non-numeric cells from numeric comparisons', () => {
|
||||
const scores: unknown[][] = [
|
||||
['Name', 'Score'],
|
||||
['Alice', '90'],
|
||||
['Bob', ''],
|
||||
['Carol', 'N/A'],
|
||||
['Dan', '60'],
|
||||
]
|
||||
const result = filterSheetRows(scores, {
|
||||
filterColumn: 'Score',
|
||||
filterValue: '50',
|
||||
filterMatchType: 'gt',
|
||||
})
|
||||
expect(result.matchedRows).toBe(2)
|
||||
expect(result.values).toEqual([scores[0], scores[1], scores[4]])
|
||||
})
|
||||
|
||||
it('falls back to lexicographic ordering when values are not numeric (ISO dates)', () => {
|
||||
const dated: unknown[][] = [
|
||||
['Task', 'Due'],
|
||||
['A', '2026-01-15'],
|
||||
['B', '2026-03-01'],
|
||||
['C', '2025-12-31'],
|
||||
]
|
||||
const result = filterSheetRows(dated, {
|
||||
filterColumn: 'Due',
|
||||
filterValue: '2026-01-01',
|
||||
filterMatchType: 'gte',
|
||||
})
|
||||
expect(result.matchedRows).toBe(2)
|
||||
})
|
||||
|
||||
it('reports columnFound=false and leaves values unchanged when the column is missing', () => {
|
||||
const result = filterSheetRows(VALUES, {
|
||||
filterColumn: 'Nonexistent',
|
||||
filterValue: 'x',
|
||||
})
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.columnFound).toBe(false)
|
||||
expect(result.matchedRows).toBe(0)
|
||||
expect(result.values).toBe(VALUES)
|
||||
expect(result.totalRows).toBe(3)
|
||||
})
|
||||
|
||||
it('handles a header-only sheet and reports the column as found when it exists', () => {
|
||||
const headerOnly: unknown[][] = [['Name', 'Status']]
|
||||
const result = filterSheetRows(headerOnly, { filterColumn: 'Status', filterValue: 'Active' })
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.columnFound).toBe(true)
|
||||
expect(result.matchedRows).toBe(0)
|
||||
expect(result.totalRows).toBe(0)
|
||||
expect(result.values).toBe(headerOnly)
|
||||
})
|
||||
|
||||
it('reports columnFound=false for a header-only sheet when the column is absent', () => {
|
||||
const headerOnly: unknown[][] = [['Name', 'Status']]
|
||||
const result = filterSheetRows(headerOnly, { filterColumn: 'Nonexistent', filterValue: 'x' })
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.columnFound).toBe(false)
|
||||
expect(result.matchedRows).toBe(0)
|
||||
expect(result.values).toBe(headerOnly)
|
||||
})
|
||||
|
||||
it('reports columnFound=false for an empty values array', () => {
|
||||
const empty: unknown[][] = []
|
||||
const result = filterSheetRows(empty, { filterColumn: 'Status', filterValue: 'Active' })
|
||||
expect(result.applied).toBe(false)
|
||||
expect(result.columnFound).toBe(false)
|
||||
expect(result.matchedRows).toBe(0)
|
||||
expect(result.totalRows).toBe(0)
|
||||
})
|
||||
|
||||
it('treats missing cells as empty strings', () => {
|
||||
const sparse: unknown[][] = [['Name', 'Status'], ['Alice'], ['Bob', 'Active']]
|
||||
const result = filterSheetRows(sparse, {
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'Active',
|
||||
filterMatchType: 'exact',
|
||||
})
|
||||
expect(result.matchedRows).toBe(1)
|
||||
expect(result.values).toEqual([sparse[0], sparse[2]])
|
||||
})
|
||||
|
||||
it('always retains the header row in filtered output', () => {
|
||||
const result = filterSheetRows(VALUES, {
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'no-match',
|
||||
filterMatchType: 'exact',
|
||||
})
|
||||
expect(result.values).toEqual([VALUES[0]])
|
||||
expect(result.matchedRows).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Client-side row filtering for Google Sheets read results.
|
||||
*
|
||||
* The Google Sheets REST API (`spreadsheets.values.get`) has no server-side
|
||||
* content filtering — `DataFilter` selects only by A1 range, grid range, or
|
||||
* developer metadata, never by cell value. Filtering by cell content must
|
||||
* therefore happen after values are fetched, over the window of rows the read
|
||||
* returned (e.g. the default `A1:Z1000`), not the entire sheet.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported ways to compare a cell against the filter value. Text operators are
|
||||
* case-insensitive. The ordering operators (`gt`/`gte`/`lt`/`lte`) compare
|
||||
* numerically when both operands parse as finite numbers, fall back to
|
||||
* case-insensitive lexicographic comparison when both are non-numeric (which
|
||||
* orders ISO dates correctly), and never match when one side is numeric and the
|
||||
* other is not (the values are not comparable).
|
||||
*/
|
||||
export type SheetFilterMatchType =
|
||||
| 'contains'
|
||||
| 'not_contains'
|
||||
| 'exact'
|
||||
| 'not_equals'
|
||||
| 'starts_with'
|
||||
| 'ends_with'
|
||||
| 'gt'
|
||||
| 'gte'
|
||||
| 'lt'
|
||||
| 'lte'
|
||||
|
||||
export interface SheetFilterOptions {
|
||||
filterColumn?: string
|
||||
filterValue?: string
|
||||
filterMatchType?: SheetFilterMatchType
|
||||
}
|
||||
|
||||
export interface SheetFilterResult {
|
||||
/** The (possibly filtered) values, always including the header row when present. */
|
||||
values: unknown[][]
|
||||
/** Whether row filtering was actually applied to the data rows. */
|
||||
applied: boolean
|
||||
/** Whether the requested filter column was found in the header row. */
|
||||
columnFound: boolean
|
||||
/** Number of data rows (excluding the header) that matched the filter. */
|
||||
matchedRows: number
|
||||
/** Total number of data rows (excluding the header) that were considered. */
|
||||
totalRows: number
|
||||
}
|
||||
|
||||
const DEFAULT_MATCH_TYPE: SheetFilterMatchType = 'contains'
|
||||
|
||||
/**
|
||||
* Parses a cell string as a finite number, or returns null when it is blank or
|
||||
* non-numeric so callers can fall back to lexicographic comparison.
|
||||
*/
|
||||
function asFiniteNumber(value: string): number | null {
|
||||
if (value.trim() === '') return null
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
/** Case-insensitive lexicographic comparison returning -1, 0, or 1. */
|
||||
function compareLexicographic(cell: string, target: string): number {
|
||||
return Math.sign(cell.toLowerCase().localeCompare(target.toLowerCase()))
|
||||
}
|
||||
|
||||
/** Evaluates a single cell against the filter target for the given match type. */
|
||||
function matchesCell(cell: string, target: string, matchType: SheetFilterMatchType): boolean {
|
||||
switch (matchType) {
|
||||
case 'gt':
|
||||
case 'gte':
|
||||
case 'lt':
|
||||
case 'lte': {
|
||||
const cellNum = asFiniteNumber(cell)
|
||||
const targetNum = asFiniteNumber(target)
|
||||
let cmp: number
|
||||
if (cellNum !== null && targetNum !== null) {
|
||||
cmp = Math.sign(cellNum - targetNum)
|
||||
} else if (cellNum === null && targetNum === null) {
|
||||
cmp = compareLexicographic(cell, target)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if (matchType === 'gt') return cmp > 0
|
||||
if (matchType === 'gte') return cmp >= 0
|
||||
if (matchType === 'lt') return cmp < 0
|
||||
return cmp <= 0
|
||||
}
|
||||
case 'exact':
|
||||
return cell.toLowerCase() === target.toLowerCase()
|
||||
case 'not_equals':
|
||||
return cell.toLowerCase() !== target.toLowerCase()
|
||||
case 'starts_with':
|
||||
return cell.toLowerCase().startsWith(target.toLowerCase())
|
||||
case 'ends_with':
|
||||
return cell.toLowerCase().endsWith(target.toLowerCase())
|
||||
case 'not_contains':
|
||||
return !cell.toLowerCase().includes(target.toLowerCase())
|
||||
default:
|
||||
return cell.toLowerCase().includes(target.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a 2D values array (header row + data rows) by matching a single column
|
||||
* against a target value. Returns the original values untouched when no filter
|
||||
* is requested, when there are no data rows, or when the column is not found —
|
||||
* the `applied`/`columnFound` flags let callers distinguish "no match possible"
|
||||
* from "everything matched".
|
||||
*/
|
||||
export function filterSheetRows(
|
||||
values: unknown[][],
|
||||
options: SheetFilterOptions
|
||||
): SheetFilterResult {
|
||||
const { filterColumn, filterValue, filterMatchType } = options
|
||||
const totalRows = Math.max(values.length - 1, 0)
|
||||
|
||||
if (!filterColumn || filterValue === undefined || filterValue === '') {
|
||||
return { values, applied: false, columnFound: true, matchedRows: totalRows, totalRows }
|
||||
}
|
||||
|
||||
const headers = values[0] ?? []
|
||||
const normalizedColumn = filterColumn.trim().toLowerCase()
|
||||
const columnIndex = headers.findIndex(
|
||||
(header) => String(header).trim().toLowerCase() === normalizedColumn
|
||||
)
|
||||
const columnFound = columnIndex !== -1
|
||||
|
||||
// No data rows to evaluate (empty or header-only sheet): nothing matched, but
|
||||
// still report whether the requested column actually exists in the header.
|
||||
if (values.length <= 1) {
|
||||
return { values, applied: false, columnFound, matchedRows: 0, totalRows: 0 }
|
||||
}
|
||||
|
||||
// Column not found: leave rows untouched and report zero matches, not totalRows.
|
||||
if (!columnFound) {
|
||||
return { values, applied: false, columnFound: false, matchedRows: 0, totalRows }
|
||||
}
|
||||
|
||||
const matchType = filterMatchType ?? DEFAULT_MATCH_TYPE
|
||||
const matched = values
|
||||
.slice(1)
|
||||
.filter((row) => matchesCell(String(row[columnIndex] ?? ''), filterValue, matchType))
|
||||
|
||||
return {
|
||||
values: [values[0], ...matched],
|
||||
applied: true,
|
||||
columnFound: true,
|
||||
matchedRows: matched.length,
|
||||
totalRows,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
GoogleSheetsV2GetSpreadsheetParams,
|
||||
GoogleSheetsV2GetSpreadsheetResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSpreadsheetV2Tool: ToolConfig<
|
||||
GoogleSheetsV2GetSpreadsheetParams,
|
||||
GoogleSheetsV2GetSpreadsheetResponse
|
||||
> = {
|
||||
id: 'google_sheets_get_spreadsheet_v2',
|
||||
name: 'Get Spreadsheet Info V2',
|
||||
description: 'Get metadata about a Google Sheets spreadsheet including title and sheet list',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
includeGridData: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include grid data (cell values). Defaults to false.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
const includeGridData = params.includeGridData ? 'true' : 'false'
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}?includeGridData=${includeGridData}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const sheets =
|
||||
data.sheets?.map((sheet: any) => ({
|
||||
sheetId: sheet.properties?.sheetId ?? 0,
|
||||
title: sheet.properties?.title ?? '',
|
||||
index: sheet.properties?.index ?? 0,
|
||||
rowCount: sheet.properties?.gridProperties?.rowCount ?? null,
|
||||
columnCount: sheet.properties?.gridProperties?.columnCount ?? null,
|
||||
hidden: sheet.properties?.hidden ?? false,
|
||||
})) ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
spreadsheetId: data.spreadsheetId ?? '',
|
||||
title: data.properties?.title ?? '',
|
||||
locale: data.properties?.locale ?? null,
|
||||
timeZone: data.properties?.timeZone ?? null,
|
||||
spreadsheetUrl: data.spreadsheetUrl ?? '',
|
||||
sheets,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
spreadsheetId: { type: 'string', description: 'The spreadsheet ID' },
|
||||
title: { type: 'string', description: 'The title of the spreadsheet' },
|
||||
locale: { type: 'string', description: 'The locale of the spreadsheet', optional: true },
|
||||
timeZone: { type: 'string', description: 'The time zone of the spreadsheet', optional: true },
|
||||
spreadsheetUrl: { type: 'string', description: 'URL to the spreadsheet' },
|
||||
sheets: {
|
||||
type: 'array',
|
||||
description: 'List of sheets in the spreadsheet',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sheetId: { type: 'number', description: 'The sheet ID' },
|
||||
title: { type: 'string', description: 'The sheet title/name' },
|
||||
index: { type: 'number', description: 'The sheet index (position)' },
|
||||
rowCount: { type: 'number', description: 'Number of rows in the sheet' },
|
||||
columnCount: { type: 'number', description: 'Number of columns in the sheet' },
|
||||
hidden: { type: 'boolean', description: 'Whether the sheet is hidden' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { appendTool, appendV2Tool } from '@/tools/google_sheets/append'
|
||||
import { batchClearV2Tool } from '@/tools/google_sheets/batch_clear'
|
||||
import { batchGetV2Tool } from '@/tools/google_sheets/batch_get'
|
||||
import { batchUpdateV2Tool } from '@/tools/google_sheets/batch_update'
|
||||
import { clearV2Tool } from '@/tools/google_sheets/clear'
|
||||
import { copySheetV2Tool } from '@/tools/google_sheets/copy_sheet'
|
||||
import { createSpreadsheetV2Tool } from '@/tools/google_sheets/create_spreadsheet'
|
||||
import { deleteRowsV2Tool } from '@/tools/google_sheets/delete_rows'
|
||||
import { deleteSheetV2Tool } from '@/tools/google_sheets/delete_sheet'
|
||||
import { deleteSpreadsheetV2Tool } from '@/tools/google_sheets/delete_spreadsheet'
|
||||
import { getSpreadsheetV2Tool } from '@/tools/google_sheets/get_spreadsheet'
|
||||
import { readTool, readV2Tool } from '@/tools/google_sheets/read'
|
||||
import { updateTool, updateV2Tool } from '@/tools/google_sheets/update'
|
||||
import { writeTool, writeV2Tool } from '@/tools/google_sheets/write'
|
||||
|
||||
// V1 exports
|
||||
export const googleSheetsReadTool = readTool
|
||||
export const googleSheetsWriteTool = writeTool
|
||||
export const googleSheetsUpdateTool = updateTool
|
||||
export const googleSheetsAppendTool = appendTool
|
||||
|
||||
// V2 exports
|
||||
export const googleSheetsReadV2Tool = readV2Tool
|
||||
export const googleSheetsWriteV2Tool = writeV2Tool
|
||||
export const googleSheetsUpdateV2Tool = updateV2Tool
|
||||
export const googleSheetsAppendV2Tool = appendV2Tool
|
||||
export const googleSheetsClearV2Tool = clearV2Tool
|
||||
export const googleSheetsGetSpreadsheetV2Tool = getSpreadsheetV2Tool
|
||||
export const googleSheetsCreateSpreadsheetV2Tool = createSpreadsheetV2Tool
|
||||
export const googleSheetsBatchGetV2Tool = batchGetV2Tool
|
||||
export const googleSheetsBatchUpdateV2Tool = batchUpdateV2Tool
|
||||
export const googleSheetsBatchClearV2Tool = batchClearV2Tool
|
||||
export const googleSheetsCopySheetV2Tool = copySheetV2Tool
|
||||
export const googleSheetsDeleteRowsV2Tool = deleteRowsV2Tool
|
||||
export const googleSheetsDeleteSheetV2Tool = deleteSheetV2Tool
|
||||
export const googleSheetsDeleteSpreadsheetV2Tool = deleteSpreadsheetV2Tool
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readV2Tool } from '@/tools/google_sheets/read'
|
||||
import type { GoogleSheetsV2ToolParams } from '@/tools/google_sheets/types'
|
||||
|
||||
const SPREADSHEET_ID = 'abc123'
|
||||
const URL = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/Sheet1!A1:Z1000`
|
||||
|
||||
const SHEET_DATA = {
|
||||
range: 'Sheet1!A1:D4',
|
||||
values: [
|
||||
['Name', 'Status'],
|
||||
['Alice', 'Active'],
|
||||
['Bob', 'Closed'],
|
||||
],
|
||||
}
|
||||
|
||||
function mockResponse(body: unknown, url = URL): Response {
|
||||
// double-cast-allowed: lightweight Response stub for transformResponse unit test
|
||||
return { url, json: async () => body } as unknown as Response
|
||||
}
|
||||
|
||||
const baseParams: GoogleSheetsV2ToolParams = {
|
||||
accessToken: 'token',
|
||||
spreadsheetId: SPREADSHEET_ID,
|
||||
sheetName: 'Sheet1',
|
||||
}
|
||||
|
||||
describe('readV2Tool.transformResponse', () => {
|
||||
it('returns values untouched and omits the filter field when no filter is requested', async () => {
|
||||
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), baseParams)
|
||||
|
||||
expect(result.output.values).toEqual(SHEET_DATA.values)
|
||||
expect('filter' in result.output).toBe(false)
|
||||
expect(result.output.range).toBe(SHEET_DATA.range)
|
||||
expect(result.output.metadata.spreadsheetId).toBe(SPREADSHEET_ID)
|
||||
})
|
||||
|
||||
it('omits the filter field when filterColumn is set but filterValue is empty', async () => {
|
||||
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
|
||||
...baseParams,
|
||||
filterColumn: 'Status',
|
||||
filterValue: '',
|
||||
})
|
||||
|
||||
expect('filter' in result.output).toBe(false)
|
||||
expect(result.output.values).toEqual(SHEET_DATA.values)
|
||||
})
|
||||
|
||||
it('filters rows and reports filter metadata when a filter is applied', async () => {
|
||||
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
|
||||
...baseParams,
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'Active',
|
||||
filterMatchType: 'exact',
|
||||
})
|
||||
|
||||
expect(result.output.values).toEqual([
|
||||
['Name', 'Status'],
|
||||
['Alice', 'Active'],
|
||||
])
|
||||
expect(result.output.filter).toEqual({
|
||||
applied: true,
|
||||
column: 'Status',
|
||||
matchType: 'exact',
|
||||
columnFound: true,
|
||||
matchedRows: 1,
|
||||
totalRows: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves values unchanged and reports columnFound=false when the column is missing', async () => {
|
||||
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
|
||||
...baseParams,
|
||||
filterColumn: 'Nonexistent',
|
||||
filterValue: 'x',
|
||||
})
|
||||
|
||||
expect(result.output.values).toEqual(SHEET_DATA.values)
|
||||
expect(result.output.filter?.columnFound).toBe(false)
|
||||
expect(result.output.filter?.applied).toBe(false)
|
||||
expect(result.output.filter?.matchedRows).toBe(0)
|
||||
})
|
||||
|
||||
it('handles a response with no values array', async () => {
|
||||
const result = await readV2Tool.transformResponse!(mockResponse({ range: 'Sheet1!A1' }), {
|
||||
...baseParams,
|
||||
filterColumn: 'Status',
|
||||
filterValue: 'Active',
|
||||
})
|
||||
|
||||
expect(result.output.values).toEqual([])
|
||||
expect(result.output.filter?.applied).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,272 @@
|
||||
import { filterSheetRows } from '@/tools/google_sheets/filter'
|
||||
import type {
|
||||
GoogleSheetsReadResponse,
|
||||
GoogleSheetsToolParams,
|
||||
GoogleSheetsV2ReadResponse,
|
||||
GoogleSheetsV2ToolParams,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadResponse> = {
|
||||
id: 'google_sheets_read',
|
||||
name: 'Read from Google Sheets',
|
||||
description: 'Read data from a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the spreadsheet (found in the URL: docs.google.com/spreadsheets/d/{SPREADSHEET_ID}/edit).',
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The A1 notation range to read (e.g. "Sheet1!A1:D10", "A1:B5"). Defaults to first sheet A1:Z1000 if not specified.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure spreadsheetId is valid
|
||||
const spreadsheetId = params.spreadsheetId?.trim()
|
||||
if (!spreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required')
|
||||
}
|
||||
|
||||
// If no range is provided, default to the first sheet without hardcoding the title
|
||||
// Using A1 notation without a sheet name targets the first sheet (per Sheets API)
|
||||
// Keep a generous column/row bound to avoid huge payloads
|
||||
if (!params.range) {
|
||||
const defaultRange = 'A1:Z1000'
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(defaultRange)}`
|
||||
}
|
||||
|
||||
// Otherwise, get values from the specified range
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(params.range)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL (guard if url is missing)
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
// Create a simple metadata object with just the ID and URL
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
properties: {},
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
// Process the values response
|
||||
const result: GoogleSheetsReadResponse = {
|
||||
success: true,
|
||||
output: {
|
||||
data: {
|
||||
range: data.range || '',
|
||||
values: data.values || [],
|
||||
},
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
outputs: {
|
||||
data: { type: 'json', description: 'Sheet data including range and cell values' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const readV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2ReadResponse> = {
|
||||
id: 'google_sheets_read_v2',
|
||||
name: 'Read from Google Sheets V2',
|
||||
description: 'Read data from a specific sheet in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the sheet/tab to read from',
|
||||
},
|
||||
cellRange: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The cell range to read (e.g. "A1:D10"). Defaults to "A1:Z1000" if not specified.',
|
||||
},
|
||||
filterColumn: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Column name (from the header row) to filter on. Filtering is applied to the rows returned by the read range (the default is A1:Z1000), not the entire sheet. If not provided, no filtering is applied.',
|
||||
},
|
||||
filterValue: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Value to match against the filter column.',
|
||||
},
|
||||
filterMatchType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'How to match the filter value. Text: "contains", "not_contains", "exact", "not_equals", "starts_with", "ends_with". Numeric/ordering: "gt", "gte", "lt", "lte" (numeric when both values are numbers, otherwise lexicographic). Defaults to "contains".',
|
||||
},
|
||||
},
|
||||
|
||||
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:Z1000'
|
||||
const fullRange = `${sheetName}!${cellRange}`
|
||||
|
||||
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(fullRange)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleSheetsV2ToolParams) => {
|
||||
const data = await response.json()
|
||||
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
const rawValues: unknown[][] = data.values ?? []
|
||||
|
||||
const filterRequested =
|
||||
Boolean(params?.filterColumn) &&
|
||||
params?.filterValue !== undefined &&
|
||||
params?.filterValue !== ''
|
||||
|
||||
const filterResult = filterSheetRows(rawValues, {
|
||||
filterColumn: params?.filterColumn,
|
||||
filterValue: params?.filterValue,
|
||||
filterMatchType: params?.filterMatchType,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
sheetName: params?.sheetName ?? '',
|
||||
range: data.range ?? '',
|
||||
values: filterResult.values,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
...(filterRequested
|
||||
? {
|
||||
filter: {
|
||||
applied: filterResult.applied,
|
||||
column: params?.filterColumn ?? '',
|
||||
matchType: params?.filterMatchType ?? 'contains',
|
||||
columnFound: filterResult.columnFound,
|
||||
matchedRows: filterResult.matchedRows,
|
||||
totalRows: filterResult.totalRows,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
sheetName: { type: 'string', description: 'Name of the sheet that was read' },
|
||||
range: { type: 'string', description: 'The range of cells that was read' },
|
||||
values: { type: 'array', description: 'The cell values as a 2D array' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
properties: {
|
||||
spreadsheetId: { type: 'string', description: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import type { GoogleSheetsV2DeleteRowsResponse } from '@/tools/google_sheets/delete_rows'
|
||||
import type { GoogleSheetsV2DeleteSheetResponse } from '@/tools/google_sheets/delete_sheet'
|
||||
import type { GoogleSheetsV2DeleteSpreadsheetResponse } from '@/tools/google_sheets/delete_spreadsheet'
|
||||
import type { SheetFilterMatchType } from '@/tools/google_sheets/filter'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleSheetsRange {
|
||||
sheetId?: number
|
||||
sheetName?: string
|
||||
range: string
|
||||
values: any[][]
|
||||
}
|
||||
|
||||
interface GoogleSheetsMetadata {
|
||||
spreadsheetId: string
|
||||
spreadsheetUrl?: string
|
||||
title?: string
|
||||
sheets?: {
|
||||
sheetId: number
|
||||
title: string
|
||||
index: number
|
||||
rowCount?: number
|
||||
columnCount?: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface GoogleSheetsReadResponse extends ToolResponse {
|
||||
output: {
|
||||
data: GoogleSheetsRange
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsWriteResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsAppendResponse extends ToolResponse {
|
||||
output: {
|
||||
tableRange: string
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsToolParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
range?: string
|
||||
values?: any[][]
|
||||
valueInputOption?: 'RAW' | 'USER_ENTERED'
|
||||
insertDataOption?: 'OVERWRITE' | 'INSERT_ROWS'
|
||||
includeValuesInResponse?: boolean
|
||||
responseValueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA'
|
||||
majorDimension?: 'ROWS' | 'COLUMNS'
|
||||
}
|
||||
|
||||
export type GoogleSheetsResponse =
|
||||
| GoogleSheetsReadResponse
|
||||
| GoogleSheetsWriteResponse
|
||||
| GoogleSheetsUpdateResponse
|
||||
| GoogleSheetsAppendResponse
|
||||
|
||||
// V2 Types - with explicit sheetName parameter
|
||||
|
||||
export interface GoogleSheetsFilterInfo {
|
||||
/** Whether row filtering was actually applied to the data rows. */
|
||||
applied: boolean
|
||||
/** The column header the filter targeted. */
|
||||
column: string
|
||||
/** The match type used for the comparison. */
|
||||
matchType: SheetFilterMatchType
|
||||
/** Whether the requested filter column was found in the header row. */
|
||||
columnFound: boolean
|
||||
/** Number of data rows (excluding the header) that matched the filter. */
|
||||
matchedRows: number
|
||||
/** Total number of data rows (excluding the header) that were considered. */
|
||||
totalRows: number
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2ReadResponse extends ToolResponse {
|
||||
output: {
|
||||
sheetName: string
|
||||
range: string
|
||||
values: any[][]
|
||||
metadata: GoogleSheetsMetadata
|
||||
filter?: GoogleSheetsFilterInfo
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2WriteResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2UpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2AppendResponse extends ToolResponse {
|
||||
output: {
|
||||
tableRange: string
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2ToolParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
sheetName: string
|
||||
cellRange?: string
|
||||
values?: any[][]
|
||||
valueInputOption?: 'RAW' | 'USER_ENTERED'
|
||||
insertDataOption?: 'OVERWRITE' | 'INSERT_ROWS'
|
||||
includeValuesInResponse?: boolean
|
||||
responseValueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA'
|
||||
majorDimension?: 'ROWS' | 'COLUMNS'
|
||||
filterColumn?: string
|
||||
filterValue?: string
|
||||
filterMatchType?: SheetFilterMatchType
|
||||
}
|
||||
|
||||
export type GoogleSheetsV2Response =
|
||||
| GoogleSheetsV2ReadResponse
|
||||
| GoogleSheetsV2WriteResponse
|
||||
| GoogleSheetsV2UpdateResponse
|
||||
| GoogleSheetsV2AppendResponse
|
||||
| GoogleSheetsV2ClearResponse
|
||||
| GoogleSheetsV2GetSpreadsheetResponse
|
||||
| GoogleSheetsV2CreateSpreadsheetResponse
|
||||
| GoogleSheetsV2BatchGetResponse
|
||||
| GoogleSheetsV2BatchUpdateResponse
|
||||
| GoogleSheetsV2BatchClearResponse
|
||||
| GoogleSheetsV2CopySheetResponse
|
||||
| GoogleSheetsV2DeleteRowsResponse
|
||||
| GoogleSheetsV2DeleteSheetResponse
|
||||
| GoogleSheetsV2DeleteSpreadsheetResponse
|
||||
|
||||
// V2 Clear Types
|
||||
export interface GoogleSheetsV2ClearParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
sheetName: string
|
||||
cellRange?: string
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2ClearResponse extends ToolResponse {
|
||||
output: {
|
||||
clearedRange: string
|
||||
sheetName: string
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Get Spreadsheet Types
|
||||
export interface GoogleSheetsV2GetSpreadsheetParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
includeGridData?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2GetSpreadsheetResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
title: string
|
||||
locale: string | null
|
||||
timeZone: string | null
|
||||
spreadsheetUrl: string
|
||||
sheets: {
|
||||
sheetId: number
|
||||
title: string
|
||||
index: number
|
||||
rowCount: number | null
|
||||
columnCount: number | null
|
||||
hidden: boolean
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Create Spreadsheet Types
|
||||
export interface GoogleSheetsV2CreateSpreadsheetParams {
|
||||
accessToken: string
|
||||
title: string
|
||||
sheetTitles?: string[]
|
||||
locale?: string
|
||||
timeZone?: string
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2CreateSpreadsheetResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
title: string
|
||||
spreadsheetUrl: string
|
||||
sheets: {
|
||||
sheetId: number
|
||||
title: string
|
||||
index: number
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Batch Get Types
|
||||
export interface GoogleSheetsV2BatchGetParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
ranges: string[]
|
||||
majorDimension?: 'ROWS' | 'COLUMNS'
|
||||
valueRenderOption?: 'FORMATTED_VALUE' | 'UNFORMATTED_VALUE' | 'FORMULA'
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2BatchGetResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
valueRanges: {
|
||||
range: string
|
||||
majorDimension: string
|
||||
values: any[][]
|
||||
}[]
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Batch Update Types
|
||||
export interface GoogleSheetsV2BatchUpdateParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
data: {
|
||||
range: string
|
||||
values: any[][]
|
||||
}[]
|
||||
valueInputOption?: 'RAW' | 'USER_ENTERED'
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2BatchUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
totalUpdatedRows: number
|
||||
totalUpdatedColumns: number
|
||||
totalUpdatedCells: number
|
||||
totalUpdatedSheets: number
|
||||
responses: {
|
||||
spreadsheetId: string
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
}[]
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Batch Clear Types
|
||||
export interface GoogleSheetsV2BatchClearParams {
|
||||
accessToken: string
|
||||
spreadsheetId: string
|
||||
ranges: string[]
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2BatchClearResponse extends ToolResponse {
|
||||
output: {
|
||||
spreadsheetId: string
|
||||
clearedRanges: string[]
|
||||
metadata: GoogleSheetsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// V2 Copy Sheet Types
|
||||
export interface GoogleSheetsV2CopySheetParams {
|
||||
accessToken: string
|
||||
sourceSpreadsheetId: string
|
||||
sheetId: number
|
||||
destinationSpreadsheetId: string
|
||||
}
|
||||
|
||||
export interface GoogleSheetsV2CopySheetResponse extends ToolResponse {
|
||||
output: {
|
||||
sheetId: number
|
||||
title: string
|
||||
index: number
|
||||
sheetType: string
|
||||
destinationSpreadsheetId: string
|
||||
destinationSpreadsheetUrl: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import type {
|
||||
GoogleSheetsToolParams,
|
||||
GoogleSheetsUpdateResponse,
|
||||
GoogleSheetsV2ToolParams,
|
||||
GoogleSheetsV2UpdateResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateResponse> = {
|
||||
id: 'google_sheets_update',
|
||||
name: 'Update Google Sheets',
|
||||
description: 'Update data in a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the spreadsheet to update',
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The A1 notation range to update (e.g. "Sheet1!A1:D10", "A1:B5")',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The data to update as a 2D array (e.g. [["Name", "Age"], ["Alice", 30]]) or array of objects.',
|
||||
},
|
||||
valueInputOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The format of the data to update',
|
||||
},
|
||||
includeValuesInResponse: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Whether to include the updated values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// If range is not provided, use a default range for the first sheet, second row to preserve headers
|
||||
const range = params.range || 'Sheet1!A2'
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}`
|
||||
)
|
||||
|
||||
// Default to USER_ENTERED if not specified
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let processedValues: any = params.values || []
|
||||
|
||||
// Minimal shape enforcement: Google requires a 2D array
|
||||
if (!Array.isArray(processedValues)) {
|
||||
processedValues = [[processedValues]]
|
||||
} else if (!processedValues.every((item: any) => Array.isArray(item))) {
|
||||
processedValues = (processedValues as any[]).map((row: any) =>
|
||||
Array.isArray(row) ? row : [row]
|
||||
)
|
||||
}
|
||||
|
||||
// Handle array of objects (existing behavior)
|
||||
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: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
if (params.range) {
|
||||
body.range = params.range
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL (guard if url is missing)
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
// Create a simple metadata object with just the ID and URL
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
properties: {},
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
output: {
|
||||
updatedRange: data.updatedRange,
|
||||
updatedRows: data.updatedRows,
|
||||
updatedColumns: data.updatedColumns,
|
||||
updatedCells: data.updatedCells,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const updateV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2UpdateResponse> = {
|
||||
id: 'google_sheets_update_v2',
|
||||
name: 'Update Google Sheets V2',
|
||||
description: 'Update data in a specific sheet in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the sheet/tab to update',
|
||||
},
|
||||
cellRange: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The cell range to update (e.g. "A1:D10", "A1"). Defaults to "A1" if not specified.',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The data to update as a 2D array (e.g. [["Name", "Age"], ["Alice", 30]]) or array of objects.',
|
||||
},
|
||||
valueInputOption: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The format of the data to update',
|
||||
},
|
||||
includeValuesInResponse: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Whether to include the updated values in the response',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const sheetName = params.sheetName?.trim()
|
||||
if (!sheetName) {
|
||||
throw new Error('Sheet name is required')
|
||||
}
|
||||
|
||||
const cellRange = params.cellRange?.trim() || 'A1'
|
||||
const fullRange = `${sheetName}!${cellRange}`
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(fullRange)}`
|
||||
)
|
||||
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let processedValues: any = params.values || []
|
||||
|
||||
// Minimal shape enforcement: Google requires a 2D array
|
||||
if (!Array.isArray(processedValues)) {
|
||||
processedValues = [[processedValues]]
|
||||
} else if (!processedValues.every((item: any) => Array.isArray(item))) {
|
||||
processedValues = (processedValues as any[]).map((row: any) =>
|
||||
Array.isArray(row) ? row : [row]
|
||||
)
|
||||
}
|
||||
|
||||
// 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: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedRange: data.updatedRange ?? null,
|
||||
updatedRows: data.updatedRows ?? 0,
|
||||
updatedColumns: data.updatedColumns ?? 0,
|
||||
updatedCells: data.updatedCells ?? 0,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import type {
|
||||
GoogleSheetsToolParams,
|
||||
GoogleSheetsV2ToolParams,
|
||||
GoogleSheetsV2WriteResponse,
|
||||
GoogleSheetsWriteResponse,
|
||||
} from '@/tools/google_sheets/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResponse> = {
|
||||
id: 'google_sheets_write',
|
||||
name: 'Write to Google Sheets',
|
||||
description: 'Write data to a Google Sheets spreadsheet',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the spreadsheet',
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The A1 notation range to write to (e.g. "Sheet1!A1:D10", "A1:B5")',
|
||||
},
|
||||
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) => {
|
||||
// If range is not provided, use a default range for the first sheet, second row to preserve headers
|
||||
const range = params.range || 'Sheet1!A2'
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}`
|
||||
)
|
||||
|
||||
// Default to USER_ENTERED if not specified
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PUT',
|
||||
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])
|
||||
) {
|
||||
// It's an array of objects
|
||||
|
||||
// First, extract all unique keys from all objects to create headers
|
||||
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)
|
||||
|
||||
// Then create rows with object values in the order of headers
|
||||
const rows = processedValues.map((obj: any) => {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
// Handle non-object items by creating an array with empty values
|
||||
return Array(headers.length).fill('')
|
||||
}
|
||||
return headers.map((key) => {
|
||||
const value = obj[key]
|
||||
// Handle nested objects/arrays by converting to JSON string
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return value === undefined ? '' : value
|
||||
})
|
||||
})
|
||||
|
||||
// Add headers as the first row, then add data rows
|
||||
processedValues = [headers, ...rows]
|
||||
}
|
||||
|
||||
const body: Record<string, any> = {
|
||||
majorDimension: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
|
||||
// Only include range if it's provided
|
||||
if (params.range) {
|
||||
body.range = params.range
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract spreadsheet ID from the URL (guard if url is missing)
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
// Create a simple metadata object with just the ID and URL
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
properties: {},
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
output: {
|
||||
updatedRange: data.updatedRange,
|
||||
updatedRows: data.updatedRows,
|
||||
updatedColumns: data.updatedColumns,
|
||||
updatedCells: data.updatedCells,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const writeV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2WriteResponse> = {
|
||||
id: 'google_sheets_write_v2',
|
||||
name: 'Write to Google Sheets V2',
|
||||
description: 'Write data to a specific sheet in a Google Sheets spreadsheet',
|
||||
version: '2.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-sheets',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Sheets API',
|
||||
},
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Sheets spreadsheet ID',
|
||||
},
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the sheet/tab to write to',
|
||||
},
|
||||
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 sheetName = params.sheetName?.trim()
|
||||
if (!sheetName) {
|
||||
throw new Error('Sheet name is required')
|
||||
}
|
||||
|
||||
// Build the range: SheetName!CellRange or just SheetName!A1
|
||||
const cellRange = params.cellRange?.trim() || 'A1'
|
||||
const fullRange = `${sheetName}!${cellRange}`
|
||||
|
||||
const url = new URL(
|
||||
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(fullRange)}`
|
||||
)
|
||||
|
||||
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
|
||||
url.searchParams.append('valueInputOption', valueInputOption)
|
||||
|
||||
if (params.includeValuesInResponse) {
|
||||
url.searchParams.append('includeValuesInResponse', 'true')
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PUT',
|
||||
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: params.majorDimension || 'ROWS',
|
||||
values: processedValues,
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
const urlParts = typeof response.url === 'string' ? response.url.split('/spreadsheets/') : []
|
||||
const spreadsheetId = urlParts[1]?.split('/')[0] || ''
|
||||
|
||||
const metadata = {
|
||||
spreadsheetId,
|
||||
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedRange: data.updatedRange ?? null,
|
||||
updatedRows: data.updatedRows ?? 0,
|
||||
updatedColumns: data.updatedColumns ?? 0,
|
||||
updatedCells: data.updatedCells ?? 0,
|
||||
metadata: {
|
||||
spreadsheetId: metadata.spreadsheetId,
|
||||
spreadsheetUrl: metadata.spreadsheetUrl,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
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: 'Google Sheets spreadsheet ID' },
|
||||
spreadsheetUrl: { type: 'string', description: 'Spreadsheet URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user