chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
import { createLogger } from '@sim/logger'
import { generateRandomString } from '@sim/utils/random'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesAddImageTool')
interface AddImageParams {
accessToken: string
presentationId: string
pageObjectId: string
imageUrl: string
width?: number
height?: number
positionX?: number
positionY?: number
}
interface AddImageResponse {
success: boolean
output: {
imageId: string
metadata: {
presentationId: string
pageObjectId: string
imageUrl: string
url: string
}
}
}
// EMU (English Metric Units) conversion: 1 inch = 914400 EMU, 1 pt = 12700 EMU
const PT_TO_EMU = 12700
export const addImageTool: ToolConfig<AddImageParams, AddImageResponse> = {
id: 'google_slides_add_image',
name: 'Add Image to Google Slides',
description: 'Insert an image into a specific slide in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the slide/page to add the image to',
},
imageUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The publicly accessible URL of the image (must be PNG, JPEG, or GIF, max 50MB)',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Width of the image in points (default: 300)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Height of the image in points (default: 200)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position from the left edge in points (default: 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position from the top edge in points (default: 100)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 pageObjectId = params.pageObjectId?.trim()
const imageUrl = params.imageUrl?.trim()
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
if (!imageUrl) {
throw new Error('Image URL is required')
}
// Generate a unique object ID for the new image
const imageObjectId = `image_${Date.now()}_${generateRandomString(7)}`
// Convert points to EMU (default sizes if not specified)
const widthEmu = (params.width || 300) * PT_TO_EMU
const heightEmu = (params.height || 200) * PT_TO_EMU
const translateX = (params.positionX || 100) * PT_TO_EMU
const translateY = (params.positionY || 100) * PT_TO_EMU
return {
requests: [
{
createImage: {
objectId: imageObjectId,
url: imageUrl,
elementProperties: {
pageObjectId: pageObjectId,
size: {
width: {
magnitude: widthEmu,
unit: 'EMU',
},
height: {
magnitude: heightEmu,
unit: 'EMU',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: translateX,
translateY: translateY,
unit: 'EMU',
},
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to add image')
}
// The response contains the created image's object ID
const createImageReply = data.replies?.[0]?.createImage
const imageId = createImageReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
imageId,
metadata: {
presentationId,
pageObjectId,
imageUrl: params?.imageUrl?.trim() || '',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
imageId: {
type: 'string',
description: 'The object ID of the newly created image',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and image URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
pageObjectId: {
type: 'string',
description: 'The page object ID where the image was inserted',
},
imageUrl: {
type: 'string',
description: 'The source image URL',
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
}
+211
View File
@@ -0,0 +1,211 @@
import { createLogger } from '@sim/logger'
import { generateRandomString } from '@sim/utils/random'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesAddSlideTool')
interface AddSlideParams {
accessToken: string
presentationId: string
layout?: string
insertionIndex?: number
placeholderIdMappings?: string
}
interface AddSlideResponse {
success: boolean
output: {
slideId: string
metadata: {
presentationId: string
layout: string
insertionIndex?: number
url: string
}
}
}
// Predefined layouts available in Google Slides API
const PREDEFINED_LAYOUTS = [
'BLANK',
'CAPTION_ONLY',
'TITLE',
'TITLE_AND_BODY',
'TITLE_AND_TWO_COLUMNS',
'TITLE_ONLY',
'SECTION_HEADER',
'SECTION_TITLE_AND_DESCRIPTION',
'ONE_COLUMN_TEXT',
'MAIN_POINT',
'BIG_NUMBER',
] as const
export const addSlideTool: ToolConfig<AddSlideParams, AddSlideResponse> = {
id: 'google_slides_add_slide',
name: 'Add Slide to Google Slides',
description: 'Add a new slide to a Google Slides presentation with a specified layout',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
layout: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The predefined layout for the slide (BLANK, TITLE, TITLE_AND_BODY, TITLE_ONLY, SECTION_HEADER, etc.). Defaults to BLANK.',
},
insertionIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'The optional zero-based index indicating where to insert the slide. If not specified, the slide is added at the end.',
},
placeholderIdMappings: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of placeholder mappings to assign custom object IDs to placeholders. Format: [{"layoutPlaceholder":{"type":"TITLE"},"objectId":"custom_title_id"}]',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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) => {
// Generate a unique object ID for the new slide
const slideObjectId = `slide_${Date.now()}_${generateRandomString(7)}`
// Validate and normalize the layout
let layout = (params.layout || 'BLANK').toUpperCase()
if (!PREDEFINED_LAYOUTS.includes(layout as (typeof PREDEFINED_LAYOUTS)[number])) {
logger.warn(`Invalid layout "${params.layout}", defaulting to BLANK`)
layout = 'BLANK'
}
const createSlideRequest: Record<string, any> = {
objectId: slideObjectId,
slideLayoutReference: {
predefinedLayout: layout,
},
}
// Add insertion index if specified
if (params.insertionIndex !== undefined && params.insertionIndex >= 0) {
createSlideRequest.insertionIndex = params.insertionIndex
}
// Add placeholder ID mappings if specified (for advanced use cases)
if (params.placeholderIdMappings?.trim()) {
try {
const mappings = JSON.parse(params.placeholderIdMappings)
if (Array.isArray(mappings) && mappings.length > 0) {
createSlideRequest.placeholderIdMappings = mappings
}
} catch (e) {
logger.warn('Invalid placeholderIdMappings JSON, ignoring:', e)
}
}
return {
requests: [
{
createSlide: createSlideRequest,
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to add slide')
}
// The response contains the created slide's object ID
const createSlideReply = data.replies?.[0]?.createSlide
const slideId = createSlideReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const layout = (params?.layout || 'BLANK').toUpperCase()
return {
success: true,
output: {
slideId,
metadata: {
presentationId,
layout,
insertionIndex: params?.insertionIndex,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
slideId: {
type: 'string',
description: 'The object ID of the newly created slide',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID, layout, and URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
layout: {
type: 'string',
description: 'The layout used for the new slide',
},
insertionIndex: {
type: 'number',
description: 'The zero-based index where the slide was inserted',
optional: true,
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
}
@@ -0,0 +1,139 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesBatchUpdateTool')
interface BatchUpdateParams {
accessToken: string
presentationId: string
requests: string
writeControl?: string
}
interface BatchUpdateResponse {
success: boolean
output: {
replies: unknown[]
writeControl: unknown
metadata: { presentationId: string; url: string; requestCount: number }
}
}
export const batchUpdateTool: ToolConfig<BatchUpdateParams, BatchUpdateResponse> = {
id: 'google_slides_batch_update',
name: 'Batch Update Google Slides (Raw)',
description:
'Run a raw Slides API batchUpdate with a list of Request objects. Use this when the higher-level tools do not cover an operation, or to bundle multiple operations into a single atomic batch (all-or-nothing).',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
requests: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of Slides API Request objects. Example: [{"replaceAllText":{"containsText":{"text":"{{title}}"},"replaceText":"Q3 Review"}}, {"updatePageProperties":{"objectId":"slide_1","pageProperties":{"pageBackgroundFill":{"solidFill":{"color":{"rgbColor":{"red":0.043,"green":0.122,"blue":0.231}}}}},"fields":"pageBackgroundFill"}}]',
},
writeControl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional JSON WriteControl object for optimistic concurrency, e.g. {"requiredRevisionId":"..."}',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const raw = params.requests
if (!raw) throw new Error('Requests JSON is required')
let requests: unknown
try {
requests = typeof raw === 'string' ? JSON.parse(raw) : raw
} catch (e) {
throw new Error(`Invalid requests JSON: ${(e as Error).message}`)
}
if (!Array.isArray(requests)) {
throw new Error('Requests must be a JSON array of Request objects')
}
if (requests.length === 0) {
throw new Error('Requests array must contain at least one Request')
}
const body: Record<string, unknown> = { requests }
if (params.writeControl?.trim()) {
try {
const wc = JSON.parse(params.writeControl)
if (wc && typeof wc === 'object') body.writeControl = wc
} catch (e) {
logger.warn('Invalid writeControl JSON, ignoring:', { error: e })
}
}
return body
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Batch update failed')
}
const presentationId = params?.presentationId?.trim() || ''
const replies: unknown[] = Array.isArray(data.replies) ? data.replies : []
return {
success: true,
output: {
replies,
writeControl: data.writeControl ?? null,
metadata: {
presentationId,
url: presentationUrl(presentationId),
requestCount: replies.length,
},
},
}
},
outputs: {
replies: {
type: 'array',
description: 'Array of reply objects, one per request (parallel-indexed)',
items: { type: 'json' },
},
writeControl: {
type: 'json',
description: 'WriteControl returned by the server (revision tracking)',
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
requestCount: { type: 'number', description: 'Number of replies returned' },
},
},
},
}
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import { presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCopyPresentationTool')
interface CopyPresentationParams {
accessToken: string
sourcePresentationId: string
title?: string
folderId?: string
}
interface CopyPresentationResponse {
success: boolean
output: {
presentationId: string
title: string
metadata: {
sourcePresentationId: string
presentationId: string
title: string
mimeType: string
url: string
}
}
}
const PRESENTATION_MIME = 'application/vnd.google-apps.presentation'
export const copyPresentationTool: ToolConfig<CopyPresentationParams, CopyPresentationResponse> = {
id: 'google_slides_copy_presentation',
name: 'Copy Google Slides Presentation',
description:
'Copy a template presentation in Drive to a new file. Use this before merging data so the original template is never modified.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides / Drive API',
},
sourcePresentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Drive file ID of the source/template presentation',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Title for the copy. Defaults to "Copy of <source title>".',
},
folderId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Drive folder ID where the copy should be placed',
},
},
request: {
url: (params) => {
const sourceId = params.sourcePresentationId?.trim()
if (!sourceId) throw new Error('Source presentation ID is required')
return `https://www.googleapis.com/drive/v3/files/${sourceId}/copy?supportsAllDrives=true`
},
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 body: Record<string, unknown> = {}
if (params.title?.trim()) body.name = params.title.trim()
if (params.folderId?.trim()) body.parents = [params.folderId.trim()]
return body
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Drive API error during copy:', { data })
throw new Error(data.error?.message || 'Failed to copy presentation')
}
const presentationId: string = data.id
const title: string = data.name || 'Untitled Presentation'
return {
success: true,
output: {
presentationId,
title,
metadata: {
sourcePresentationId: params?.sourcePresentationId?.trim() || '',
presentationId,
title,
mimeType: PRESENTATION_MIME,
url: presentationUrl(presentationId),
},
},
}
},
outputs: {
presentationId: { type: 'string', description: 'ID of the new copied presentation' },
title: { type: 'string', description: 'Title of the new presentation' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
sourcePresentationId: { type: 'string', description: 'Source/template presentation ID' },
presentationId: { type: 'string', description: 'New presentation ID' },
title: { type: 'string', description: 'New presentation title' },
mimeType: { type: 'string', description: 'MIME type of the presentation' },
url: { type: 'string', description: 'URL to the new presentation' },
},
},
},
}
+176
View File
@@ -0,0 +1,176 @@
import { createLogger } from '@sim/logger'
import type {
GoogleSlidesCreateResponse,
GoogleSlidesToolParams,
} from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateTool')
export const createTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesCreateResponse> = {
id: 'google_slides_create',
name: 'Create Google Slides Presentation',
description: 'Create a new Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the presentation to create',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The content to add to the first slide',
},
folderSelector: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Drive folder ID to create the presentation in (e.g., 1ABCxyz...)',
},
folderId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID of the folder to create the presentation in (internal use)',
},
},
request: {
url: () => {
return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
},
method: 'POST',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.title) {
throw new Error('Title is required')
}
const requestBody: any = {
name: params.title,
mimeType: 'application/vnd.google-apps.presentation',
}
// Add parent folder if specified (prefer folderSelector over folderId)
const folderId = params.folderSelector || params.folderId
if (folderId) {
requestBody.parents = [folderId]
}
return requestBody
},
},
postProcess: async (result, params, executeTool) => {
if (!result.success) {
return result
}
const presentationId = result.output.metadata.presentationId
if (params.content && presentationId) {
try {
const writeParams = {
accessToken: params.accessToken,
presentationId: presentationId,
content: params.content,
}
const writeResult = await executeTool('google_slides_write', writeParams)
if (!writeResult.success) {
logger.warn(
'Failed to add content to presentation, but presentation was created:',
writeResult.error
)
}
} catch (error) {
logger.warn('Error adding content to presentation:', { error })
// Don't fail the overall operation if adding content fails
}
}
return result
},
transformResponse: async (response: Response) => {
try {
// Get the response data
const responseText = await response.text()
const data = JSON.parse(responseText)
const presentationId = data.id
const title = data.name
const metadata = {
presentationId,
title: title || 'Untitled Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
}
return {
success: true,
output: {
metadata,
},
}
} catch (error) {
logger.error('Google Slides create - Error processing response:', {
error,
})
throw error
}
},
outputs: {
metadata: {
type: 'json',
description: 'Created presentation metadata including ID, title, and URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
title: {
type: 'string',
description: 'The presentation title',
},
mimeType: {
type: 'string',
description: 'The mime type of the presentation',
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
}
+157
View File
@@ -0,0 +1,157 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildElementProperties,
generateObjectId,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateLineTool')
interface CreateLineParams {
accessToken: string
presentationId: string
pageObjectId: string
lineCategory?: 'STRAIGHT' | 'BENT' | 'CURVED'
width?: number
height?: number
positionX?: number
positionY?: number
}
interface CreateLineResponse {
success: boolean
output: {
lineId: string
lineCategory: string
metadata: { presentationId: string; pageObjectId: string; url: string }
}
}
export const createLineTool: ToolConfig<CreateLineParams, CreateLineResponse> = {
id: 'google_slides_create_line',
name: 'Create Line in Google Slides',
description: 'Create a line or connector on a slide.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the slide to add the line to',
},
lineCategory: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'STRAIGHT (default), BENT, or CURVED',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Line width in points (default 200)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Line height in points (default 0 — horizontal line)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position in points (default 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position in points (default 100)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const pageObjectId = params.pageObjectId?.trim()
if (!pageObjectId) throw new Error('Page Object ID is required')
const objectId = generateObjectId('line')
const elementProperties = buildElementProperties({
pageObjectId,
width: params.width,
height: params.height ?? 1,
positionX: params.positionX,
positionY: params.positionY,
defaultWidth: 200,
defaultHeight: 1,
})
return {
requests: [
{
createLine: {
objectId,
lineCategory: params.lineCategory || 'STRAIGHT',
elementProperties,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create line')
}
const lineId = data.replies?.[0]?.createLine?.objectId ?? ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
lineId,
lineCategory: params?.lineCategory || 'STRAIGHT',
metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
lineId: { type: 'string', description: 'Object ID of the new line' },
lineCategory: { type: 'string', description: 'Line category created' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
pageObjectId: { type: 'string', description: 'The slide ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,160 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildCellLocation,
buildTextRange,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateParagraphBulletsTool')
interface CreateParagraphBulletsParams {
accessToken: string
presentationId: string
objectId: string
rowIndex?: number
columnIndex?: number
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
bulletPreset?: string
}
interface CreateParagraphBulletsResponse {
success: boolean
output: {
created: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const createParagraphBulletsTool: ToolConfig<
CreateParagraphBulletsParams,
CreateParagraphBulletsResponse
> = {
id: 'google_slides_create_paragraph_bullets',
name: 'Create Paragraph Bullets in Google Slides',
description:
'Convert paragraphs in a shape or table cell into a bulleted or numbered list using a Google Slides bullet preset.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape or table containing the text',
},
rowIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based row index',
},
columnIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based column index',
},
rangeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Range to apply bullets to: ALL (default), FROM_START_INDEX, or FIXED_RANGE',
},
startIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start index for FROM_START_INDEX or FIXED_RANGE',
},
endIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End index for FIXED_RANGE',
},
bulletPreset: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Bullet preset (e.g. BULLET_DISC_CIRCLE_SQUARE, BULLET_ARROW_DIAMOND_DISC, NUMBERED_DIGIT_ALPHA_ROMAN, NUMBERED_DIGIT_ALPHA_ROMAN_PARENS, NUMBERED_DIGIT_NESTED). Defaults to BULLET_DISC_CIRCLE_SQUARE.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const createRequest: Record<string, unknown> = {
objectId,
textRange: buildTextRange({
rangeType: params.rangeType,
startIndex: params.startIndex,
endIndex: params.endIndex,
}),
bulletPreset: params.bulletPreset?.trim() || 'BULLET_DISC_CIRCLE_SQUARE',
}
const cellLocation = buildCellLocation({
rowIndex: params.rowIndex,
columnIndex: params.columnIndex,
})
if (cellLocation) createRequest.cellLocation = cellLocation
return { requests: [{ createParagraphBullets: createRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create paragraph bullets')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
created: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
created: { type: 'boolean', description: 'Whether bullets were created' },
objectId: { type: 'string', description: 'The object where bullets were created' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,367 @@
import { createLogger } from '@sim/logger'
import { generateRandomString } from '@sim/utils/random'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateShapeTool')
interface CreateShapeParams {
accessToken: string
presentationId: string
pageObjectId: string
shapeType: string
width?: number
height?: number
positionX?: number
positionY?: number
}
interface CreateShapeResponse {
success: boolean
output: {
shapeId: string
shapeType: string
metadata: {
presentationId: string
pageObjectId: string
url: string
}
}
}
// EMU (English Metric Units) conversion: 1 pt = 12700 EMU
const PT_TO_EMU = 12700
// Common shape types available in Google Slides API
const SHAPE_TYPES = [
'TEXT_BOX',
'RECTANGLE',
'ROUND_RECTANGLE',
'ELLIPSE',
'ARC',
'BENT_ARROW',
'BENT_UP_ARROW',
'BEVEL',
'BLOCK_ARC',
'BRACE_PAIR',
'BRACKET_PAIR',
'CAN',
'CHEVRON',
'CHORD',
'CLOUD',
'CORNER',
'CUBE',
'CURVED_DOWN_ARROW',
'CURVED_LEFT_ARROW',
'CURVED_RIGHT_ARROW',
'CURVED_UP_ARROW',
'DECAGON',
'DIAGONAL_STRIPE',
'DIAMOND',
'DODECAGON',
'DONUT',
'DOUBLE_WAVE',
'DOWN_ARROW',
'DOWN_ARROW_CALLOUT',
'FOLDED_CORNER',
'FRAME',
'HALF_FRAME',
'HEART',
'HEPTAGON',
'HEXAGON',
'HOME_PLATE',
'HORIZONTAL_SCROLL',
'IRREGULAR_SEAL_1',
'IRREGULAR_SEAL_2',
'LEFT_ARROW',
'LEFT_ARROW_CALLOUT',
'LEFT_BRACE',
'LEFT_BRACKET',
'LEFT_RIGHT_ARROW',
'LEFT_RIGHT_ARROW_CALLOUT',
'LEFT_RIGHT_UP_ARROW',
'LEFT_UP_ARROW',
'LIGHTNING_BOLT',
'MATH_DIVIDE',
'MATH_EQUAL',
'MATH_MINUS',
'MATH_MULTIPLY',
'MATH_NOT_EQUAL',
'MATH_PLUS',
'MOON',
'NO_SMOKING',
'NOTCHED_RIGHT_ARROW',
'OCTAGON',
'PARALLELOGRAM',
'PENTAGON',
'PIE',
'PLAQUE',
'PLUS',
'QUAD_ARROW',
'QUAD_ARROW_CALLOUT',
'RIBBON',
'RIBBON_2',
'RIGHT_ARROW',
'RIGHT_ARROW_CALLOUT',
'RIGHT_BRACE',
'RIGHT_BRACKET',
'ROUND_1_RECTANGLE',
'ROUND_2_DIAGONAL_RECTANGLE',
'ROUND_2_SAME_RECTANGLE',
'RIGHT_TRIANGLE',
'SMILEY_FACE',
'SNIP_1_RECTANGLE',
'SNIP_2_DIAGONAL_RECTANGLE',
'SNIP_2_SAME_RECTANGLE',
'SNIP_ROUND_RECTANGLE',
'STAR_10',
'STAR_12',
'STAR_16',
'STAR_24',
'STAR_32',
'STAR_4',
'STAR_5',
'STAR_6',
'STAR_7',
'STAR_8',
'STRIPED_RIGHT_ARROW',
'SUN',
'TRAPEZOID',
'TRIANGLE',
'UP_ARROW',
'UP_ARROW_CALLOUT',
'UP_DOWN_ARROW',
'UTURN_ARROW',
'VERTICAL_SCROLL',
'WAVE',
'WEDGE_ELLIPSE_CALLOUT',
'WEDGE_RECTANGLE_CALLOUT',
'WEDGE_ROUND_RECTANGLE_CALLOUT',
'FLOW_CHART_ALTERNATE_PROCESS',
'FLOW_CHART_COLLATE',
'FLOW_CHART_CONNECTOR',
'FLOW_CHART_DECISION',
'FLOW_CHART_DELAY',
'FLOW_CHART_DISPLAY',
'FLOW_CHART_DOCUMENT',
'FLOW_CHART_EXTRACT',
'FLOW_CHART_INPUT_OUTPUT',
'FLOW_CHART_INTERNAL_STORAGE',
'FLOW_CHART_MAGNETIC_DISK',
'FLOW_CHART_MAGNETIC_DRUM',
'FLOW_CHART_MAGNETIC_TAPE',
'FLOW_CHART_MANUAL_INPUT',
'FLOW_CHART_MANUAL_OPERATION',
'FLOW_CHART_MERGE',
'FLOW_CHART_MULTIDOCUMENT',
'FLOW_CHART_OFFLINE_STORAGE',
'FLOW_CHART_OFFPAGE_CONNECTOR',
'FLOW_CHART_ONLINE_STORAGE',
'FLOW_CHART_OR',
'FLOW_CHART_PREDEFINED_PROCESS',
'FLOW_CHART_PREPARATION',
'FLOW_CHART_PROCESS',
'FLOW_CHART_PUNCHED_CARD',
'FLOW_CHART_PUNCHED_TAPE',
'FLOW_CHART_SORT',
'FLOW_CHART_SUMMING_JUNCTION',
'FLOW_CHART_TERMINATOR',
'ARROW_EAST',
'ARROW_NORTH_EAST',
'ARROW_NORTH',
'SPEECH',
'STARBURST',
'TEARDROP',
'ELLIPSE_RIBBON',
'ELLIPSE_RIBBON_2',
'CLOUD_CALLOUT',
'CUSTOM',
] as const
export const createShapeTool: ToolConfig<CreateShapeParams, CreateShapeResponse> = {
id: 'google_slides_create_shape',
name: 'Create Shape in Google Slides',
description:
'Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the slide/page to add the shape to',
},
shapeType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The type of shape to create. Common types: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, DIAMOND, STAR_5, ARROW_EAST, HEART, CLOUD',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Width of the shape in points (default: 200)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Height of the shape in points (default: 100)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position from the left edge in points (default: 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position from the top edge in points (default: 100)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 pageObjectId = params.pageObjectId?.trim()
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
let shapeType = (params.shapeType || 'RECTANGLE').toUpperCase()
if (!SHAPE_TYPES.includes(shapeType as (typeof SHAPE_TYPES)[number])) {
logger.warn(`Invalid shape type "${params.shapeType}", defaulting to RECTANGLE`)
shapeType = 'RECTANGLE'
}
// Generate a unique object ID for the new shape
const shapeObjectId = `shape_${Date.now()}_${generateRandomString(7)}`
// Convert points to EMU
const widthEmu = (params.width || 200) * PT_TO_EMU
const heightEmu = (params.height || 100) * PT_TO_EMU
const translateX = (params.positionX || 100) * PT_TO_EMU
const translateY = (params.positionY || 100) * PT_TO_EMU
return {
requests: [
{
createShape: {
objectId: shapeObjectId,
shapeType,
elementProperties: {
pageObjectId,
size: {
width: {
magnitude: widthEmu,
unit: 'EMU',
},
height: {
magnitude: heightEmu,
unit: 'EMU',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX,
translateY,
unit: 'EMU',
},
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create shape')
}
const createShapeReply = data.replies?.[0]?.createShape
const shapeId = createShapeReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
const shapeType = (params?.shapeType || 'RECTANGLE').toUpperCase()
return {
success: true,
output: {
shapeId,
shapeType,
metadata: {
presentationId,
pageObjectId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
shapeId: {
type: 'string',
description: 'The object ID of the newly created shape',
},
shapeType: {
type: 'string',
description: 'The type of shape that was created',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and page object ID',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
pageObjectId: {
type: 'string',
description: 'The page object ID where the shape was created',
},
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,175 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildElementProperties,
generateObjectId,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateSheetsChartTool')
interface CreateSheetsChartParams {
accessToken: string
presentationId: string
pageObjectId: string
spreadsheetId: string
chartId: number
linkingMode?: 'LINKED' | 'NOT_LINKED_IMAGE'
width?: number
height?: number
positionX?: number
positionY?: number
}
interface CreateSheetsChartResponse {
success: boolean
output: {
chartObjectId: string
metadata: { presentationId: string; pageObjectId: string; url: string }
}
}
export const createSheetsChartTool: ToolConfig<CreateSheetsChartParams, CreateSheetsChartResponse> =
{
id: 'google_slides_create_sheets_chart',
name: 'Embed Google Sheets Chart in Slides',
description:
'Embed a chart from a Google Sheets spreadsheet onto a slide. LINKED charts can be refreshed; NOT_LINKED_IMAGE inserts a static image of the chart.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the slide to add the chart to',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Sheets spreadsheet ID containing the chart',
},
chartId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Numeric chart ID within the spreadsheet',
},
linkingMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LINKED (default) or NOT_LINKED_IMAGE',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Width in points (default 400)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Height in points (default 300)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position in points (default 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position in points (default 100)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const pageObjectId = params.pageObjectId?.trim()
const spreadsheetId = params.spreadsheetId?.trim()
if (!pageObjectId) throw new Error('Page Object ID is required')
if (!spreadsheetId) throw new Error('Spreadsheet ID is required')
if (params.chartId === undefined) throw new Error('Chart ID is required')
const objectId = generateObjectId('chart')
const elementProperties = buildElementProperties({
pageObjectId,
width: params.width,
height: params.height,
positionX: params.positionX,
positionY: params.positionY,
defaultWidth: 400,
defaultHeight: 300,
})
return {
requests: [
{
createSheetsChart: {
objectId,
spreadsheetId,
chartId: params.chartId,
linkingMode: params.linkingMode || 'LINKED',
elementProperties,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create sheets chart')
}
const chartObjectId = data.replies?.[0]?.createSheetsChart?.objectId ?? ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
chartObjectId,
metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
chartObjectId: { type: 'string', description: 'Object ID of the inserted chart' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
pageObjectId: { type: 'string', description: 'The slide ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,236 @@
import { createLogger } from '@sim/logger'
import { generateRandomString } from '@sim/utils/random'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateTableTool')
interface CreateTableParams {
accessToken: string
presentationId: string
pageObjectId: string
rows: number
columns: number
width?: number
height?: number
positionX?: number
positionY?: number
}
interface CreateTableResponse {
success: boolean
output: {
tableId: string
rows: number
columns: number
metadata: {
presentationId: string
pageObjectId: string
url: string
}
}
}
// EMU (English Metric Units) conversion: 1 pt = 12700 EMU
const PT_TO_EMU = 12700
export const createTableTool: ToolConfig<CreateTableParams, CreateTableResponse> = {
id: 'google_slides_create_table',
name: 'Create Table in Google Slides',
description: 'Create a new table on a slide in a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the slide/page to add the table to',
},
rows: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows in the table (minimum 1)',
},
columns: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns in the table (minimum 1)',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Width of the table in points (default: 400)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Height of the table in points (default: 200)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position from the left edge in points (default: 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position from the top edge in points (default: 100)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 pageObjectId = params.pageObjectId?.trim()
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
const rows = params.rows
const columns = params.columns
if (!rows || rows < 1) {
throw new Error('Rows must be at least 1')
}
if (!columns || columns < 1) {
throw new Error('Columns must be at least 1')
}
// Generate a unique object ID for the new table
const tableObjectId = `table_${Date.now()}_${generateRandomString(7)}`
// Convert points to EMU
const widthEmu = (params.width || 400) * PT_TO_EMU
const heightEmu = (params.height || 200) * PT_TO_EMU
const translateX = (params.positionX || 100) * PT_TO_EMU
const translateY = (params.positionY || 100) * PT_TO_EMU
return {
requests: [
{
createTable: {
objectId: tableObjectId,
rows,
columns,
elementProperties: {
pageObjectId,
size: {
width: {
magnitude: widthEmu,
unit: 'EMU',
},
height: {
magnitude: heightEmu,
unit: 'EMU',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX,
translateY,
unit: 'EMU',
},
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create table')
}
const createTableReply = data.replies?.[0]?.createTable
const tableId = createTableReply?.objectId || ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
tableId,
rows: params?.rows ?? 0,
columns: params?.columns ?? 0,
metadata: {
presentationId,
pageObjectId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
tableId: {
type: 'string',
description: 'The object ID of the newly created table',
},
rows: {
type: 'number',
description: 'Number of rows in the table',
},
columns: {
type: 'number',
description: 'Number of columns in the table',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and page object ID',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
pageObjectId: {
type: 'string',
description: 'The page object ID where the table was created',
},
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,165 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildElementProperties,
generateObjectId,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesCreateVideoTool')
interface CreateVideoParams {
accessToken: string
presentationId: string
pageObjectId: string
source: 'YOUTUBE' | 'DRIVE'
videoId: string
width?: number
height?: number
positionX?: number
positionY?: number
}
interface CreateVideoResponse {
success: boolean
output: {
videoObjectId: string
metadata: { presentationId: string; pageObjectId: string; url: string }
}
}
export const createVideoTool: ToolConfig<CreateVideoParams, CreateVideoResponse> = {
id: 'google_slides_create_video',
name: 'Embed Video in Google Slides',
description: 'Embed a YouTube or Google Drive video on a slide.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the slide to add the video to',
},
source: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'YOUTUBE or DRIVE',
},
videoId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'YouTube video ID or Drive file ID',
},
width: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Width in points (default 400)',
},
height: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Height in points (default 225)',
},
positionX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position in points (default 100)',
},
positionY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position in points (default 100)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const pageObjectId = params.pageObjectId?.trim()
const videoId = params.videoId?.trim()
if (!pageObjectId) throw new Error('Page Object ID is required')
if (!videoId) throw new Error('Video ID is required')
if (!params.source) throw new Error('Source is required (YOUTUBE or DRIVE)')
const objectId = generateObjectId('video')
const elementProperties = buildElementProperties({
pageObjectId,
width: params.width,
height: params.height,
positionX: params.positionX,
positionY: params.positionY,
defaultWidth: 400,
defaultHeight: 225,
})
return {
requests: [
{
createVideo: {
objectId,
source: params.source,
id: videoId,
elementProperties,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to create video')
}
const videoObjectId = data.replies?.[0]?.createVideo?.objectId ?? ''
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
return {
success: true,
output: {
videoObjectId,
metadata: { presentationId, pageObjectId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
videoObjectId: { type: 'string', description: 'Object ID of the inserted video' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
pageObjectId: { type: 'string', description: 'The slide ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,135 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDeleteObjectTool')
interface DeleteObjectParams {
accessToken: string
presentationId: string
objectId: string
}
interface DeleteObjectResponse {
success: boolean
output: {
deleted: boolean
objectId: string
metadata: {
presentationId: string
url: string
}
}
}
export const deleteObjectTool: ToolConfig<DeleteObjectParams, DeleteObjectResponse> = {
id: 'google_slides_delete_object',
name: 'Delete Object from Google Slides',
description:
'Delete a page element (shape, image, table, etc.) or an entire slide from a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the element or slide to delete',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 objectId = params.objectId?.trim()
if (!objectId) {
throw new Error('Object ID is required')
}
return {
requests: [
{
deleteObject: {
objectId,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to delete object')
}
const presentationId = params?.presentationId?.trim() || ''
const objectId = params?.objectId?.trim() || ''
return {
success: true,
output: {
deleted: true,
objectId,
metadata: {
presentationId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the object was successfully deleted',
},
objectId: {
type: 'string',
description: 'The object ID that was deleted',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and URL',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,150 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildCellLocation,
buildTextRange,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDeleteParagraphBulletsTool')
interface DeleteParagraphBulletsParams {
accessToken: string
presentationId: string
objectId: string
rowIndex?: number
columnIndex?: number
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
}
interface DeleteParagraphBulletsResponse {
success: boolean
output: {
deleted: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const deleteParagraphBulletsTool: ToolConfig<
DeleteParagraphBulletsParams,
DeleteParagraphBulletsResponse
> = {
id: 'google_slides_delete_paragraph_bullets',
name: 'Delete Paragraph Bullets in Google Slides',
description: 'Remove bullets/numbering from paragraphs in a shape or table cell.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape or table containing the text',
},
rowIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based row index',
},
columnIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based column index',
},
rangeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Range to clear bullets from: ALL (default), FROM_START_INDEX, or FIXED_RANGE',
},
startIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start index for FROM_START_INDEX or FIXED_RANGE',
},
endIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End index for FIXED_RANGE',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const deleteRequest: Record<string, unknown> = {
objectId,
textRange: buildTextRange({
rangeType: params.rangeType,
startIndex: params.startIndex,
endIndex: params.endIndex,
}),
}
const cellLocation = buildCellLocation({
rowIndex: params.rowIndex,
columnIndex: params.columnIndex,
})
if (cellLocation) deleteRequest.cellLocation = cellLocation
return { requests: [{ deleteParagraphBullets: deleteRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to delete paragraph bullets')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
deleted: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether bullets were deleted' },
objectId: { type: 'string', description: 'The object whose bullets were deleted' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,116 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDeleteTableColumnTool')
interface DeleteTableColumnParams {
accessToken: string
presentationId: string
tableObjectId: string
rowIndex: number
columnIndex: number
}
interface DeleteTableColumnResponse {
success: boolean
output: {
deleted: boolean
tableObjectId: string
metadata: { presentationId: string; url: string }
}
}
export const deleteTableColumnTool: ToolConfig<DeleteTableColumnParams, DeleteTableColumnResponse> =
{
id: 'google_slides_delete_table_column',
name: 'Delete Table Column in Google Slides',
description: 'Delete the column containing the reference cell from a table.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
tableObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of any cell in the column',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index identifying the column to delete',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const tableObjectId = params.tableObjectId?.trim()
if (!tableObjectId) throw new Error('Table object ID is required')
return {
requests: [
{
deleteTableColumn: {
tableObjectId,
cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to delete table column')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
deleted: true,
tableObjectId: params?.tableObjectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the column was deleted' },
tableObjectId: { type: 'string', description: 'The table updated' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDeleteTableRowTool')
interface DeleteTableRowParams {
accessToken: string
presentationId: string
tableObjectId: string
rowIndex: number
columnIndex: number
}
interface DeleteTableRowResponse {
success: boolean
output: {
deleted: boolean
tableObjectId: string
metadata: { presentationId: string; url: string }
}
}
export const deleteTableRowTool: ToolConfig<DeleteTableRowParams, DeleteTableRowResponse> = {
id: 'google_slides_delete_table_row',
name: 'Delete Table Row in Google Slides',
description: 'Delete the row containing the reference cell from a table.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
tableObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index identifying the row to delete',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of any cell in the row',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const tableObjectId = params.tableObjectId?.trim()
if (!tableObjectId) throw new Error('Table object ID is required')
return {
requests: [
{
deleteTableRow: {
tableObjectId,
cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to delete table row')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
deleted: true,
tableObjectId: params?.tableObjectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the row was deleted' },
tableObjectId: { type: 'string', description: 'The table updated' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+148
View File
@@ -0,0 +1,148 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildCellLocation,
buildTextRange,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDeleteTextTool')
interface DeleteTextParams {
accessToken: string
presentationId: string
objectId: string
rowIndex?: number
columnIndex?: number
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
}
interface DeleteTextResponse {
success: boolean
output: {
deleted: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const deleteTextTool: ToolConfig<DeleteTextParams, DeleteTextResponse> = {
id: 'google_slides_delete_text',
name: 'Delete Text in Google Slides',
description:
'Delete text from a shape or table cell. Use range type ALL to clear all text, or FIXED_RANGE / FROM_START_INDEX to delete a specific span.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape or table containing the text',
},
rowIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based row index',
},
columnIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based column index',
},
rangeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Range to delete: ALL (default), FROM_START_INDEX, or FIXED_RANGE',
},
startIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start index for FROM_START_INDEX or FIXED_RANGE',
},
endIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End index for FIXED_RANGE',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const deleteRequest: Record<string, unknown> = {
objectId,
textRange: buildTextRange({
rangeType: params.rangeType,
startIndex: params.startIndex,
endIndex: params.endIndex,
}),
}
const cellLocation = buildCellLocation({
rowIndex: params.rowIndex,
columnIndex: params.columnIndex,
})
if (cellLocation) deleteRequest.cellLocation = cellLocation
return { requests: [{ deleteText: deleteRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to delete text')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
deleted: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the text was deleted' },
objectId: { type: 'string', description: 'The object whose text was deleted' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,160 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesDuplicateObjectTool')
interface DuplicateObjectParams {
accessToken: string
presentationId: string
objectId: string
objectIds?: string
}
interface DuplicateObjectResponse {
success: boolean
output: {
duplicatedObjectId: string
metadata: {
presentationId: string
sourceObjectId: string
url: string
}
}
}
export const duplicateObjectTool: ToolConfig<DuplicateObjectParams, DuplicateObjectResponse> = {
id: 'google_slides_duplicate_object',
name: 'Duplicate Object in Google Slides',
description:
'Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the element or slide to duplicate',
},
objectIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional JSON object mapping source object IDs (within the slide being duplicated) to new object IDs for the duplicates. Format: {"sourceId1":"newId1","sourceId2":"newId2"}',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 objectId = params.objectId?.trim()
if (!objectId) {
throw new Error('Object ID is required')
}
const duplicateRequest: Record<string, any> = {
objectId,
}
// Parse objectIds mapping if provided
if (params.objectIds?.trim()) {
try {
const mapping = JSON.parse(params.objectIds)
if (typeof mapping === 'object' && !Array.isArray(mapping)) {
duplicateRequest.objectIds = mapping
}
} catch (e) {
logger.warn('Invalid objectIds JSON, ignoring:', e)
}
}
return {
requests: [
{
duplicateObject: duplicateRequest,
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to duplicate object')
}
const duplicateReply = data.replies?.[0]?.duplicateObject
const duplicatedObjectId = duplicateReply?.objectId ?? ''
const presentationId = params?.presentationId?.trim() || ''
const objectId = params?.objectId?.trim() || ''
return {
success: true,
output: {
duplicatedObjectId,
metadata: {
presentationId,
sourceObjectId: objectId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
duplicatedObjectId: {
type: 'string',
description: 'The object ID of the newly created duplicate',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and source object ID',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
sourceObjectId: {
type: 'string',
description: 'The original object ID that was duplicated',
},
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,223 @@
/**
* @vitest-environment node
*/
import {
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
inputValidationMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockUploadCopilotFile, mockUploadExecutionFile } = vi.hoisted(() => ({
mockUploadCopilotFile: vi.fn(),
mockUploadExecutionFile: vi.fn(),
}))
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
vi.mock('@/lib/uploads/contexts/copilot', () => ({
uploadCopilotFile: mockUploadCopilotFile,
}))
vi.mock('@/lib/uploads/contexts/execution', () => ({
uploadExecutionFile: mockUploadExecutionFile,
}))
import { POST } from '@/app/api/tools/google_slides/export-presentation/route'
import type { ExportPresentationParams } from '@/tools/google_slides/export_presentation'
import { exportPresentationTool } from '@/tools/google_slides/export_presentation'
describe('Google Slides export presentation tool', () => {
beforeEach(() => {
vi.clearAllMocks()
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
authType: 'internal_jwt',
})
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
isValid: true,
resolvedIP: '93.184.216.34',
originalHostname: 'www.googleapis.com',
})
mockUploadExecutionFile.mockResolvedValue({
id: 'file-1',
name: 'presentation-1.pdf',
size: 7,
type: 'application/pdf',
url: '/api/files/serve/execution/file-1',
key: 'execution/workflow/file-1',
context: 'execution',
})
mockUploadCopilotFile.mockResolvedValue({
id: 'copilot-file-1',
name: 'presentation-1.pdf',
size: 4,
type: 'application/pdf',
mimeType: 'application/pdf',
url: '/api/files/serve/copilot/copilot-file-1',
key: 'copilot/copilot-file-1',
context: 'copilot',
})
})
it('routes exports through the internal API with execution context', () => {
const params: ExportPresentationParams = {
accessToken: 'token',
presentationId: 'presentation-1',
exportFormat: 'PDF',
_context: {
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
},
}
expect(exportPresentationTool.request.url).toBe('/api/tools/google_slides/export-presentation')
expect(exportPresentationTool.request.method).toBe('POST')
expect(exportPresentationTool.request.body?.(params)).toEqual({
accessToken: 'token',
presentationId: 'presentation-1',
exportFormat: 'PDF',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
})
})
it('rejects presentation IDs that would break export URL structure', async () => {
const response = await POST(
createMockRequest('POST', {
accessToken: 'token',
presentationId: 'abc?mimeType=evil',
exportFormat: 'PDF',
})
)
const result = (await response.json()) as { success: false; error: string }
expect(response.status).toBe(400)
expect(result.error).toContain('invalid characters')
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('stores exports as execution file references and keeps small legacy base64 output', async () => {
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
new Response('content', {
status: 200,
headers: { 'content-type': 'application/pdf' },
})
)
const response = await POST(
createMockRequest('POST', {
accessToken: 'token',
presentationId: 'presentation-1',
exportFormat: 'PDF',
workspaceId: 'workspace-1',
workflowId: 'workflow-1',
executionId: 'execution-1',
})
)
const result = (await response.json()) as {
success: true
output: {
file: { key: string; context: string; mimeType?: string }
contentBase64?: string
}
}
expect(response.status).toBe(200)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledWith(
'https://www.googleapis.com/drive/v3/files/presentation-1/export?mimeType=application%2Fpdf',
'93.184.216.34',
expect.objectContaining({
headers: { Authorization: 'Bearer token' },
maxResponseBytes: 10 * 1024 * 1024,
})
)
expect(mockUploadExecutionFile).toHaveBeenCalledWith(
{ workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1' },
Buffer.from('content'),
'presentation-1.pdf',
'application/pdf',
'user-1'
)
expect(result?.output.file).toMatchObject({
key: 'execution/workflow/file-1',
context: 'execution',
mimeType: 'application/pdf',
})
expect(result.output.contentBase64).toBe(Buffer.from('content').toString('base64'))
})
it('stores exports in copilot storage when execution context is unavailable', async () => {
const bytes = Uint8Array.from([0, 255, 1, 254])
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
new Response(bytes, {
status: 200,
headers: { 'content-type': 'application/pdf' },
})
)
const response = await POST(
createMockRequest('POST', {
accessToken: 'token',
presentationId: 'presentation-1',
exportFormat: 'PDF',
})
)
const result = (await response.json()) as {
success: true
output: {
file: { key: string; context: string; url: string }
contentBase64?: string
sizeBytes: number
}
}
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
expect(mockUploadCopilotFile).toHaveBeenCalledWith({
buffer: Buffer.from(bytes),
fileName: 'presentation-1.pdf',
contentType: 'application/pdf',
userId: 'user-1',
})
expect(result.output.file).toMatchObject({
key: 'copilot/copilot-file-1',
context: 'copilot',
url: '/api/files/serve/copilot/copilot-file-1',
})
expect(result.output.contentBase64).toBe(Buffer.from(bytes).toString('base64'))
expect(result.output.sizeBytes).toBe(bytes.byteLength)
})
it('maps internal API responses into tool output', async () => {
const response = new Response(
JSON.stringify({
success: true,
output: {
file: {
key: 'copilot/copilot-file-1',
context: 'copilot',
url: '/api/files/serve/copilot/copilot-file-1',
},
mimeType: 'application/pdf',
sizeBytes: 3,
metadata: {
presentationId: 'presentation-1',
url: 'https://docs.google.com/presentation/d/presentation-1/edit',
exportFormat: 'PDF',
},
},
}),
{
status: 200,
headers: { 'content-type': 'application/json' },
}
)
const result = await exportPresentationTool.transformResponse?.(response)
expect(result?.output.file?.key).toBe('copilot/copilot-file-1')
expect(result?.output.metadata.presentationId).toBe('presentation-1')
})
})
@@ -0,0 +1,107 @@
import type { UserFile } from '@/executor/types'
import type { ToolConfig } from '@/tools/types'
export interface ExportPresentationParams {
accessToken: string
presentationId: string
exportFormat?: 'PDF' | 'PPTX' | 'ODP' | 'TXT' | 'PNG' | 'JPEG' | 'SVG'
_context?: Record<string, unknown>
}
export interface ExportPresentationResponse {
success: boolean
output: {
contentBase64?: string
file?: UserFile & { mimeType?: string }
mimeType: string
sizeBytes: number
metadata: { presentationId: string; url: string; exportFormat: string }
}
}
export const exportPresentationTool: ToolConfig<
ExportPresentationParams,
ExportPresentationResponse
> = {
id: 'google_slides_export_presentation',
name: 'Export Google Slides Presentation',
description:
'Export a presentation to PDF, PPTX, ODP, TXT, PNG, JPEG, or SVG via the Drive export endpoint. Stores the exported file as an execution file when execution context is available.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides / Drive API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
exportFormat: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Format: PDF (default), PPTX, ODP, TXT, PNG, JPEG, or SVG',
},
},
request: {
url: '/api/tools/google_slides/export-presentation',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
accessToken: params.accessToken,
presentationId: params.presentationId,
exportFormat: params.exportFormat,
workspaceId:
typeof params._context?.workspaceId === 'string' ? params._context.workspaceId : undefined,
workflowId:
typeof params._context?.workflowId === 'string' ? params._context.workflowId : undefined,
executionId:
typeof params._context?.executionId === 'string' ? params._context.executionId : undefined,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok || data.success === false) {
throw new Error(data.error || 'Failed to export presentation')
}
return {
success: true,
output: data.output,
}
},
outputs: {
file: {
type: 'file',
description: 'Stored exported presentation file',
optional: true,
},
contentBase64: {
type: 'string',
description: 'Deprecated legacy inline content. New exports return file.',
optional: true,
},
mimeType: { type: 'string', description: 'MIME type of the exported content' },
sizeBytes: { type: 'number', description: 'Size of the exported content in bytes' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
exportFormat: { type: 'string', description: 'Export format used' },
},
},
},
}
+164
View File
@@ -0,0 +1,164 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesGetPageTool')
interface GetPageParams {
accessToken: string
presentationId: string
pageObjectId: string
}
interface GetPageResponse {
success: boolean
output: {
objectId: string
pageType: string
pageElements: any[]
slideProperties: {
layoutObjectId: string | null
masterObjectId: string | null
notesPage: any | null
} | null
metadata: {
presentationId: string
url: string
}
}
}
export const getPageTool: ToolConfig<GetPageParams, GetPageResponse> = {
id: 'google_slides_get_page',
name: 'Get Slide Page',
description:
'Get detailed information about a specific slide/page in a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the slide/page to retrieve',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
const pageObjectId = params.pageObjectId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to get page')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
objectId: data.objectId,
pageType: data.pageType ?? 'SLIDE',
pageElements: data.pageElements ?? [],
slideProperties: data.slideProperties
? {
layoutObjectId: data.slideProperties.layoutObjectId ?? null,
masterObjectId: data.slideProperties.masterObjectId ?? null,
notesPage: data.slideProperties.notesPage ?? null,
}
: null,
metadata: {
presentationId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
objectId: {
type: 'string',
description: 'The object ID of the page',
},
pageType: {
type: 'string',
description: 'The type of page (SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER)',
},
pageElements: {
type: 'array',
description: 'Array of page elements (shapes, images, tables, etc.) on this page',
items: {
type: 'json',
},
},
slideProperties: {
type: 'object',
description: 'Properties specific to slides (layout, master, notes)',
optional: true,
properties: {
layoutObjectId: {
type: 'string',
description: 'Object ID of the layout this slide is based on',
},
masterObjectId: {
type: 'string',
description: 'Object ID of the master this slide is based on',
},
notesPage: {
type: 'json',
description: 'The notes page associated with the slide',
optional: true,
},
},
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and URL',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,190 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesGetThumbnailTool')
interface GetThumbnailParams {
accessToken: string
presentationId: string
pageObjectId: string
thumbnailSize?: string
mimeType?: string
}
interface GetThumbnailResponse {
success: boolean
output: {
contentUrl: string
width: number
height: number
metadata: {
presentationId: string
pageObjectId: string
thumbnailSize: string
mimeType: string
}
}
}
// Available thumbnail sizes
const THUMBNAIL_SIZES = ['SMALL', 'MEDIUM', 'LARGE'] as const
// Available MIME types for thumbnails
const MIME_TYPES = ['PNG'] as const
export const getThumbnailTool: ToolConfig<GetThumbnailParams, GetThumbnailResponse> = {
id: 'google_slides_get_thumbnail',
name: 'Get Slide Thumbnail',
description: 'Generate a thumbnail image of a specific slide in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
pageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object ID of the slide/page to get a thumbnail for',
},
thumbnailSize: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The size of the thumbnail: SMALL (200px), MEDIUM (800px), or LARGE (1600px). Defaults to MEDIUM.',
},
mimeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The MIME type of the thumbnail image: PNG. Defaults to PNG.',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
const pageObjectId = params.pageObjectId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
if (!pageObjectId) {
throw new Error('Page Object ID is required')
}
// Build the URL with query parameters for thumbnail properties
let size = (params.thumbnailSize || 'MEDIUM').toUpperCase()
if (!THUMBNAIL_SIZES.includes(size as (typeof THUMBNAIL_SIZES)[number])) {
size = 'MEDIUM'
}
// Validate and normalize mimeType
let mimeType = (params.mimeType || 'PNG').toUpperCase()
if (!MIME_TYPES.includes(mimeType as (typeof MIME_TYPES)[number])) {
mimeType = 'PNG'
}
// The API uses thumbnailProperties as query parameters
let url = `https://slides.googleapis.com/v1/presentations/${presentationId}/pages/${pageObjectId}/thumbnail?thumbnailProperties.thumbnailSize=${size}`
// Add mimeType if not the default (PNG)
if (mimeType !== 'PNG') {
url += `&thumbnailProperties.mimeType=${mimeType}`
}
return url
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to get thumbnail')
}
const presentationId = params?.presentationId?.trim() || ''
const pageObjectId = params?.pageObjectId?.trim() || ''
const thumbnailSize = (params?.thumbnailSize || 'MEDIUM').toUpperCase()
const mimeType = (params?.mimeType || 'PNG').toUpperCase()
return {
success: true,
output: {
contentUrl: data.contentUrl,
width: data.width,
height: data.height,
metadata: {
presentationId,
pageObjectId,
thumbnailSize,
mimeType,
},
},
}
},
outputs: {
contentUrl: {
type: 'string',
description: 'URL to the thumbnail image (valid for 30 minutes)',
},
width: {
type: 'number',
description: 'Width of the thumbnail in pixels',
},
height: {
type: 'number',
description: 'Height of the thumbnail in pixels',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and page object ID',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
pageObjectId: {
type: 'string',
description: 'The page object ID for the thumbnail',
},
thumbnailSize: {
type: 'string',
description: 'The requested thumbnail size',
},
mimeType: {
type: 'string',
description: 'The thumbnail MIME type',
},
},
},
},
}
@@ -0,0 +1,131 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
generateObjectId,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesGroupObjectsTool')
interface GroupObjectsParams {
accessToken: string
presentationId: string
childrenObjectIds: string
groupObjectId?: string
}
interface GroupObjectsResponse {
success: boolean
output: {
grouped: boolean
groupObjectId: string
childrenObjectIds: string[]
metadata: { presentationId: string; url: string }
}
}
export const groupObjectsTool: ToolConfig<GroupObjectsParams, GroupObjectsResponse> = {
id: 'google_slides_group_objects',
name: 'Group Objects in Google Slides',
description: 'Group two or more page elements on the same slide into a single object group.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
childrenObjectIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated object IDs of the elements to group (must be on the same slide)',
},
groupObjectId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional object ID to assign to the new group',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const children = (params.childrenObjectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
if (children.length < 2) throw new Error('At least two child object IDs are required')
const groupObjectId = params.groupObjectId?.trim() || generateObjectId('group')
return {
requests: [
{
groupObjects: {
groupObjectId,
childrenObjectIds: children,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to group objects')
}
const groupObjectId = data.replies?.[0]?.groupObjects?.objectId ?? params?.groupObjectId ?? ''
const presentationId = params?.presentationId?.trim() || ''
const children = (params?.childrenObjectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
return {
success: true,
output: {
grouped: true,
groupObjectId,
childrenObjectIds: children,
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
grouped: { type: 'boolean', description: 'Whether the objects were grouped' },
groupObjectId: { type: 'string', description: 'Object ID of the new group' },
childrenObjectIds: {
type: 'array',
description: 'IDs of the grouped children',
items: { type: 'string' },
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+114
View File
@@ -0,0 +1,114 @@
import { addImageTool } from '@/tools/google_slides/add_image'
import { addSlideTool } from '@/tools/google_slides/add_slide'
import { batchUpdateTool } from '@/tools/google_slides/batch_update'
import { copyPresentationTool } from '@/tools/google_slides/copy_presentation'
import { createTool } from '@/tools/google_slides/create'
import { createLineTool } from '@/tools/google_slides/create_line'
import { createParagraphBulletsTool } from '@/tools/google_slides/create_paragraph_bullets'
import { createShapeTool } from '@/tools/google_slides/create_shape'
import { createSheetsChartTool } from '@/tools/google_slides/create_sheets_chart'
import { createTableTool } from '@/tools/google_slides/create_table'
import { createVideoTool } from '@/tools/google_slides/create_video'
import { deleteObjectTool } from '@/tools/google_slides/delete_object'
import { deleteParagraphBulletsTool } from '@/tools/google_slides/delete_paragraph_bullets'
import { deleteTableColumnTool } from '@/tools/google_slides/delete_table_column'
import { deleteTableRowTool } from '@/tools/google_slides/delete_table_row'
import { deleteTextTool } from '@/tools/google_slides/delete_text'
import { duplicateObjectTool } from '@/tools/google_slides/duplicate_object'
import { exportPresentationTool } from '@/tools/google_slides/export_presentation'
import { getPageTool } from '@/tools/google_slides/get_page'
import { getThumbnailTool } from '@/tools/google_slides/get_thumbnail'
import { groupObjectsTool } from '@/tools/google_slides/group_objects'
import { insertTableColumnsTool } from '@/tools/google_slides/insert_table_columns'
import { insertTableRowsTool } from '@/tools/google_slides/insert_table_rows'
import { insertTextTool } from '@/tools/google_slides/insert_text'
import { mergeTableCellsTool } from '@/tools/google_slides/merge_table_cells'
import { readTool } from '@/tools/google_slides/read'
import { refreshSheetsChartTool } from '@/tools/google_slides/refresh_sheets_chart'
import { replaceAllShapesWithImageTool } from '@/tools/google_slides/replace_all_shapes_with_image'
import { replaceAllShapesWithSheetsChartTool } from '@/tools/google_slides/replace_all_shapes_with_sheets_chart'
import { replaceAllTextTool } from '@/tools/google_slides/replace_all_text'
import { replaceImageTool } from '@/tools/google_slides/replace_image'
import { rerouteLineTool } from '@/tools/google_slides/reroute_line'
import { ungroupObjectsTool } from '@/tools/google_slides/ungroup_objects'
import { unmergeTableCellsTool } from '@/tools/google_slides/unmerge_table_cells'
import { updateImagePropertiesTool } from '@/tools/google_slides/update_image_properties'
import { updateLineCategoryTool } from '@/tools/google_slides/update_line_category'
import { updateLinePropertiesTool } from '@/tools/google_slides/update_line_properties'
import { updatePageElementAltTextTool } from '@/tools/google_slides/update_page_element_alt_text'
import { updatePageElementTransformTool } from '@/tools/google_slides/update_page_element_transform'
import { updatePageElementsZOrderTool } from '@/tools/google_slides/update_page_elements_z_order'
import { updatePagePropertiesTool } from '@/tools/google_slides/update_page_properties'
import { updateParagraphStyleTool } from '@/tools/google_slides/update_paragraph_style'
import { updateShapePropertiesTool } from '@/tools/google_slides/update_shape_properties'
import { updateSlidePropertiesTool } from '@/tools/google_slides/update_slide_properties'
import { updateSlidesPositionTool } from '@/tools/google_slides/update_slides_position'
import { updateTableBorderPropertiesTool } from '@/tools/google_slides/update_table_border_properties'
import { updateTableCellPropertiesTool } from '@/tools/google_slides/update_table_cell_properties'
import { updateTableColumnPropertiesTool } from '@/tools/google_slides/update_table_column_properties'
import { updateTableRowPropertiesTool } from '@/tools/google_slides/update_table_row_properties'
import { updateTextStyleTool } from '@/tools/google_slides/update_text_style'
import { updateVideoPropertiesTool } from '@/tools/google_slides/update_video_properties'
import { writeTool } from '@/tools/google_slides/write'
export const googleSlidesReadTool = readTool
export const googleSlidesWriteTool = writeTool
export const googleSlidesCreateTool = createTool
export const googleSlidesReplaceAllTextTool = replaceAllTextTool
export const googleSlidesAddSlideTool = addSlideTool
export const googleSlidesGetThumbnailTool = getThumbnailTool
export const googleSlidesAddImageTool = addImageTool
export const googleSlidesGetPageTool = getPageTool
export const googleSlidesDeleteObjectTool = deleteObjectTool
export const googleSlidesDuplicateObjectTool = duplicateObjectTool
export const googleSlidesUpdateSlidesPositionTool = updateSlidesPositionTool
export const googleSlidesCreateTableTool = createTableTool
export const googleSlidesCreateShapeTool = createShapeTool
export const googleSlidesInsertTextTool = insertTextTool
export const googleSlidesUpdateTextStyleTool = updateTextStyleTool
export const googleSlidesUpdateParagraphStyleTool = updateParagraphStyleTool
export const googleSlidesDeleteTextTool = deleteTextTool
export const googleSlidesCreateParagraphBulletsTool = createParagraphBulletsTool
export const googleSlidesDeleteParagraphBulletsTool = deleteParagraphBulletsTool
export const googleSlidesReplaceAllShapesWithImageTool = replaceAllShapesWithImageTool
export const googleSlidesReplaceImageTool = replaceImageTool
export const googleSlidesUpdateImagePropertiesTool = updateImagePropertiesTool
export const googleSlidesUpdateShapePropertiesTool = updateShapePropertiesTool
export const googleSlidesUpdatePagePropertiesTool = updatePagePropertiesTool
export const googleSlidesUpdateSlidePropertiesTool = updateSlidePropertiesTool
export const googleSlidesUpdatePageElementAltTextTool = updatePageElementAltTextTool
export const googleSlidesUpdatePageElementTransformTool = updatePageElementTransformTool
export const googleSlidesUpdatePageElementsZOrderTool = updatePageElementsZOrderTool
export const googleSlidesGroupObjectsTool = groupObjectsTool
export const googleSlidesUngroupObjectsTool = ungroupObjectsTool
export const googleSlidesCreateLineTool = createLineTool
export const googleSlidesUpdateLinePropertiesTool = updateLinePropertiesTool
export const googleSlidesUpdateLineCategoryTool = updateLineCategoryTool
export const googleSlidesRerouteLineTool = rerouteLineTool
export const googleSlidesInsertTableRowsTool = insertTableRowsTool
export const googleSlidesInsertTableColumnsTool = insertTableColumnsTool
export const googleSlidesDeleteTableRowTool = deleteTableRowTool
export const googleSlidesDeleteTableColumnTool = deleteTableColumnTool
export const googleSlidesMergeTableCellsTool = mergeTableCellsTool
export const googleSlidesUnmergeTableCellsTool = unmergeTableCellsTool
export const googleSlidesUpdateTableCellPropertiesTool = updateTableCellPropertiesTool
export const googleSlidesUpdateTableBorderPropertiesTool = updateTableBorderPropertiesTool
export const googleSlidesUpdateTableColumnPropertiesTool = updateTableColumnPropertiesTool
export const googleSlidesUpdateTableRowPropertiesTool = updateTableRowPropertiesTool
export const googleSlidesCreateSheetsChartTool = createSheetsChartTool
export const googleSlidesRefreshSheetsChartTool = refreshSheetsChartTool
export const googleSlidesReplaceAllShapesWithSheetsChartTool = replaceAllShapesWithSheetsChartTool
export const googleSlidesCreateVideoTool = createVideoTool
export const googleSlidesUpdateVideoPropertiesTool = updateVideoPropertiesTool
export const googleSlidesBatchUpdateTool = batchUpdateTool
export const googleSlidesCopyPresentationTool = copyPresentationTool
export const googleSlidesExportPresentationTool = exportPresentationTool
@@ -0,0 +1,139 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesInsertTableColumnsTool')
interface InsertTableColumnsParams {
accessToken: string
presentationId: string
tableObjectId: string
rowIndex: number
columnIndex: number
number: number
insertRight?: boolean
}
interface InsertTableColumnsResponse {
success: boolean
output: {
inserted: boolean
tableObjectId: string
number: number
metadata: { presentationId: string; url: string }
}
}
export const insertTableColumnsTool: ToolConfig<
InsertTableColumnsParams,
InsertTableColumnsResponse
> = {
id: 'google_slides_insert_table_columns',
name: 'Insert Table Columns in Google Slides',
description: 'Insert one or more columns into a table, left or right of a reference cell.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
tableObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the reference cell',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the reference cell',
},
number: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns to insert (minimum 1)',
},
insertRight: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Insert to the right of the reference column instead of left (default false)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const tableObjectId = params.tableObjectId?.trim()
if (!tableObjectId) throw new Error('Table object ID is required')
const number = params.number
if (!number || number < 1) throw new Error('Number of columns must be at least 1')
return {
requests: [
{
insertTableColumns: {
tableObjectId,
cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
insertRight: params.insertRight ?? false,
number,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to insert table columns')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
inserted: true,
tableObjectId: params?.tableObjectId?.trim() || '',
number: params?.number ?? 0,
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
inserted: { type: 'boolean', description: 'Whether columns were inserted' },
tableObjectId: { type: 'string', description: 'The table updated' },
number: { type: 'number', description: 'Number of columns inserted' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,136 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesInsertTableRowsTool')
interface InsertTableRowsParams {
accessToken: string
presentationId: string
tableObjectId: string
rowIndex: number
columnIndex: number
number: number
insertBelow?: boolean
}
interface InsertTableRowsResponse {
success: boolean
output: {
inserted: boolean
tableObjectId: string
number: number
metadata: { presentationId: string; url: string }
}
}
export const insertTableRowsTool: ToolConfig<InsertTableRowsParams, InsertTableRowsResponse> = {
id: 'google_slides_insert_table_rows',
name: 'Insert Table Rows in Google Slides',
description: 'Insert one or more rows into a table, above or below a reference cell.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
tableObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the reference cell',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the reference cell',
},
number: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows to insert (minimum 1)',
},
insertBelow: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Insert below the reference row instead of above (default false)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const tableObjectId = params.tableObjectId?.trim()
if (!tableObjectId) throw new Error('Table object ID is required')
const number = params.number
if (!number || number < 1) throw new Error('Number of rows must be at least 1')
return {
requests: [
{
insertTableRows: {
tableObjectId,
cellLocation: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
insertBelow: params.insertBelow ?? false,
number,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to insert table rows')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
inserted: true,
tableObjectId: params?.tableObjectId?.trim() || '',
number: params?.number ?? 0,
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
inserted: { type: 'boolean', description: 'Whether rows were inserted' },
tableObjectId: { type: 'string', description: 'The table updated' },
number: { type: 'number', description: 'Number of rows inserted' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+163
View File
@@ -0,0 +1,163 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesInsertTextTool')
interface InsertTextParams {
accessToken: string
presentationId: string
objectId: string
text: string
insertionIndex?: number
}
interface InsertTextResponse {
success: boolean
output: {
inserted: boolean
objectId: string
text: string
metadata: {
presentationId: string
url: string
}
}
}
export const insertTextTool: ToolConfig<InsertTextParams, InsertTextResponse> = {
id: 'google_slides_insert_text',
name: 'Insert Text in Google Slides',
description:
'Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or table cells.',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'The object ID of the shape or table cell to insert text into. For table cells, use the cell object ID.',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text to insert',
},
insertionIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'The zero-based index at which to insert the text. If not specified, text is inserted at the beginning (index 0).',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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 objectId = params.objectId?.trim()
if (!objectId) {
throw new Error('Object ID is required')
}
if (params.text === undefined || params.text === null) {
throw new Error('Text is required')
}
return {
requests: [
{
insertText: {
objectId,
text: params.text,
insertionIndex: params.insertionIndex ?? 0,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to insert text')
}
const presentationId = params?.presentationId?.trim() || ''
const objectId = params?.objectId?.trim() || ''
return {
success: true,
output: {
inserted: true,
objectId,
text: params?.text ?? '',
metadata: {
presentationId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
inserted: {
type: 'boolean',
description: 'Whether the text was successfully inserted',
},
objectId: {
type: 'string',
description: 'The object ID where text was inserted',
},
text: {
type: 'string',
description: 'The text that was inserted',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and URL',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,137 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesMergeTableCellsTool')
interface MergeTableCellsParams {
accessToken: string
presentationId: string
objectId: string
rowIndex: number
columnIndex: number
rowSpan: number
columnSpan: number
}
interface MergeTableCellsResponse {
success: boolean
output: {
merged: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const mergeTableCellsTool: ToolConfig<MergeTableCellsParams, MergeTableCellsResponse> = {
id: 'google_slides_merge_table_cells',
name: 'Merge Table Cells in Google Slides',
description:
'Merge a rectangular range of table cells into a single cell. The range starts at (rowIndex, columnIndex) and covers rowSpan × columnSpan cells.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the top-left cell',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the top-left cell',
},
rowSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows to merge (minimum 1)',
},
columnSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns to merge (minimum 1)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
if (!params.rowSpan || params.rowSpan < 1) throw new Error('rowSpan must be at least 1')
if (!params.columnSpan || params.columnSpan < 1)
throw new Error('columnSpan must be at least 1')
return {
requests: [
{
mergeTableCells: {
objectId,
tableRange: {
location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
rowSpan: params.rowSpan,
columnSpan: params.columnSpan,
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to merge table cells')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
merged: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
merged: { type: 'boolean', description: 'Whether the cells were merged' },
objectId: { type: 'string', description: 'The table updated' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+117
View File
@@ -0,0 +1,117 @@
import type { GoogleSlidesReadResponse, GoogleSlidesToolParams } from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
export const readTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesReadResponse> = {
id: 'google_slides_read',
name: 'Read Google Slides Presentation',
description: 'Read content from a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
},
request: {
url: (params) => {
// Ensure presentationId is valid
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
},
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 slides from the response
const slides = data.slides || []
// Create presentation metadata
const metadata = {
presentationId: data.presentationId,
title: data.title || 'Untitled Presentation',
pageSize: data.pageSize,
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
}
return {
success: true,
output: {
slides,
metadata,
},
}
},
outputs: {
slides: { type: 'json', description: 'Array of slides with their content' },
metadata: {
type: 'json',
description: 'Presentation metadata including ID, title, and URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
title: {
type: 'string',
description: 'The presentation title',
},
pageSize: {
type: 'object',
description: 'Presentation page size',
optional: true,
properties: {
width: {
type: 'json',
description: 'Page width as a Dimension object',
},
height: {
type: 'json',
description: 'Page height as a Dimension object',
},
},
},
mimeType: {
type: 'string',
description: 'The mime type of the presentation',
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
}
@@ -0,0 +1,95 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesRefreshSheetsChartTool')
interface RefreshSheetsChartParams {
accessToken: string
presentationId: string
objectId: string
}
interface RefreshSheetsChartResponse {
success: boolean
output: {
refreshed: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const refreshSheetsChartTool: ToolConfig<
RefreshSheetsChartParams,
RefreshSheetsChartResponse
> = {
id: 'google_slides_refresh_sheets_chart',
name: 'Refresh Sheets Chart in Slides',
description:
'Refresh an embedded linked Sheets chart so it reflects the latest spreadsheet data.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the embedded chart to refresh',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
return { requests: [{ refreshSheetsChart: { objectId } }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to refresh sheets chart')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
refreshed: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
refreshed: { type: 'boolean', description: 'Whether the chart was refreshed' },
objectId: { type: 'string', description: 'The chart object refreshed' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,151 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesReplaceAllShapesWithImageTool')
interface ReplaceAllShapesWithImageParams {
accessToken: string
presentationId: string
imageUrl: string
findText: string
matchCase?: boolean
imageReplaceMethod?: 'CENTER_INSIDE' | 'CENTER_CROP'
pageObjectIds?: string
}
interface ReplaceAllShapesWithImageResponse {
success: boolean
output: {
occurrencesChanged: number
metadata: { presentationId: string; url: string; imageUrl: string; findText: string }
}
}
export const replaceAllShapesWithImageTool: ToolConfig<
ReplaceAllShapesWithImageParams,
ReplaceAllShapesWithImageResponse
> = {
id: 'google_slides_replace_all_shapes_with_image',
name: 'Replace All Shapes With Image in Google Slides',
description:
"Find every shape whose text matches the given token (e.g. {{cover-image}}) and replace it with an image, preserving the shape's position and bounds.",
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
imageUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
"Publicly fetchable image URL (PNG, JPEG, or GIF; max 50 MB and accessible to Google's servers)",
},
findText: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Text content of shapes to replace (e.g. {{cover-image}})',
},
matchCase: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Case-sensitive match (default: true)',
},
imageReplaceMethod: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'How the image fits the shape: CENTER_INSIDE (preserve aspect, fit inside) or CENTER_CROP (fill, crop overflow). Default: CENTER_INSIDE.',
},
pageObjectIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated slide IDs to limit replacement to specific slides',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const imageUrl = params.imageUrl?.trim()
const findText = params.findText
if (!imageUrl) throw new Error('Image URL is required')
if (!findText) throw new Error('Find text is required')
const request: Record<string, unknown> = {
imageUrl,
containsText: {
text: findText,
matchCase: params.matchCase !== false,
},
imageReplaceMethod: params.imageReplaceMethod || 'CENTER_INSIDE',
}
if (params.pageObjectIds?.trim()) {
request.pageObjectIds = params.pageObjectIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
}
return { requests: [{ replaceAllShapesWithImage: request }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to replace shapes with image')
}
const occurrencesChanged = data.replies?.[0]?.replaceAllShapesWithImage?.occurrencesChanged ?? 0
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
occurrencesChanged,
metadata: {
presentationId,
url: presentationUrl(presentationId),
imageUrl: params?.imageUrl?.trim() || '',
findText: params?.findText || '',
},
},
}
},
outputs: {
occurrencesChanged: {
type: 'number',
description: 'Number of shapes that were replaced with the image',
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
imageUrl: { type: 'string', description: 'The image URL inserted' },
findText: { type: 'string', description: 'The matched text token' },
},
},
},
}
@@ -0,0 +1,164 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesReplaceAllShapesWithSheetsChartTool')
interface ReplaceAllShapesWithSheetsChartParams {
accessToken: string
presentationId: string
spreadsheetId: string
chartId: number
findText: string
matchCase?: boolean
linkingMode?: 'LINKED' | 'NOT_LINKED_IMAGE'
pageObjectIds?: string
}
interface ReplaceAllShapesWithSheetsChartResponse {
success: boolean
output: {
occurrencesChanged: number
metadata: {
presentationId: string
url: string
findText: string
spreadsheetId: string
chartId: number
}
}
}
export const replaceAllShapesWithSheetsChartTool: ToolConfig<
ReplaceAllShapesWithSheetsChartParams,
ReplaceAllShapesWithSheetsChartResponse
> = {
id: 'google_slides_replace_all_shapes_with_sheets_chart',
name: 'Replace All Shapes With Sheets Chart in Slides',
description:
"Find every shape matching a text token (e.g. {{revenue-chart}}) and replace each with the same embedded Sheets chart, preserving the shape's position and bounds.",
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
spreadsheetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Sheets spreadsheet ID containing the chart',
},
chartId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Numeric chart ID within the spreadsheet',
},
findText: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Text content of shapes to replace (e.g. {{revenue-chart}})',
},
matchCase: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Case-sensitive match (default true)',
},
linkingMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'LINKED (default) or NOT_LINKED_IMAGE',
},
pageObjectIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated slide IDs to limit replacement to specific slides',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const spreadsheetId = params.spreadsheetId?.trim()
if (!spreadsheetId) throw new Error('Spreadsheet ID is required')
if (params.chartId === undefined) throw new Error('Chart ID is required')
const findText = params.findText
if (!findText) throw new Error('Find text is required')
const request: Record<string, unknown> = {
spreadsheetId,
chartId: params.chartId,
linkingMode: params.linkingMode || 'LINKED',
containsText: { text: findText, matchCase: params.matchCase !== false },
}
if (params.pageObjectIds?.trim()) {
request.pageObjectIds = params.pageObjectIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
}
return { requests: [{ replaceAllShapesWithSheetsChart: request }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to replace shapes with sheets chart')
}
const occurrencesChanged =
data.replies?.[0]?.replaceAllShapesWithSheetsChart?.occurrencesChanged ?? 0
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
occurrencesChanged,
metadata: {
presentationId,
url: presentationUrl(presentationId),
findText: params?.findText || '',
spreadsheetId: params?.spreadsheetId?.trim() || '',
chartId: params?.chartId ?? 0,
},
},
}
},
outputs: {
occurrencesChanged: {
type: 'number',
description: 'Number of shapes replaced with the chart',
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
findText: { type: 'string', description: 'The matched text token' },
spreadsheetId: { type: 'string', description: 'Source spreadsheet ID' },
chartId: { type: 'number', description: 'Source chart ID' },
},
},
},
}
@@ -0,0 +1,187 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesReplaceAllTextTool')
interface ReplaceAllTextParams {
accessToken: string
presentationId: string
findText: string
replaceText: string
matchCase?: boolean
pageObjectIds?: string
}
interface ReplaceAllTextResponse {
success: boolean
output: {
occurrencesChanged: number
metadata: {
presentationId: string
findText: string
replaceText: string
url: string
}
}
}
export const replaceAllTextTool: ToolConfig<ReplaceAllTextParams, ReplaceAllTextResponse> = {
id: 'google_slides_replace_all_text',
name: 'Replace All Text in Google Slides',
description: 'Find and replace all occurrences of text throughout a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
findText: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text to find (e.g., {{placeholder}})',
},
replaceText: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text to replace with',
},
matchCase: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the search should be case-sensitive (default: true)',
},
pageObjectIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated list of slide object IDs to limit replacements to specific slides (leave empty for all slides)',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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.findText) {
throw new Error('Find text is required')
}
if (params.replaceText === undefined || params.replaceText === null) {
throw new Error('Replace text is required')
}
const replaceAllTextRequest: Record<string, any> = {
containsText: {
text: params.findText,
matchCase: params.matchCase !== false, // Default to true
},
replaceText: params.replaceText,
}
// Add pageObjectIds if specified to limit replacements to specific slides
if (params.pageObjectIds?.trim()) {
replaceAllTextRequest.pageObjectIds = params.pageObjectIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
}
return {
requests: [
{
replaceAllText: replaceAllTextRequest,
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to replace text')
}
// The response contains replies array with replaceAllText results
const replaceResult = data.replies?.[0]?.replaceAllText
const occurrencesChanged = replaceResult?.occurrencesChanged || 0
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
occurrencesChanged,
metadata: {
presentationId,
findText: params?.findText || '',
replaceText: params?.replaceText || '',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
occurrencesChanged: {
type: 'number',
description: 'Number of text occurrences that were replaced',
},
metadata: {
type: 'json',
description: 'Operation metadata including presentation ID and URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
findText: {
type: 'string',
description: 'The text that was searched for',
},
replaceText: {
type: 'string',
description: 'The text that replaced the matches',
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
}
@@ -0,0 +1,125 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesReplaceImageTool')
interface ReplaceImageParams {
accessToken: string
presentationId: string
imageObjectId: string
imageUrl: string
imageReplaceMethod?: 'CENTER_INSIDE' | 'CENTER_CROP'
}
interface ReplaceImageResponse {
success: boolean
output: {
replaced: boolean
imageObjectId: string
metadata: { presentationId: string; url: string; imageUrl: string }
}
}
export const replaceImageTool: ToolConfig<ReplaceImageParams, ReplaceImageResponse> = {
id: 'google_slides_replace_image',
name: 'Replace Image in Google Slides',
description:
"Replace the source of an existing image with a new image URL, preserving the image's position, size, and properties.",
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
imageObjectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the existing image to replace',
},
imageUrl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New publicly fetchable image URL (PNG, JPEG, or GIF, max 50 MB)',
},
imageReplaceMethod: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'CENTER_INSIDE (preserve aspect) or CENTER_CROP (fill, crop overflow). Default: CENTER_INSIDE.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const imageObjectId = params.imageObjectId?.trim()
const imageUrl = params.imageUrl?.trim()
if (!imageObjectId) throw new Error('Image object ID is required')
if (!imageUrl) throw new Error('Image URL is required')
return {
requests: [
{
replaceImage: {
imageObjectId,
url: imageUrl,
imageReplaceMethod: params.imageReplaceMethod || 'CENTER_INSIDE',
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to replace image')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
replaced: true,
imageObjectId: params?.imageObjectId?.trim() || '',
metadata: {
presentationId,
url: presentationUrl(presentationId),
imageUrl: params?.imageUrl?.trim() || '',
},
},
}
},
outputs: {
replaced: { type: 'boolean', description: 'Whether the image was replaced' },
imageObjectId: { type: 'string', description: 'The image object that was replaced' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
imageUrl: { type: 'string', description: 'The new image URL' },
},
},
},
}
@@ -0,0 +1,92 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesRerouteLineTool')
interface RerouteLineParams {
accessToken: string
presentationId: string
objectId: string
}
interface RerouteLineResponse {
success: boolean
output: {
rerouted: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const rerouteLineTool: ToolConfig<RerouteLineParams, RerouteLineResponse> = {
id: 'google_slides_reroute_line',
name: 'Reroute Line in Google Slides',
description:
'Reroute a connector line so it efficiently connects its endpoint shapes — useful after moving the shapes the line connects.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the connector line to reroute',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
return { requests: [{ rerouteLine: { objectId } }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to reroute line')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
rerouted: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
rerouted: { type: 'boolean', description: 'Whether the line was rerouted' },
objectId: { type: 'string', description: 'The line object rerouted' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+50
View File
@@ -0,0 +1,50 @@
import type { ToolResponse } from '@/tools/types'
interface GoogleSlidesMetadata {
presentationId: string
title: string
pageSize?: {
width: number
height: number
}
mimeType?: string
createdTime?: string
modifiedTime?: string
url?: string
}
export interface GoogleSlidesReadResponse extends ToolResponse {
output: {
slides: any[]
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesWriteResponse extends ToolResponse {
output: {
updatedContent: boolean
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesCreateResponse extends ToolResponse {
output: {
metadata: GoogleSlidesMetadata
}
}
export interface GoogleSlidesToolParams {
accessToken: string
presentationId?: string
manualPresentationId?: string
title?: string
content?: string
slideIndex?: number
folderId?: string
folderSelector?: string
}
export type GoogleSlidesResponse =
| GoogleSlidesReadResponse
| GoogleSlidesWriteResponse
| GoogleSlidesCreateResponse
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUngroupObjectsTool')
interface UngroupObjectsParams {
accessToken: string
presentationId: string
objectIds: string
}
interface UngroupObjectsResponse {
success: boolean
output: {
ungrouped: boolean
objectIds: string[]
metadata: { presentationId: string; url: string }
}
}
export const ungroupObjectsTool: ToolConfig<UngroupObjectsParams, UngroupObjectsResponse> = {
id: 'google_slides_ungroup_objects',
name: 'Ungroup Objects in Google Slides',
description: 'Ungroup one or more object groups, releasing their children back to the slide.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated object IDs of the groups to ungroup',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectIds = (params.objectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
if (objectIds.length === 0) throw new Error('At least one group object ID is required')
return { requests: [{ ungroupObjects: { objectIds } }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to ungroup objects')
}
const presentationId = params?.presentationId?.trim() || ''
const objectIds = (params?.objectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
return {
success: true,
output: {
ungrouped: true,
objectIds,
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
ungrouped: { type: 'boolean', description: 'Whether the objects were ungrouped' },
objectIds: {
type: 'array',
description: 'Group IDs that were ungrouped',
items: { type: 'string' },
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,137 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUnmergeTableCellsTool')
interface UnmergeTableCellsParams {
accessToken: string
presentationId: string
objectId: string
rowIndex: number
columnIndex: number
rowSpan: number
columnSpan: number
}
interface UnmergeTableCellsResponse {
success: boolean
output: {
unmerged: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const unmergeTableCellsTool: ToolConfig<UnmergeTableCellsParams, UnmergeTableCellsResponse> =
{
id: 'google_slides_unmerge_table_cells',
name: 'Unmerge Table Cells in Google Slides',
description: 'Unmerge any merged cells within the given table range.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the top-left cell of the range',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the top-left cell of the range',
},
rowSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows in the range (minimum 1)',
},
columnSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns in the range (minimum 1)',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
if (!params.rowSpan || params.rowSpan < 1) throw new Error('rowSpan must be at least 1')
if (!params.columnSpan || params.columnSpan < 1)
throw new Error('columnSpan must be at least 1')
return {
requests: [
{
unmergeTableCells: {
objectId,
tableRange: {
location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
rowSpan: params.rowSpan,
columnSpan: params.columnSpan,
},
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to unmerge table cells')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
unmerged: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
unmerged: { type: 'boolean', description: 'Whether the cells were unmerged' },
objectId: { type: 'string', description: 'The table updated' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,267 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateImagePropertiesTool')
interface UpdateImagePropertiesParams {
accessToken: string
presentationId: string
objectId: string
brightness?: number
contrast?: number
transparency?: number
linkUrl?: string
outlineColor?: string
outlineWeight?: number
outlineDashStyle?: string
cropLeftOffset?: number
cropRightOffset?: number
cropTopOffset?: number
cropBottomOffset?: number
cropAngle?: number
propertiesJson?: string
fields?: string
}
interface UpdateImagePropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateImagePropertiesTool: ToolConfig<
UpdateImagePropertiesParams,
UpdateImagePropertiesResponse
> = {
id: 'google_slides_update_image_properties',
name: 'Update Image Properties in Google Slides',
description:
'Update image properties — brightness, contrast, transparency, crop, outline, link — on an existing image.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the image to update',
},
brightness: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Brightness adjustment between -1.0 and 1.0',
},
contrast: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Contrast adjustment between -1.0 and 1.0',
},
transparency: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Transparency between 0.0 (opaque) and 1.0 (fully transparent)',
},
linkUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Make the image a hyperlink to this URL',
},
outlineColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline color as hex (e.g. #1A73E8)',
},
outlineWeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Outline weight in points',
},
outlineDashStyle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT',
},
cropLeftOffset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Crop offset from left edge (0.0 to 1.0)',
},
cropRightOffset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Crop offset from right edge (0.0 to 1.0)',
},
cropTopOffset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Crop offset from top edge (0.0 to 1.0)',
},
cropBottomOffset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Crop offset from bottom edge (0.0 to 1.0)',
},
cropAngle: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Crop rotation angle in radians (clockwise)',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw ImageProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.brightness !== undefined) {
props.brightness = params.brightness
fieldList.push('brightness')
}
if (params.contrast !== undefined) {
props.contrast = params.contrast
fieldList.push('contrast')
}
if (params.transparency !== undefined) {
props.transparency = params.transparency
fieldList.push('transparency')
}
if (params.linkUrl) {
props.link = { url: params.linkUrl }
fieldList.push('link')
}
const outline: Record<string, unknown> = {}
const outlineColor = hexToOpaqueColor(params.outlineColor)
if (outlineColor) {
outline.outlineFill = { solidFill: { color: outlineColor } }
}
if (params.outlineWeight !== undefined) {
outline.weight = { magnitude: params.outlineWeight, unit: 'PT' }
}
if (params.outlineDashStyle) {
outline.dashStyle = params.outlineDashStyle
}
if (Object.keys(outline).length > 0) {
props.outline = outline
fieldList.push('outline')
}
const crop: Record<string, unknown> = {}
if (params.cropLeftOffset !== undefined) crop.leftOffset = params.cropLeftOffset
if (params.cropRightOffset !== undefined) crop.rightOffset = params.cropRightOffset
if (params.cropTopOffset !== undefined) crop.topOffset = params.cropTopOffset
if (params.cropBottomOffset !== undefined) crop.bottomOffset = params.cropBottomOffset
if (params.cropAngle !== undefined) crop.angle = params.cropAngle
if (Object.keys(crop).length > 0) {
props.cropProperties = crop
fieldList.push('cropProperties')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') {
Object.assign(props, extra)
}
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [
{
updateImageProperties: { objectId, imageProperties: props, fields },
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update image properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the image properties were updated' },
objectId: { type: 'string', description: 'The image object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,115 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateLineCategoryTool')
interface UpdateLineCategoryParams {
accessToken: string
presentationId: string
objectId: string
lineCategory: 'STRAIGHT' | 'BENT' | 'CURVED'
}
interface UpdateLineCategoryResponse {
success: boolean
output: {
updated: boolean
objectId: string
lineCategory: string
metadata: { presentationId: string; url: string }
}
}
export const updateLineCategoryTool: ToolConfig<
UpdateLineCategoryParams,
UpdateLineCategoryResponse
> = {
id: 'google_slides_update_line_category',
name: 'Update Line Category in Google Slides',
description: "Change a connector line's category (STRAIGHT, BENT, or CURVED).",
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the connector line',
},
lineCategory: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New line category: STRAIGHT, BENT, or CURVED',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
if (!params.lineCategory) throw new Error('Line category is required')
return {
requests: [
{
updateLineCategory: {
objectId,
lineCategory: params.lineCategory,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update line category')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
lineCategory: params?.lineCategory ?? '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the line category was updated' },
objectId: { type: 'string', description: 'The line object updated' },
lineCategory: { type: 'string', description: 'New line category' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,201 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateLinePropertiesTool')
interface UpdateLinePropertiesParams {
accessToken: string
presentationId: string
objectId: string
lineColor?: string
lineWeight?: number
dashStyle?: string
startArrow?: string
endArrow?: string
linkUrl?: string
propertiesJson?: string
fields?: string
}
interface UpdateLinePropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateLinePropertiesTool: ToolConfig<
UpdateLinePropertiesParams,
UpdateLinePropertiesResponse
> = {
id: 'google_slides_update_line_properties',
name: 'Update Line Properties in Google Slides',
description: 'Update line appearance — color, weight, dash style, arrows, link.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the line',
},
lineColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Line color as hex',
},
lineWeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Line weight in points',
},
dashStyle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT',
},
startArrow: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Start arrow style: NONE, STEALTH_ARROW, FILL_ARROW, FILL_CIRCLE, FILL_SQUARE, FILL_DIAMOND, OPEN_ARROW, OPEN_CIRCLE, OPEN_SQUARE, OPEN_DIAMOND',
},
endArrow: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End arrow style (same values as startArrow)',
},
linkUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Hyperlink URL',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw LineProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
const color = hexToOpaqueColor(params.lineColor)
if (color) {
props.lineFill = { solidFill: { color } }
fieldList.push('lineFill')
}
if (params.lineWeight !== undefined) {
props.weight = { magnitude: params.lineWeight, unit: 'PT' }
fieldList.push('weight')
}
if (params.dashStyle) {
props.dashStyle = params.dashStyle
fieldList.push('dashStyle')
}
if (params.startArrow) {
props.startArrow = params.startArrow
fieldList.push('startArrow')
}
if (params.endArrow) {
props.endArrow = params.endArrow
fieldList.push('endArrow')
}
if (params.linkUrl) {
props.link = { url: params.linkUrl }
fieldList.push('link')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [{ updateLineProperties: { objectId, lineProperties: props, fields } }],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update line properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the line properties were updated' },
objectId: { type: 'string', description: 'The line object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,114 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdatePageElementAltTextTool')
interface UpdatePageElementAltTextParams {
accessToken: string
presentationId: string
objectId: string
title?: string
description?: string
}
interface UpdatePageElementAltTextResponse {
success: boolean
output: {
updated: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const updatePageElementAltTextTool: ToolConfig<
UpdatePageElementAltTextParams,
UpdatePageElementAltTextResponse
> = {
id: 'google_slides_update_page_element_alt_text',
name: 'Update Alt Text in Google Slides',
description:
'Set the accessibility title and/or description (alt text) of a page element such as an image, shape, or group.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the page element',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Accessibility title for the element',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Accessibility description (alt text) for the element',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const updateRequest: Record<string, unknown> = { objectId }
if (params.title !== undefined) updateRequest.title = params.title
if (params.description !== undefined) updateRequest.description = params.description
return { requests: [{ updatePageElementAltText: updateRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update alt text')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether alt text was updated' },
objectId: { type: 'string', description: 'The element updated' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,171 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
PT_TO_EMU,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdatePageElementTransformTool')
interface UpdatePageElementTransformParams {
accessToken: string
presentationId: string
objectId: string
scaleX?: number
scaleY?: number
shearX?: number
shearY?: number
translateX?: number
translateY?: number
applyMode?: 'ABSOLUTE' | 'RELATIVE'
}
interface UpdatePageElementTransformResponse {
success: boolean
output: {
updated: boolean
objectId: string
metadata: { presentationId: string; url: string }
}
}
export const updatePageElementTransformTool: ToolConfig<
UpdatePageElementTransformParams,
UpdatePageElementTransformResponse
> = {
id: 'google_slides_update_page_element_transform',
name: 'Update Page Element Transform in Google Slides',
description:
'Move, resize, scale, or shear a page element. Translate is specified in points; applyMode controls whether the transform is absolute (default) or relative (multiplied with the current transform).',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the page element to transform',
},
scaleX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Horizontal scale factor (default 1)',
},
scaleY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Vertical scale factor (default 1)',
},
shearX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Horizontal shear factor (default 0)',
},
shearY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Vertical shear factor (default 0)',
},
translateX: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'X position in points (absolute) or delta (relative)',
},
translateY: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Y position in points (absolute) or delta (relative)',
},
applyMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ABSOLUTE replaces the current transform; RELATIVE multiplies with it. Default ABSOLUTE.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const transform: Record<string, unknown> = {
unit: 'EMU',
}
transform.scaleX = params.scaleX ?? 1
transform.scaleY = params.scaleY ?? 1
if (params.shearX !== undefined) transform.shearX = params.shearX
if (params.shearY !== undefined) transform.shearY = params.shearY
if (params.translateX !== undefined) transform.translateX = params.translateX * PT_TO_EMU
if (params.translateY !== undefined) transform.translateY = params.translateY * PT_TO_EMU
return {
requests: [
{
updatePageElementTransform: {
objectId,
transform,
applyMode: params.applyMode || 'ABSOLUTE',
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update transform')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the transform was updated' },
objectId: { type: 'string', description: 'The element transformed' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,122 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdatePageElementsZOrderTool')
interface UpdatePageElementsZOrderParams {
accessToken: string
presentationId: string
objectIds: string
operation: 'BRING_TO_FRONT' | 'BRING_FORWARD' | 'SEND_BACKWARD' | 'SEND_TO_BACK'
}
interface UpdatePageElementsZOrderResponse {
success: boolean
output: {
reordered: boolean
objectIds: string[]
operation: string
metadata: { presentationId: string; url: string }
}
}
export const updatePageElementsZOrderTool: ToolConfig<
UpdatePageElementsZOrderParams,
UpdatePageElementsZOrderResponse
> = {
id: 'google_slides_update_page_elements_z_order',
name: 'Update Z-Order in Google Slides',
description: 'Bring elements to front, send to back, or step them one layer forward/backward.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated object IDs of the elements to reorder',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'BRING_TO_FRONT, BRING_FORWARD, SEND_BACKWARD, or SEND_TO_BACK',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectIds = (params.objectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
if (objectIds.length === 0) throw new Error('At least one object ID is required')
if (!params.operation) throw new Error('Operation is required')
return {
requests: [
{
updatePageElementsZOrder: {
pageElementObjectIds: objectIds,
operation: params.operation,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update z-order')
}
const presentationId = params?.presentationId?.trim() || ''
const objectIds = (params?.objectIds || '')
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
return {
success: true,
output: {
reordered: true,
objectIds,
operation: params?.operation ?? '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
reordered: { type: 'boolean', description: 'Whether the z-order was changed' },
objectIds: { type: 'array', description: 'Elements reordered', items: { type: 'string' } },
operation: { type: 'string', description: 'Operation applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,185 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdatePagePropertiesTool')
interface UpdatePagePropertiesParams {
accessToken: string
presentationId: string
objectId: string
backgroundColor?: string
backgroundAlpha?: number
backgroundImageUrl?: string
backgroundUnset?: boolean
propertiesJson?: string
fields?: string
}
interface UpdatePagePropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updatePagePropertiesTool: ToolConfig<
UpdatePagePropertiesParams,
UpdatePagePropertiesResponse
> = {
id: 'google_slides_update_page_properties',
name: 'Update Page Properties in Google Slides',
description:
'Update slide/page background — solid color or stretched picture — and other page properties.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the slide/page to update',
},
backgroundColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Solid background color as hex (e.g. #0B1F3A)',
},
backgroundAlpha: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Background fill opacity between 0.0 and 1.0',
},
backgroundImageUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Publicly fetchable image URL to use as a stretched picture background',
},
backgroundUnset: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, removes the background so the slide inherits its layout background',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw PageProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.backgroundUnset) {
props.pageBackgroundFill = { propertyState: 'NOT_RENDERED' }
fieldList.push('pageBackgroundFill.propertyState')
} else if (params.backgroundImageUrl?.trim()) {
props.pageBackgroundFill = {
stretchedPictureFill: { contentUrl: params.backgroundImageUrl.trim() },
propertyState: 'RENDERED',
}
fieldList.push('pageBackgroundFill')
} else {
const bg = hexToOpaqueColor(params.backgroundColor)
if (bg) {
props.pageBackgroundFill = {
solidFill: {
color: bg,
...(params.backgroundAlpha !== undefined ? { alpha: params.backgroundAlpha } : {}),
},
propertyState: 'RENDERED',
}
fieldList.push('pageBackgroundFill')
}
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [{ updatePageProperties: { objectId, pageProperties: props, fields } }],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update page properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the page properties were updated' },
objectId: { type: 'string', description: 'The page object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,288 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildCellLocation,
buildTextRange,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateParagraphStyleTool')
interface UpdateParagraphStyleParams {
accessToken: string
presentationId: string
objectId: string
rowIndex?: number
columnIndex?: number
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
alignment?: 'START' | 'CENTER' | 'END' | 'JUSTIFIED'
lineSpacing?: number
indentStart?: number
indentEnd?: number
indentFirstLine?: number
spaceAbove?: number
spaceBelow?: number
direction?: 'LEFT_TO_RIGHT' | 'RIGHT_TO_LEFT'
spacingMode?: 'NEVER_COLLAPSE' | 'COLLAPSE_LISTS'
styleJson?: string
fields?: string
}
interface UpdateParagraphStyleResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateParagraphStyleTool: ToolConfig<
UpdateParagraphStyleParams,
UpdateParagraphStyleResponse
> = {
id: 'google_slides_update_paragraph_style',
name: 'Update Paragraph Style in Google Slides',
description:
'Update paragraph styling — alignment, line spacing, indents, space above/below — for text in a shape or table cell.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape or table containing the text',
},
rowIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based row index',
},
columnIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based column index',
},
rangeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Range to style: ALL (default), FROM_START_INDEX, or FIXED_RANGE',
},
startIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start index for FROM_START_INDEX or FIXED_RANGE',
},
endIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End index for FIXED_RANGE',
},
alignment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text alignment: START, CENTER, END, or JUSTIFIED',
},
lineSpacing: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Line spacing as a percentage (100 = single, 200 = double)',
},
indentStart: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start-edge indent in points',
},
indentEnd: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End-edge indent in points',
},
indentFirstLine: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'First-line indent in points',
},
spaceAbove: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Space above the paragraph in points',
},
spaceBelow: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Space below the paragraph in points',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text direction: LEFT_TO_RIGHT or RIGHT_TO_LEFT',
},
spacingMode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Spacing mode: NEVER_COLLAPSE or COLLAPSE_LISTS',
},
styleJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw ParagraphStyle JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const style: Record<string, unknown> = {}
const fieldList: string[] = []
const ptDim = (pt: number) => ({ magnitude: pt, unit: 'PT' })
if (params.alignment) {
style.alignment = params.alignment
fieldList.push('alignment')
}
if (params.lineSpacing !== undefined) {
style.lineSpacing = params.lineSpacing
fieldList.push('lineSpacing')
}
if (params.indentStart !== undefined) {
style.indentStart = ptDim(params.indentStart)
fieldList.push('indentStart')
}
if (params.indentEnd !== undefined) {
style.indentEnd = ptDim(params.indentEnd)
fieldList.push('indentEnd')
}
if (params.indentFirstLine !== undefined) {
style.indentFirstLine = ptDim(params.indentFirstLine)
fieldList.push('indentFirstLine')
}
if (params.spaceAbove !== undefined) {
style.spaceAbove = ptDim(params.spaceAbove)
fieldList.push('spaceAbove')
}
if (params.spaceBelow !== undefined) {
style.spaceBelow = ptDim(params.spaceBelow)
fieldList.push('spaceBelow')
}
if (params.direction) {
style.direction = params.direction
fieldList.push('direction')
}
if (params.spacingMode) {
style.spacingMode = params.spacingMode
fieldList.push('spacingMode')
}
if (params.styleJson?.trim()) {
try {
const extra = JSON.parse(params.styleJson)
if (extra && typeof extra === 'object') {
Object.assign(style, extra)
}
} catch (e) {
logger.warn('Invalid styleJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
const updateRequest: Record<string, unknown> = {
objectId,
style,
textRange: buildTextRange({
rangeType: params.rangeType,
startIndex: params.startIndex,
endIndex: params.endIndex,
}),
fields,
}
const cellLocation = buildCellLocation({
rowIndex: params.rowIndex,
columnIndex: params.columnIndex,
})
if (cellLocation) updateRequest.cellLocation = cellLocation
return { requests: [{ updateParagraphStyle: updateRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update paragraph style')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the paragraph style was updated' },
objectId: { type: 'string', description: 'The object whose paragraph was styled' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,246 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateShapePropertiesTool')
interface UpdateShapePropertiesParams {
accessToken: string
presentationId: string
objectId: string
fillColor?: string
fillAlpha?: number
fillUnset?: boolean
outlineColor?: string
outlineWeight?: number
outlineDashStyle?: string
outlineUnset?: boolean
linkUrl?: string
contentAlignment?: 'TOP' | 'MIDDLE' | 'BOTTOM'
autofitType?: 'NONE' | 'TEXT_AUTOFIT' | 'SHAPE_AUTOFIT'
propertiesJson?: string
fields?: string
}
interface UpdateShapePropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateShapePropertiesTool: ToolConfig<
UpdateShapePropertiesParams,
UpdateShapePropertiesResponse
> = {
id: 'google_slides_update_shape_properties',
name: 'Update Shape Properties in Google Slides',
description:
"Update a shape's appearance — background fill color, outline, link, content alignment, autofit. Pass only the properties you want to change.",
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape to update',
},
fillColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Solid background fill color as hex (e.g. #FF6F61)',
},
fillAlpha: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Fill opacity between 0.0 (transparent) and 1.0 (opaque)',
},
fillUnset: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, removes any fill so the shape inherits its layout/master fill',
},
outlineColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline color as hex',
},
outlineWeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Outline weight in points',
},
outlineDashStyle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT',
},
outlineUnset: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'When true, removes any outline so the shape inherits its layout/master outline',
},
linkUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Make the shape a hyperlink to this URL',
},
contentAlignment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Vertical alignment of shape contents: TOP, MIDDLE, or BOTTOM',
},
autofitType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Autofit behavior: NONE, TEXT_AUTOFIT, or SHAPE_AUTOFIT',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw ShapeProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
const fill = hexToOpaqueColor(params.fillColor)
if (params.fillUnset) {
props.shapeBackgroundFill = { propertyState: 'NOT_RENDERED' }
fieldList.push('shapeBackgroundFill.propertyState')
} else if (fill) {
props.shapeBackgroundFill = {
solidFill: {
color: fill,
...(params.fillAlpha !== undefined ? { alpha: params.fillAlpha } : {}),
},
propertyState: 'RENDERED',
}
fieldList.push('shapeBackgroundFill')
}
const outlineColor = hexToOpaqueColor(params.outlineColor)
if (params.outlineUnset) {
props.outline = { propertyState: 'NOT_RENDERED' }
fieldList.push('outline.propertyState')
} else if (outlineColor || params.outlineWeight !== undefined || params.outlineDashStyle) {
const outline: Record<string, unknown> = { propertyState: 'RENDERED' }
if (outlineColor) outline.outlineFill = { solidFill: { color: outlineColor } }
if (params.outlineWeight !== undefined)
outline.weight = { magnitude: params.outlineWeight, unit: 'PT' }
if (params.outlineDashStyle) outline.dashStyle = params.outlineDashStyle
props.outline = outline
fieldList.push('outline')
}
if (params.linkUrl) {
props.link = { url: params.linkUrl }
fieldList.push('link')
}
if (params.contentAlignment) {
props.contentAlignment = params.contentAlignment
fieldList.push('contentAlignment')
}
if (params.autofitType) {
props.autofit = { autofitType: params.autofitType }
fieldList.push('autofit.autofitType')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [{ updateShapeProperties: { objectId, shapeProperties: props, fields } }],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update shape properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the shape properties were updated' },
objectId: { type: 'string', description: 'The shape object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,140 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateSlidePropertiesTool')
interface UpdateSlidePropertiesParams {
accessToken: string
presentationId: string
objectId: string
isSkipped?: boolean
propertiesJson?: string
fields?: string
}
interface UpdateSlidePropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateSlidePropertiesTool: ToolConfig<
UpdateSlidePropertiesParams,
UpdateSlidePropertiesResponse
> = {
id: 'google_slides_update_slide_properties',
name: 'Update Slide Properties in Google Slides',
description:
'Update slide-specific properties such as whether the slide is skipped during presentation.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the slide to update',
},
isSkipped: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the slide is skipped in presentation mode',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw SlideProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.isSkipped !== undefined) {
props.isSkipped = params.isSkipped
fieldList.push('isSkipped')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [{ updateSlideProperties: { objectId, slideProperties: props, fields } }],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update slide properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the slide properties were updated' },
objectId: { type: 'string', description: 'The slide object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,171 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateSlidesPositionTool')
interface UpdateSlidesPositionParams {
accessToken: string
presentationId: string
slideObjectIds: string
insertionIndex: number
}
interface UpdateSlidesPositionResponse {
success: boolean
output: {
moved: boolean
slideObjectIds: string[]
insertionIndex: number
metadata: {
presentationId: string
url: string
}
}
}
export const updateSlidesPositionTool: ToolConfig<
UpdateSlidesPositionParams,
UpdateSlidesPositionResponse
> = {
id: 'google_slides_update_slides_position',
name: 'Reorder Slides in Google Slides',
description: 'Move one or more slides to a new position in a Google Slides presentation',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
slideObjectIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of slide object IDs to move. The slides will maintain their relative order.',
},
insertionIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description:
'The zero-based index where the slides should be moved. All slides with indices greater than or equal to this will be shifted right.',
},
},
request: {
url: (params) => {
const presentationId = params.presentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
return `https://slides.googleapis.com/v1/presentations/${presentationId}: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.slideObjectIds?.trim()) {
throw new Error('Slide object IDs are required')
}
if (params.insertionIndex === undefined || params.insertionIndex < 0) {
throw new Error('Insertion index must be a non-negative number')
}
const slideObjectIds = params.slideObjectIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
if (slideObjectIds.length === 0) {
throw new Error('At least one slide object ID is required')
}
return {
requests: [
{
updateSlidesPosition: {
slideObjectIds,
insertionIndex: params.insertionIndex,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update slides position')
}
const presentationId = params?.presentationId?.trim() || ''
const slideObjectIds =
params?.slideObjectIds
?.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0) ?? []
return {
success: true,
output: {
moved: true,
slideObjectIds,
insertionIndex: params?.insertionIndex ?? 0,
metadata: {
presentationId,
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
},
},
}
},
outputs: {
moved: {
type: 'boolean',
description: 'Whether the slides were successfully moved',
},
slideObjectIds: {
type: 'array',
description: 'The slide object IDs that were moved',
items: {
type: 'string',
},
},
insertionIndex: {
type: 'number',
description: 'The index where the slides were moved to',
},
metadata: {
type: 'object',
description: 'Operation metadata including presentation ID and URL',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,227 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateTableBorderPropertiesTool')
interface UpdateTableBorderPropertiesParams {
accessToken: string
presentationId: string
objectId: string
rowIndex: number
columnIndex: number
rowSpan: number
columnSpan: number
borderPosition?:
| 'ALL'
| 'BOTTOM'
| 'INNER'
| 'INNER_HORIZONTAL'
| 'INNER_VERTICAL'
| 'LEFT'
| 'OUTER'
| 'RIGHT'
| 'TOP'
borderColor?: string
borderWeight?: number
dashStyle?: string
propertiesJson?: string
fields?: string
}
interface UpdateTableBorderPropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateTableBorderPropertiesTool: ToolConfig<
UpdateTableBorderPropertiesParams,
UpdateTableBorderPropertiesResponse
> = {
id: 'google_slides_update_table_border_properties',
name: 'Update Table Border Properties in Google Slides',
description:
'Update border color, weight, and dash style for a position (e.g. ALL, INNER, OUTER) in a table range.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the top-left cell of the range',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the top-left cell of the range',
},
rowSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows in the range (minimum 1)',
},
columnSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns in the range (minimum 1)',
},
borderPosition: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Which borders to update: ALL (default), BOTTOM, INNER, INNER_HORIZONTAL, INNER_VERTICAL, LEFT, OUTER, RIGHT, TOP',
},
borderColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Border color as hex',
},
borderWeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Border weight in points',
},
dashStyle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Dash style: SOLID, DOT, DASH, DASH_DOT, LONG_DASH, LONG_DASH_DOT',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw TableBorderProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
const color = hexToOpaqueColor(params.borderColor)
if (color) {
props.tableBorderFill = { solidFill: { color } }
fieldList.push('tableBorderFill')
}
if (params.borderWeight !== undefined) {
props.weight = { magnitude: params.borderWeight, unit: 'PT' }
fieldList.push('weight')
}
if (params.dashStyle) {
props.dashStyle = params.dashStyle
fieldList.push('dashStyle')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [
{
updateTableBorderProperties: {
objectId,
tableRange: {
location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
rowSpan: params.rowSpan,
columnSpan: params.columnSpan,
},
borderPosition: params.borderPosition || 'ALL',
tableBorderProperties: props,
fields,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update table border properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the border properties were updated' },
objectId: { type: 'string', description: 'The table updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,210 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateTableCellPropertiesTool')
interface UpdateTableCellPropertiesParams {
accessToken: string
presentationId: string
objectId: string
rowIndex: number
columnIndex: number
rowSpan: number
columnSpan: number
backgroundColor?: string
backgroundAlpha?: number
contentAlignment?: 'TOP' | 'MIDDLE' | 'BOTTOM'
propertiesJson?: string
fields?: string
}
interface UpdateTableCellPropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateTableCellPropertiesTool: ToolConfig<
UpdateTableCellPropertiesParams,
UpdateTableCellPropertiesResponse
> = {
id: 'google_slides_update_table_cell_properties',
name: 'Update Table Cell Properties in Google Slides',
description: 'Update background fill and content alignment for a range of table cells.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based row index of the top-left cell of the range',
},
columnIndex: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Zero-based column index of the top-left cell of the range',
},
rowSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of rows in the range (minimum 1)',
},
columnSpan: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Number of columns in the range (minimum 1)',
},
backgroundColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Cell background color as hex (e.g. #F1F3F4)',
},
backgroundAlpha: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Background fill opacity between 0.0 and 1.0',
},
contentAlignment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Vertical alignment of cell content: TOP, MIDDLE, or BOTTOM',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw TableCellProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
const bg = hexToOpaqueColor(params.backgroundColor)
if (bg) {
props.tableCellBackgroundFill = {
solidFill: {
color: bg,
...(params.backgroundAlpha !== undefined ? { alpha: params.backgroundAlpha } : {}),
},
propertyState: 'RENDERED',
}
fieldList.push('tableCellBackgroundFill')
}
if (params.contentAlignment) {
props.contentAlignment = params.contentAlignment
fieldList.push('contentAlignment')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [
{
updateTableCellProperties: {
objectId,
tableRange: {
location: { rowIndex: params.rowIndex, columnIndex: params.columnIndex },
rowSpan: params.rowSpan,
columnSpan: params.columnSpan,
},
tableCellProperties: props,
fields,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update table cell properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the cell properties were updated' },
objectId: { type: 'string', description: 'The table updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,158 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateTableColumnPropertiesTool')
interface UpdateTableColumnPropertiesParams {
accessToken: string
presentationId: string
objectId: string
columnIndices: string
columnWidth?: number
propertiesJson?: string
fields?: string
}
interface UpdateTableColumnPropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateTableColumnPropertiesTool: ToolConfig<
UpdateTableColumnPropertiesParams,
UpdateTableColumnPropertiesResponse
> = {
id: 'google_slides_update_table_column_properties',
name: 'Update Table Column Properties in Google Slides',
description: 'Update column widths and other column-level table properties.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
columnIndices: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated zero-based column indices to update (e.g. "0,2,3")',
},
columnWidth: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Column width in points',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw TableColumnProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
const columnIndices = (params.columnIndices || '')
.split(',')
.map((s) => Number.parseInt(s.trim(), 10))
.filter((n) => Number.isFinite(n) && n >= 0)
if (columnIndices.length === 0) throw new Error('At least one column index is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.columnWidth !== undefined) {
props.columnWidth = { magnitude: params.columnWidth, unit: 'PT' }
fieldList.push('columnWidth')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [
{
updateTableColumnProperties: {
objectId,
columnIndices,
tableColumnProperties: props,
fields,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update table column properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the column properties were updated' },
objectId: { type: 'string', description: 'The table updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,158 @@
import { createLogger } from '@sim/logger'
import { authJsonHeaders, batchUpdateUrl, presentationUrl } from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateTableRowPropertiesTool')
interface UpdateTableRowPropertiesParams {
accessToken: string
presentationId: string
objectId: string
rowIndices: string
minRowHeight?: number
propertiesJson?: string
fields?: string
}
interface UpdateTableRowPropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateTableRowPropertiesTool: ToolConfig<
UpdateTableRowPropertiesParams,
UpdateTableRowPropertiesResponse
> = {
id: 'google_slides_update_table_row_properties',
name: 'Update Table Row Properties in Google Slides',
description: 'Update minimum row heights and other row-level table properties.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the table',
},
rowIndices: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated zero-based row indices to update (e.g. "0,2,3")',
},
minRowHeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Minimum row height in points',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw TableRowProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Table object ID is required')
const rowIndices = (params.rowIndices || '')
.split(',')
.map((s) => Number.parseInt(s.trim(), 10))
.filter((n) => Number.isFinite(n) && n >= 0)
if (rowIndices.length === 0) throw new Error('At least one row index is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.minRowHeight !== undefined) {
props.minRowHeight = { magnitude: params.minRowHeight, unit: 'PT' }
fieldList.push('minRowHeight')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [
{
updateTableRowProperties: {
objectId,
rowIndices,
tableRowProperties: props,
fields,
},
},
],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update table row properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the row properties were updated' },
objectId: { type: 'string', description: 'The table updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,311 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
buildCellLocation,
buildTextRange,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateTextStyleTool')
interface UpdateTextStyleParams {
accessToken: string
presentationId: string
objectId: string
rowIndex?: number
columnIndex?: number
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
bold?: boolean
italic?: boolean
underline?: boolean
strikethrough?: boolean
smallCaps?: boolean
fontFamily?: string
fontSize?: number
foregroundColor?: string
backgroundColor?: string
linkUrl?: string
baselineOffset?: 'NONE' | 'SUPERSCRIPT' | 'SUBSCRIPT'
styleJson?: string
fields?: string
}
interface UpdateTextStyleResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateTextStyleTool: ToolConfig<UpdateTextStyleParams, UpdateTextStyleResponse> = {
id: 'google_slides_update_text_style',
name: 'Update Text Style in Google Slides',
description:
'Update the styling of text in a shape or table cell (bold, italic, font family, font size, foreground/background color, link, etc.). Only the fields you set are applied.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the shape or table containing the text',
},
rowIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based row index',
},
columnIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When targeting a table cell, the zero-based column index',
},
rangeType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Range to style: ALL (default), FROM_START_INDEX, or FIXED_RANGE',
},
startIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start index for FROM_START_INDEX or FIXED_RANGE',
},
endIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End index for FIXED_RANGE',
},
bold: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the text is bold',
},
italic: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the text is italic',
},
underline: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the text is underlined',
},
strikethrough: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the text has strikethrough',
},
smallCaps: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the text is rendered in small caps',
},
fontFamily: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Font family name (must be a font available to Google Slides)',
},
fontSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Font size in points',
},
foregroundColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text color as hex (e.g. #1A73E8)',
},
backgroundColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text background color as hex (e.g. #FFF8E1)',
},
linkUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Convert the range to a hyperlink with this URL',
},
baselineOffset: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Baseline offset: NONE, SUPERSCRIPT, or SUBSCRIPT',
},
styleJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Advanced: raw TextStyle JSON merged with the simple fields above (overrides them on conflict)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Advanced: explicit FieldMask. If omitted, the mask is computed from the fields you provided (or "*" when styleJson is used without explicit fields).',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const style: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.bold !== undefined) {
style.bold = params.bold
fieldList.push('bold')
}
if (params.italic !== undefined) {
style.italic = params.italic
fieldList.push('italic')
}
if (params.underline !== undefined) {
style.underline = params.underline
fieldList.push('underline')
}
if (params.strikethrough !== undefined) {
style.strikethrough = params.strikethrough
fieldList.push('strikethrough')
}
if (params.smallCaps !== undefined) {
style.smallCaps = params.smallCaps
fieldList.push('smallCaps')
}
if (params.fontFamily) {
style.fontFamily = params.fontFamily
fieldList.push('fontFamily')
}
if (params.fontSize !== undefined) {
style.fontSize = { magnitude: params.fontSize, unit: 'PT' }
fieldList.push('fontSize')
}
const fg = hexToOpaqueColor(params.foregroundColor)
if (fg) {
style.foregroundColor = { opaqueColor: fg }
fieldList.push('foregroundColor')
}
const bg = hexToOpaqueColor(params.backgroundColor)
if (bg) {
style.backgroundColor = { opaqueColor: bg }
fieldList.push('backgroundColor')
}
if (params.linkUrl) {
style.link = { url: params.linkUrl }
fieldList.push('link')
}
if (params.baselineOffset) {
style.baselineOffset = params.baselineOffset
fieldList.push('baselineOffset')
}
if (params.styleJson?.trim()) {
try {
const extra = JSON.parse(params.styleJson)
if (extra && typeof extra === 'object') {
Object.assign(style, extra)
}
} catch (e) {
logger.warn('Invalid styleJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
const updateRequest: Record<string, unknown> = {
objectId,
style,
textRange: buildTextRange({
rangeType: params.rangeType,
startIndex: params.startIndex,
endIndex: params.endIndex,
}),
fields,
}
const cellLocation = buildCellLocation({
rowIndex: params.rowIndex,
columnIndex: params.columnIndex,
})
if (cellLocation) updateRequest.cellLocation = cellLocation
return { requests: [{ updateTextStyle: updateRequest }] }
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update text style')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the text style was updated' },
objectId: { type: 'string', description: 'The object whose text was styled' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
@@ -0,0 +1,208 @@
import { createLogger } from '@sim/logger'
import {
authJsonHeaders,
batchUpdateUrl,
hexToOpaqueColor,
presentationUrl,
} from '@/tools/google_slides/utils'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesUpdateVideoPropertiesTool')
interface UpdateVideoPropertiesParams {
accessToken: string
presentationId: string
objectId: string
autoPlay?: boolean
mute?: boolean
start?: number
end?: number
outlineColor?: string
outlineWeight?: number
outlineDashStyle?: string
propertiesJson?: string
fields?: string
}
interface UpdateVideoPropertiesResponse {
success: boolean
output: {
updated: boolean
objectId: string
fields: string
metadata: { presentationId: string; url: string }
}
}
export const updateVideoPropertiesTool: ToolConfig<
UpdateVideoPropertiesParams,
UpdateVideoPropertiesResponse
> = {
id: 'google_slides_update_video_properties',
name: 'Update Video Properties in Google Slides',
description: 'Update video playback options (autoPlay, mute, start/end) and outline.',
version: '1.0.0',
oauth: { required: true, provider: 'google-drive' },
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Object ID of the video',
},
autoPlay: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Play the video automatically when the slide is shown',
},
mute: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Mute the video',
},
start: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Playback start time in seconds',
},
end: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Playback end time in seconds',
},
outlineColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline color as hex',
},
outlineWeight: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Outline weight in points',
},
outlineDashStyle: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Outline dash style',
},
propertiesJson: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: raw VideoProperties JSON merged with the simple fields above',
},
fields: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Advanced: explicit FieldMask. If omitted, computed from provided fields.',
},
},
request: {
url: (params) => batchUpdateUrl(params.presentationId),
method: 'POST',
headers: (params) => authJsonHeaders(params.accessToken),
body: (params) => {
const objectId = params.objectId?.trim()
if (!objectId) throw new Error('Object ID is required')
const props: Record<string, unknown> = {}
const fieldList: string[] = []
if (params.autoPlay !== undefined) {
props.autoPlay = params.autoPlay
fieldList.push('autoPlay')
}
if (params.mute !== undefined) {
props.mute = params.mute
fieldList.push('mute')
}
if (params.start !== undefined) {
props.start = params.start
fieldList.push('start')
}
if (params.end !== undefined) {
props.end = params.end
fieldList.push('end')
}
const outlineColor = hexToOpaqueColor(params.outlineColor)
if (outlineColor || params.outlineWeight !== undefined || params.outlineDashStyle) {
const outline: Record<string, unknown> = { propertyState: 'RENDERED' }
if (outlineColor) outline.outlineFill = { solidFill: { color: outlineColor } }
if (params.outlineWeight !== undefined)
outline.weight = { magnitude: params.outlineWeight, unit: 'PT' }
if (params.outlineDashStyle) outline.dashStyle = params.outlineDashStyle
props.outline = outline
fieldList.push('outline')
}
if (params.propertiesJson?.trim()) {
try {
const extra = JSON.parse(params.propertiesJson)
if (extra && typeof extra === 'object') Object.assign(props, extra)
} catch (e) {
logger.warn('Invalid propertiesJson, ignoring:', { error: e })
}
}
const fields = params.fields?.trim() || (fieldList.length > 0 ? fieldList.join(',') : '*')
return {
requests: [{ updateVideoProperties: { objectId, videoProperties: props, fields } }],
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Google Slides API error:', { data })
throw new Error(data.error?.message || 'Failed to update video properties')
}
const presentationId = params?.presentationId?.trim() || ''
return {
success: true,
output: {
updated: true,
objectId: params?.objectId?.trim() || '',
fields: params?.fields?.trim() || '',
metadata: { presentationId, url: presentationUrl(presentationId) },
},
}
},
outputs: {
updated: { type: 'boolean', description: 'Whether the video properties were updated' },
objectId: { type: 'string', description: 'The video object updated' },
fields: { type: 'string', description: 'FieldMask applied' },
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
presentationId: { type: 'string', description: 'The presentation ID' },
url: { type: 'string', description: 'URL to the presentation' },
},
},
},
}
+136
View File
@@ -0,0 +1,136 @@
import { generateRandomString } from '@sim/utils/random'
/** 1 point = 12700 EMU (English Metric Units). */
export const PT_TO_EMU = 12700
export interface OpaqueColor {
rgbColor: { red: number; green: number; blue: number }
}
export interface TextRangeInput {
rangeType?: 'ALL' | 'FROM_START_INDEX' | 'FIXED_RANGE'
startIndex?: number
endIndex?: number
}
export interface CellLocationInput {
rowIndex?: number
columnIndex?: number
}
/**
* Convert a hex color string (`#RRGGBB`, `#RGB`, or bare hex) into the
* Google Slides API's OpaqueColor shape with rgbColor scaled 0-1.
* Returns null when input is empty/invalid.
*/
export function hexToOpaqueColor(input?: string | null): OpaqueColor | null {
if (!input) return null
let hex = input.trim().replace(/^#/, '')
if (hex.length === 3) {
hex = hex
.split('')
.map((c) => c + c)
.join('')
}
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null
const r = Number.parseInt(hex.slice(0, 2), 16)
const g = Number.parseInt(hex.slice(2, 4), 16)
const b = Number.parseInt(hex.slice(4, 6), 16)
return {
rgbColor: {
red: r / 255,
green: g / 255,
blue: b / 255,
},
}
}
/**
* Build a Slides API TextRange. Defaults to range type ALL.
* `FROM_START_INDEX` requires startIndex; `FIXED_RANGE` requires both indices.
*/
export function buildTextRange(input: TextRangeInput | undefined) {
const rangeType = input?.rangeType ?? 'ALL'
if (rangeType === 'FROM_START_INDEX') {
return { type: 'FROM_START_INDEX', startIndex: input?.startIndex ?? 0 }
}
if (rangeType === 'FIXED_RANGE') {
return {
type: 'FIXED_RANGE',
startIndex: input?.startIndex ?? 0,
endIndex: input?.endIndex ?? 0,
}
}
return { type: 'ALL' }
}
/**
* Build an optional cellLocation if both row and column indices are provided.
* Slides API treats absence as targeting the shape itself (not a table cell).
*/
export function buildCellLocation(input: CellLocationInput | undefined) {
if (input?.rowIndex === undefined || input?.columnIndex === undefined) return undefined
return { rowIndex: input.rowIndex, columnIndex: input.columnIndex }
}
/** Generate a stable-enough object ID prefixed by kind. */
export function generateObjectId(kind: string): string {
return `${kind}_${Date.now()}_${generateRandomString(7)}`
}
/** Convert a points value (or undefined) to an EMU Dimension object. */
export function ptToEmuDimension(pt: number | undefined, fallbackPt: number) {
return {
magnitude: (pt ?? fallbackPt) * PT_TO_EMU,
unit: 'EMU',
}
}
/** Build a standard PageElementProperties with size + transform from points. */
export function buildElementProperties(opts: {
pageObjectId: string
width?: number
height?: number
positionX?: number
positionY?: number
defaultWidth?: number
defaultHeight?: number
defaultX?: number
defaultY?: number
}) {
return {
pageObjectId: opts.pageObjectId,
size: {
width: ptToEmuDimension(opts.width, opts.defaultWidth ?? 200),
height: ptToEmuDimension(opts.height, opts.defaultHeight ?? 100),
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: (opts.positionX ?? opts.defaultX ?? 100) * PT_TO_EMU,
translateY: (opts.positionY ?? opts.defaultY ?? 100) * PT_TO_EMU,
unit: 'EMU',
},
}
}
/** Standard presentation URL for embedding in metadata. */
export function presentationUrl(presentationId: string): string {
return `https://docs.google.com/presentation/d/${presentationId}/edit`
}
/** Standard fetch headers for Slides API JSON calls. */
export function authJsonHeaders(accessToken: string) {
if (!accessToken) throw new Error('Access token is required')
return {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
}
}
/** Resolve the batchUpdate URL for a given presentation. */
export function batchUpdateUrl(presentationId: string | undefined): string {
const id = presentationId?.trim()
if (!id) throw new Error('Presentation ID is required')
return `https://slides.googleapis.com/v1/presentations/${id}:batchUpdate`
}
+255
View File
@@ -0,0 +1,255 @@
import { createLogger } from '@sim/logger'
import type { GoogleSlidesToolParams, GoogleSlidesWriteResponse } from '@/tools/google_slides/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleSlidesWriteTool')
export const writeTool: ToolConfig<GoogleSlidesToolParams, GoogleSlidesWriteResponse> = {
id: 'google_slides_write',
name: 'Write to Google Slides Presentation',
description: 'Write or update content in a Google Slides presentation',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Google Slides API',
},
presentationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Slides presentation ID',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The content to write to the slide',
},
slideIndex: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The index of the slide to write to (defaults to first slide)',
},
},
request: {
url: (params) => {
// Ensure presentationId is valid
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
// First, we'll read the presentation to get slide information
return `https://slides.googleapis.com/v1/presentations/${presentationId}`
},
method: 'GET',
headers: (params) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
postProcess: async (result, params, _executeTool) => {
if (!result.success) {
return result
}
// Validate content
if (!params.content) {
throw new Error('Content is required')
}
const presentationId = params.presentationId?.trim() || params.manualPresentationId?.trim()
if (!presentationId) {
throw new Error('Presentation ID is required')
}
try {
// Get the presentation data from the initial read
const presentationData = await fetch(
`https://slides.googleapis.com/v1/presentations/${presentationId}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${params.accessToken}`,
},
}
).then((res) => res.json())
const metadata = {
presentationId,
title: presentationData.title || 'Updated Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${presentationId}/edit`,
}
const slideIndex =
typeof params.slideIndex === 'string'
? Number.parseInt(params.slideIndex, 10)
: (params.slideIndex ?? 0)
const slide = presentationData.slides?.[slideIndex]
if (Number.isNaN(slideIndex) || slideIndex < 0) {
return {
success: false,
error: 'Slide index must be a non-negative number',
output: {
updatedContent: false,
metadata,
},
}
}
if (!slide) {
return {
success: false,
error: `Slide at index ${slideIndex} not found`,
output: {
updatedContent: false,
metadata,
},
}
}
// Create requests to add content to the slide
const textBoxId = `textbox_${Date.now()}`
const requests = [
{
createShape: {
objectId: textBoxId,
shapeType: 'TEXT_BOX',
elementProperties: {
pageObjectId: slide.objectId,
size: {
width: {
magnitude: 400,
unit: 'PT',
},
height: {
magnitude: 100,
unit: 'PT',
},
},
transform: {
scaleX: 1,
scaleY: 1,
translateX: 50,
translateY: 100,
unit: 'PT',
},
},
},
},
{
insertText: {
objectId: textBoxId,
text: params.content,
insertionIndex: 0,
},
},
]
// Make the batchUpdate request
const updateResponse = await fetch(
`https://slides.googleapis.com/v1/presentations/${presentationId}:batchUpdate`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}
)
if (!updateResponse.ok) {
const errorText = await updateResponse.text()
logger.error('Failed to update presentation:', { errorText })
return {
success: false,
error: 'Failed to update presentation',
output: {
updatedContent: false,
metadata,
},
}
}
return {
success: true,
output: {
updatedContent: true,
metadata,
},
}
} catch (error) {
logger.error('Error in postProcess:', { error })
throw error
}
},
outputs: {
updatedContent: {
type: 'boolean',
description: 'Indicates if presentation content was updated successfully',
},
metadata: {
type: 'json',
description: 'Updated presentation metadata including ID, title, and URL',
properties: {
presentationId: {
type: 'string',
description: 'The presentation ID',
},
title: {
type: 'string',
description: 'The presentation title',
},
mimeType: {
type: 'string',
description: 'The mime type of the presentation',
},
url: {
type: 'string',
description: 'URL to open the presentation',
},
},
},
},
transformResponse: async (response: Response) => {
// This is just for the initial read, the actual response comes from postProcess
const data = await response.json()
const metadata = {
presentationId: data.presentationId,
title: data.title || 'Presentation',
mimeType: 'application/vnd.google-apps.presentation',
url: `https://docs.google.com/presentation/d/${data.presentationId}/edit`,
}
return {
success: true,
output: {
updatedContent: false,
metadata,
},
}
},
}