chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
GoogleDocsCreateNamedRangeResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildContentRange,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createNamedRangeTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsCreateNamedRangeResponse
|
||||
> = {
|
||||
id: 'google_docs_create_named_range',
|
||||
name: 'Create Named Range in Google Docs Document',
|
||||
description:
|
||||
'Create a named range over a span of content in a Google Docs document so it can be referenced or deleted later. The name may be 1-256 characters and need not be unique.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the range to create (1-256 characters)',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range (exclusive)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 name = params.name ? String(params.name).trim() : ''
|
||||
if (!name) {
|
||||
throw new Error('name is required')
|
||||
}
|
||||
if (name.length > 256) {
|
||||
throw new Error('name must be 256 characters or fewer')
|
||||
}
|
||||
const range = buildContentRange(params.startIndex, params.endIndex)
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createNamedRange: { name, range },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
const namedRangeId = data.replies?.[0]?.createNamedRange?.namedRangeId ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
namedRangeId,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
namedRangeId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the created named range',
|
||||
optional: true,
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type {
|
||||
GoogleDocsCreateParagraphBulletsResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildContentRange,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const BULLET_PRESETS = new Set([
|
||||
'BULLET_DISC_CIRCLE_SQUARE',
|
||||
'BULLET_DIAMONDX_ARROW3D_SQUARE',
|
||||
'BULLET_CHECKBOX',
|
||||
'BULLET_ARROW_DIAMOND_DISC',
|
||||
'BULLET_STAR_CIRCLE_SQUARE',
|
||||
'BULLET_ARROW3D_CIRCLE_SQUARE',
|
||||
'NUMBERED_DECIMAL_ALPHA_ROMAN',
|
||||
'NUMBERED_DECIMAL_ALPHA_ROMAN_PARENS',
|
||||
'NUMBERED_DECIMAL_NESTED',
|
||||
'NUMBERED_UPPERALPHA_ALPHA_ROMAN',
|
||||
'NUMBERED_UPPERROMAN_UPPERALPHA_DECIMAL',
|
||||
'NUMBERED_ZERODECIMAL_ALPHA_ROMAN',
|
||||
])
|
||||
|
||||
const DEFAULT_BULLET_PRESET = 'BULLET_DISC_CIRCLE_SQUARE'
|
||||
|
||||
export const createParagraphBulletsTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsCreateParagraphBulletsResponse
|
||||
> = {
|
||||
id: 'google_docs_create_paragraph_bullets',
|
||||
name: 'Create Paragraph Bullets in Google Docs Document',
|
||||
description:
|
||||
'Add bulleted or numbered list formatting to the paragraphs overlapping a range of text in a Google Docs document, using a chosen bullet glyph preset.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range to bullet (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range to bullet (exclusive)',
|
||||
},
|
||||
bulletPreset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The bullet glyph preset to apply. Defaults to BULLET_DISC_CIRCLE_SQUARE. Examples: BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN, NUMBERED_DECIMAL_NESTED.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 range = buildContentRange(params.startIndex, params.endIndex)
|
||||
|
||||
let bulletPreset = DEFAULT_BULLET_PRESET
|
||||
if (params.bulletPreset != null && String(params.bulletPreset).trim() !== '') {
|
||||
bulletPreset = String(params.bulletPreset).trim().toUpperCase()
|
||||
if (!BULLET_PRESETS.has(bulletPreset)) {
|
||||
throw new Error(
|
||||
'bulletPreset must be a valid BulletGlyphPreset (e.g. BULLET_DISC_CIRCLE_SQUARE, BULLET_CHECKBOX, NUMBERED_DECIMAL_ALPHA_ROMAN)'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
createParagraphBullets: { range, bulletPreset },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the bullets were applied successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateShortId } from '@sim/utils/id'
|
||||
import type { GoogleDocsCreateResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleDocsCreateTool')
|
||||
|
||||
const DOC_MIME_TYPE = 'application/vnd.google-apps.document'
|
||||
|
||||
/**
|
||||
* Build a multipart/related body for Drive's files.create upload endpoint.
|
||||
* Used when converting Markdown to a Google Doc in a single round-trip.
|
||||
* See: https://developers.google.com/workspace/drive/api/guides/manage-uploads
|
||||
*/
|
||||
function buildMarkdownMultipartBody(
|
||||
metadata: Record<string, unknown>,
|
||||
markdownContent: string,
|
||||
boundary: string
|
||||
): string {
|
||||
return (
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Type: application/json; charset=UTF-8\r\n\r\n` +
|
||||
`${JSON.stringify(metadata)}\r\n` +
|
||||
`--${boundary}\r\n` +
|
||||
`Content-Type: text/markdown\r\n\r\n` +
|
||||
`${markdownContent}\r\n` +
|
||||
`--${boundary}--`
|
||||
)
|
||||
}
|
||||
|
||||
function shouldUseMarkdownUpload(params: GoogleDocsToolParams): boolean {
|
||||
return Boolean(params.markdown && params.content)
|
||||
}
|
||||
|
||||
export const createTool: ToolConfig<GoogleDocsToolParams, GoogleDocsCreateResponse> = {
|
||||
id: 'google_docs_create',
|
||||
name: 'Create Google Docs Document',
|
||||
description: 'Create a new Google Docs document',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The title of the document to create',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The content of the document to create',
|
||||
},
|
||||
folderSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Drive folder ID to create the document in (e.g., 1ABCxyz...)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the folder to create the document in (internal use)',
|
||||
},
|
||||
markdown: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'When true, content is interpreted as Markdown and converted to formatted Google Docs content (headings, bold/italic, lists, tables, links, code blocks, blockquotes). Default: false (content inserted as plain text).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
return shouldUseMarkdownUpload(params)
|
||||
? 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true'
|
||||
: 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
if (shouldUseMarkdownUpload(params)) {
|
||||
const boundary = `sim_gdocs_md_${generateShortId(24)}`
|
||||
// Stash on params so body() uses the matching boundary string
|
||||
;(params as GoogleDocsToolParams & { _boundary?: string })._boundary = boundary
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': `multipart/related; boundary=${boundary}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.title) {
|
||||
throw new Error('Title is required')
|
||||
}
|
||||
|
||||
const folderId = params.folderSelector || params.folderId
|
||||
const metadata: Record<string, unknown> = {
|
||||
name: params.title,
|
||||
mimeType: DOC_MIME_TYPE,
|
||||
}
|
||||
if (folderId) {
|
||||
metadata.parents = [folderId]
|
||||
}
|
||||
|
||||
if (shouldUseMarkdownUpload(params)) {
|
||||
const boundary = (params as GoogleDocsToolParams & { _boundary?: string })._boundary
|
||||
if (!boundary) {
|
||||
// headers() runs before body() in formatRequestParams and stashes the boundary
|
||||
// on the same params reference. Missing _boundary means that contract was broken,
|
||||
// which would silently produce a Content-Type / body boundary mismatch (HTTP 400).
|
||||
// Throw loudly instead of fabricating a mismatched boundary.
|
||||
throw new Error(
|
||||
'Multipart boundary missing on params — headers() must run before body() for markdown upload'
|
||||
)
|
||||
}
|
||||
return buildMarkdownMultipartBody(metadata, params.content ?? '', boundary)
|
||||
}
|
||||
|
||||
return metadata
|
||||
},
|
||||
},
|
||||
|
||||
postProcess: async (result, params, executeTool) => {
|
||||
if (!result.success) {
|
||||
return result
|
||||
}
|
||||
|
||||
const documentId = result.output.metadata.documentId
|
||||
|
||||
// When the markdown upload path ran, content was already inserted via Drive's
|
||||
// text/markdown import conversion during files.create — no follow-up write needed.
|
||||
if (shouldUseMarkdownUpload(params)) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (params.content && documentId) {
|
||||
try {
|
||||
const writeParams = {
|
||||
accessToken: params.accessToken,
|
||||
documentId: documentId,
|
||||
content: params.content,
|
||||
}
|
||||
|
||||
const writeResult = await executeTool('google_docs_write', writeParams)
|
||||
|
||||
if (!writeResult.success) {
|
||||
logger.warn(
|
||||
'Failed to add content to document, but document was created:',
|
||||
writeResult.error
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error adding content to document:', { 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 documentId = data.id
|
||||
const title = data.name
|
||||
|
||||
const metadata = {
|
||||
documentId,
|
||||
title: title || 'Untitled Document',
|
||||
mimeType: DOC_MIME_TYPE,
|
||||
url: `https://docs.google.com/document/d/${documentId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Google Docs create - Error processing response:', {
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
GoogleDocsDeleteContentRangeResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildContentRange,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteContentRangeTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsDeleteContentRangeResponse
|
||||
> = {
|
||||
id: 'google_docs_delete_content_range',
|
||||
name: 'Delete Content Range in Google Docs Document',
|
||||
description:
|
||||
'Delete all content between a start and end character index in a Google Docs document. The endIndex is exclusive and must be greater than the startIndex.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to delete content from',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range to delete (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range to delete (exclusive)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 range = buildContentRange(params.startIndex, params.endIndex)
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
deleteContentRange: { range },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the content range was deleted successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
GoogleDocsDeleteNamedRangeResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import { buildBatchUpdateMetadata, resolveDocumentId } from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteNamedRangeTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsDeleteNamedRangeResponse
|
||||
> = {
|
||||
id: 'google_docs_delete_named_range',
|
||||
name: 'Delete Named Range in Google Docs Document',
|
||||
description:
|
||||
'Delete one or more named ranges from a Google Docs document by their ID or by name. Provide exactly one of namedRangeId or name; deleting by name removes all ranges sharing that name. The content itself is not removed.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
namedRangeId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The ID of the named range to delete. Provide exactly one of namedRangeId or namedRangeName.',
|
||||
},
|
||||
namedRangeName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The name of the named range(s) to delete. All ranges sharing this name are removed. Provide exactly one of namedRangeId or namedRangeName.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 namedRangeId = params.namedRangeId ? String(params.namedRangeId).trim() : ''
|
||||
const name = params.namedRangeName ? String(params.namedRangeName).trim() : ''
|
||||
if (!namedRangeId && !name) {
|
||||
throw new Error('Either namedRangeId or namedRangeName is required')
|
||||
}
|
||||
if (namedRangeId && name) {
|
||||
throw new Error('Provide exactly one of namedRangeId or namedRangeName, not both')
|
||||
}
|
||||
const deleteNamedRange = namedRangeId ? { namedRangeId } : { name }
|
||||
return {
|
||||
requests: [{ deleteNamedRange }],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the named range(s) were deleted successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
GoogleDocsDeleteParagraphBulletsResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildContentRange,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteParagraphBulletsTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsDeleteParagraphBulletsResponse
|
||||
> = {
|
||||
id: 'google_docs_delete_paragraph_bullets',
|
||||
name: 'Delete Paragraph Bullets in Google Docs Document',
|
||||
description:
|
||||
'Remove bullet or numbered list formatting from the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range to clear bullets from (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range to clear bullets from (exclusive)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 range = buildContentRange(params.startIndex, params.endIndex)
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
deleteParagraphBullets: { range },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the bullets were removed successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createTool } from '@/tools/google_docs/create'
|
||||
import { createNamedRangeTool } from '@/tools/google_docs/create-named-range'
|
||||
import { createParagraphBulletsTool } from '@/tools/google_docs/create-paragraph-bullets'
|
||||
import { deleteContentRangeTool } from '@/tools/google_docs/delete-content-range'
|
||||
import { deleteNamedRangeTool } from '@/tools/google_docs/delete-named-range'
|
||||
import { deleteParagraphBulletsTool } from '@/tools/google_docs/delete-paragraph-bullets'
|
||||
import { insertImageTool } from '@/tools/google_docs/insert-image'
|
||||
import { insertPageBreakTool } from '@/tools/google_docs/insert-page-break'
|
||||
import { insertTableTool } from '@/tools/google_docs/insert-table'
|
||||
import { insertTextTool } from '@/tools/google_docs/insert-text'
|
||||
import { readTool } from '@/tools/google_docs/read'
|
||||
import { replaceTextTool } from '@/tools/google_docs/replace-text'
|
||||
import { updateParagraphStyleTool } from '@/tools/google_docs/update-paragraph-style'
|
||||
import { updateTextStyleTool } from '@/tools/google_docs/update-text-style'
|
||||
import { writeTool } from '@/tools/google_docs/write'
|
||||
|
||||
export const googleDocsReadTool = readTool
|
||||
export const googleDocsWriteTool = writeTool
|
||||
export const googleDocsCreateTool = createTool
|
||||
export const googleDocsInsertTextTool = insertTextTool
|
||||
export const googleDocsReplaceTextTool = replaceTextTool
|
||||
export const googleDocsInsertTableTool = insertTableTool
|
||||
export const googleDocsInsertImageTool = insertImageTool
|
||||
export const googleDocsInsertPageBreakTool = insertPageBreakTool
|
||||
export const googleDocsUpdateTextStyleTool = updateTextStyleTool
|
||||
export const googleDocsDeleteContentRangeTool = deleteContentRangeTool
|
||||
export const googleDocsUpdateParagraphStyleTool = updateParagraphStyleTool
|
||||
export const googleDocsCreateParagraphBulletsTool = createParagraphBulletsTool
|
||||
export const googleDocsDeleteParagraphBulletsTool = deleteParagraphBulletsTool
|
||||
export const googleDocsCreateNamedRangeTool = createNamedRangeTool
|
||||
export const googleDocsDeleteNamedRangeTool = deleteNamedRangeTool
|
||||
|
||||
export * from './types'
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { GoogleDocsInsertImageResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildInsertLocation,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const insertImageTool: ToolConfig<GoogleDocsToolParams, GoogleDocsInsertImageResponse> = {
|
||||
id: 'google_docs_insert_image',
|
||||
name: 'Insert Image into Google Docs Document',
|
||||
description:
|
||||
'Insert an inline image from a public URL into a Google Docs document. The image must be publicly accessible and under 50 MB. When no index is provided, the image is appended to the end of the document.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to insert the image into',
|
||||
},
|
||||
imageUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The publicly accessible URL of the image to insert',
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The character index (the document body starts at index 1) at which to insert the image. When omitted, the image is appended to the end of the document.',
|
||||
},
|
||||
width: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional image width in points (PT)',
|
||||
},
|
||||
height: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional image height in points (PT)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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.imageUrl) {
|
||||
throw new Error('Image URL is required')
|
||||
}
|
||||
|
||||
const insertInlineImage: Record<string, unknown> = {
|
||||
...buildInsertLocation(params.index),
|
||||
uri: params.imageUrl,
|
||||
}
|
||||
|
||||
const objectSize: Record<string, unknown> = {}
|
||||
if (typeof params.width === 'number' && Number.isFinite(params.width)) {
|
||||
objectSize.width = { magnitude: params.width, unit: 'PT' }
|
||||
}
|
||||
if (typeof params.height === 'number' && Number.isFinite(params.height)) {
|
||||
objectSize.height = { magnitude: params.height, unit: 'PT' }
|
||||
}
|
||||
if (Object.keys(objectSize).length > 0) {
|
||||
insertInlineImage.objectSize = objectSize
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [{ insertInlineImage }],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
const objectId = data.replies?.[0]?.insertInlineImage?.objectId ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
objectId,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
objectId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the inserted inline image object',
|
||||
optional: true,
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type {
|
||||
GoogleDocsInsertPageBreakResponse,
|
||||
GoogleDocsToolParams,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildInsertLocation,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const insertPageBreakTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsInsertPageBreakResponse
|
||||
> = {
|
||||
id: 'google_docs_insert_page_break',
|
||||
name: 'Insert Page Break into Google Docs Document',
|
||||
description:
|
||||
'Insert a page break into a Google Docs document. When no index is provided, the page break is appended to the end of the document.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to insert the page break into',
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The character index (the document body starts at index 1) at which to insert the page break. When omitted, the page break is appended to the end of the document.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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) => {
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
insertPageBreak: {
|
||||
...buildInsertLocation(params.index),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the page break was inserted successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { GoogleDocsInsertTableResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildInsertLocation,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const insertTableTool: ToolConfig<GoogleDocsToolParams, GoogleDocsInsertTableResponse> = {
|
||||
id: 'google_docs_insert_table',
|
||||
name: 'Insert Table into Google Docs Document',
|
||||
description:
|
||||
'Insert an empty table with the given number of rows and columns into a Google Docs document. When no index is provided, the table is appended to the end of the document.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to insert the table into',
|
||||
},
|
||||
rows: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The number of rows in the table',
|
||||
},
|
||||
columns: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The number of columns in the table',
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The character index (the document body starts at index 1) at which to insert the table. When omitted, the table is appended to the end of the document.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 rows = Number(params.rows)
|
||||
const columns = Number(params.columns)
|
||||
if (!Number.isFinite(rows) || rows < 1) {
|
||||
throw new Error('Rows must be a positive number')
|
||||
}
|
||||
if (!Number.isFinite(columns) || columns < 1) {
|
||||
throw new Error('Columns must be a positive number')
|
||||
}
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
insertTable: {
|
||||
...buildInsertLocation(params.index),
|
||||
rows,
|
||||
columns,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the table was inserted successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { GoogleDocsInsertTextResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildInsertLocation,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const insertTextTool: ToolConfig<GoogleDocsToolParams, GoogleDocsInsertTextResponse> = {
|
||||
id: 'google_docs_insert_text',
|
||||
name: 'Insert Text into Google Docs Document',
|
||||
description:
|
||||
'Insert text at a specific index in a Google Docs document. When no index is provided, text is appended to the end of the document. Text is inserted literally; Markdown is not interpreted.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to insert text into',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to insert',
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The character index (the document body starts at index 1) at which to insert the text. When omitted, text is appended to the end of the document.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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.text) {
|
||||
throw new Error('Text is required')
|
||||
}
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
insertText: {
|
||||
...buildInsertLocation(params.index),
|
||||
text: params.text,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if text was inserted successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { GoogleDocsReadResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import { extractTextFromDocument } from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const readTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReadResponse> = {
|
||||
id: 'google_docs_read',
|
||||
name: 'Read Google Docs Document',
|
||||
description: 'Read content from a Google Docs document',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Docs document ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure documentId is valid
|
||||
const documentId = params.documentId?.trim() || params.manualDocumentId?.trim()
|
||||
if (!documentId) {
|
||||
throw new Error('Document ID is required')
|
||||
}
|
||||
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}`
|
||||
},
|
||||
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 document content from the response
|
||||
let content = ''
|
||||
if (data.body?.content) {
|
||||
content = extractTextFromDocument(data)
|
||||
}
|
||||
|
||||
// Create document metadata
|
||||
const metadata = {
|
||||
documentId: data.documentId,
|
||||
title: data.title || 'Untitled Document',
|
||||
mimeType: 'application/vnd.google-apps.document',
|
||||
url: `https://docs.google.com/document/d/${data.documentId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Extracted document text content' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { GoogleDocsReplaceTextResponse, GoogleDocsToolParams } from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
parseOptionalBoolean,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const replaceTextTool: ToolConfig<GoogleDocsToolParams, GoogleDocsReplaceTextResponse> = {
|
||||
id: 'google_docs_replace_text',
|
||||
name: 'Find and Replace Text in Google Docs Document',
|
||||
description:
|
||||
'Replace all occurrences of a search string with new text across a Google Docs document.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
searchText: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to find',
|
||||
},
|
||||
replaceText: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text to replace matches with. Use an empty string to delete matches.',
|
||||
},
|
||||
matchCase: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the search should be case sensitive. Defaults to false.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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.searchText) {
|
||||
throw new Error('Search text is required')
|
||||
}
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
replaceAllText: {
|
||||
containsText: {
|
||||
text: params.searchText,
|
||||
matchCase: parseOptionalBoolean(params.matchCase) ?? false,
|
||||
},
|
||||
replaceText: params.replaceText ?? '',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
const occurrencesChanged = Number(data.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
occurrencesChanged,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
occurrencesChanged: {
|
||||
type: 'number',
|
||||
description: 'The number of occurrences that were replaced',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDocsMetadata {
|
||||
documentId: string
|
||||
title: string
|
||||
mimeType?: string
|
||||
createdTime?: string
|
||||
modifiedTime?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface GoogleDocsReadResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsWriteResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsCreateResponse extends ToolResponse {
|
||||
output: {
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsInsertTextResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsReplaceTextResponse extends ToolResponse {
|
||||
output: {
|
||||
occurrencesChanged: number
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsInsertTableResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsInsertImageResponse extends ToolResponse {
|
||||
output: {
|
||||
objectId: string | null
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsInsertPageBreakResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsUpdateTextStyleResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsDeleteContentRangeResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsUpdateParagraphStyleResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsCreateParagraphBulletsResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsDeleteParagraphBulletsResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsCreateNamedRangeResponse extends ToolResponse {
|
||||
output: {
|
||||
namedRangeId: string | null
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsDeleteNamedRangeResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: GoogleDocsMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDocsToolParams {
|
||||
accessToken: string
|
||||
documentId?: string
|
||||
manualDocumentId?: string
|
||||
title?: string
|
||||
content?: string
|
||||
folderId?: string
|
||||
folderSelector?: string
|
||||
markdown?: boolean
|
||||
text?: string
|
||||
index?: number
|
||||
searchText?: string
|
||||
replaceText?: string
|
||||
matchCase?: boolean
|
||||
rows?: number
|
||||
columns?: number
|
||||
imageUrl?: string
|
||||
width?: number
|
||||
height?: number
|
||||
startIndex?: number
|
||||
endIndex?: number
|
||||
bold?: boolean
|
||||
italic?: boolean
|
||||
underline?: boolean
|
||||
fontSize?: number
|
||||
namedStyleType?: string
|
||||
alignment?: string
|
||||
bulletPreset?: string
|
||||
name?: string
|
||||
namedRangeId?: string
|
||||
namedRangeName?: string
|
||||
}
|
||||
|
||||
export type GoogleDocsResponse =
|
||||
| GoogleDocsReadResponse
|
||||
| GoogleDocsWriteResponse
|
||||
| GoogleDocsCreateResponse
|
||||
| GoogleDocsInsertTextResponse
|
||||
| GoogleDocsReplaceTextResponse
|
||||
| GoogleDocsInsertTableResponse
|
||||
| GoogleDocsInsertImageResponse
|
||||
| GoogleDocsInsertPageBreakResponse
|
||||
| GoogleDocsUpdateTextStyleResponse
|
||||
| GoogleDocsDeleteContentRangeResponse
|
||||
| GoogleDocsUpdateParagraphStyleResponse
|
||||
| GoogleDocsCreateParagraphBulletsResponse
|
||||
| GoogleDocsDeleteParagraphBulletsResponse
|
||||
| GoogleDocsCreateNamedRangeResponse
|
||||
| GoogleDocsDeleteNamedRangeResponse
|
||||
@@ -0,0 +1,180 @@
|
||||
import type {
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsUpdateParagraphStyleResponse,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
buildContentRange,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const NAMED_STYLE_TYPES = new Set([
|
||||
'NORMAL_TEXT',
|
||||
'TITLE',
|
||||
'SUBTITLE',
|
||||
'HEADING_1',
|
||||
'HEADING_2',
|
||||
'HEADING_3',
|
||||
'HEADING_4',
|
||||
'HEADING_5',
|
||||
'HEADING_6',
|
||||
])
|
||||
|
||||
/**
|
||||
* Maps user-facing alignment names to the Google Docs API `ParagraphStyle.alignment`
|
||||
* enum (the API rejects LEFT/RIGHT/JUSTIFY — the valid values are START/CENTER/END/JUSTIFIED).
|
||||
*/
|
||||
const ALIGNMENT_MAP: Record<string, string> = {
|
||||
LEFT: 'START',
|
||||
START: 'START',
|
||||
CENTER: 'CENTER',
|
||||
RIGHT: 'END',
|
||||
END: 'END',
|
||||
JUSTIFY: 'JUSTIFIED',
|
||||
JUSTIFIED: 'JUSTIFIED',
|
||||
}
|
||||
|
||||
export const updateParagraphStyleTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsUpdateParagraphStyleResponse
|
||||
> = {
|
||||
id: 'google_docs_update_paragraph_style',
|
||||
name: 'Update Paragraph Style in Google Docs Document',
|
||||
description:
|
||||
'Apply a named paragraph style (such as a heading or title) and/or alignment to the paragraphs overlapping a range of text in a Google Docs document, identified by its start and end character index.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range to style (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range to style (exclusive)',
|
||||
},
|
||||
namedStyleType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The named paragraph style to apply. One of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6.',
|
||||
},
|
||||
alignment: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The paragraph alignment to apply. One of: LEFT, CENTER, RIGHT, JUSTIFY.',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 range = buildContentRange(params.startIndex, params.endIndex)
|
||||
|
||||
const paragraphStyle: Record<string, unknown> = {}
|
||||
const fields: string[] = []
|
||||
|
||||
if (params.namedStyleType != null && String(params.namedStyleType).trim() !== '') {
|
||||
const namedStyleType = String(params.namedStyleType).trim().toUpperCase()
|
||||
if (!NAMED_STYLE_TYPES.has(namedStyleType)) {
|
||||
throw new Error(
|
||||
'namedStyleType must be one of: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6'
|
||||
)
|
||||
}
|
||||
paragraphStyle.namedStyleType = namedStyleType
|
||||
fields.push('namedStyleType')
|
||||
}
|
||||
|
||||
if (params.alignment != null && String(params.alignment).trim() !== '') {
|
||||
const alignment = ALIGNMENT_MAP[String(params.alignment).trim().toUpperCase()]
|
||||
if (!alignment) {
|
||||
throw new Error('alignment must be one of: LEFT, CENTER, RIGHT, JUSTIFY')
|
||||
}
|
||||
paragraphStyle.alignment = alignment
|
||||
fields.push('alignment')
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error('At least one of namedStyleType or alignment must be provided')
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
updateParagraphStyle: {
|
||||
range,
|
||||
paragraphStyle,
|
||||
fields: fields.join(','),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the paragraph style was applied successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type {
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsUpdateTextStyleResponse,
|
||||
} from '@/tools/google_docs/types'
|
||||
import {
|
||||
buildBatchUpdateMetadata,
|
||||
parseOptionalBoolean,
|
||||
resolveDocumentId,
|
||||
} from '@/tools/google_docs/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateTextStyleTool: ToolConfig<
|
||||
GoogleDocsToolParams,
|
||||
GoogleDocsUpdateTextStyleResponse
|
||||
> = {
|
||||
id: 'google_docs_update_text_style',
|
||||
name: 'Apply Text Style in Google Docs Document',
|
||||
description:
|
||||
'Apply bold, italic, underline, and/or font size to a range of text in a Google Docs document, identified by its start and end character index.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the document to update',
|
||||
},
|
||||
startIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The start character index (the document body starts at index 1) of the range to style (inclusive)',
|
||||
},
|
||||
endIndex: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The end character index of the range to style (exclusive)',
|
||||
},
|
||||
bold: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to make the text bold',
|
||||
},
|
||||
italic: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to make the text italic',
|
||||
},
|
||||
underline: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to underline the text',
|
||||
},
|
||||
fontSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The font size to apply, in points (PT)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
const documentId = resolveDocumentId(params)
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}: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 startIndex = Number(params.startIndex)
|
||||
const endIndex = Number(params.endIndex)
|
||||
if (!Number.isFinite(startIndex) || !Number.isFinite(endIndex)) {
|
||||
throw new Error('startIndex and endIndex are required')
|
||||
}
|
||||
if (endIndex <= startIndex) {
|
||||
throw new Error('endIndex must be greater than startIndex')
|
||||
}
|
||||
|
||||
const textStyle: Record<string, unknown> = {}
|
||||
const fields: string[] = []
|
||||
|
||||
const bold = parseOptionalBoolean(params.bold)
|
||||
if (bold !== undefined) {
|
||||
textStyle.bold = bold
|
||||
fields.push('bold')
|
||||
}
|
||||
const italic = parseOptionalBoolean(params.italic)
|
||||
if (italic !== undefined) {
|
||||
textStyle.italic = italic
|
||||
fields.push('italic')
|
||||
}
|
||||
const underline = parseOptionalBoolean(params.underline)
|
||||
if (underline !== undefined) {
|
||||
textStyle.underline = underline
|
||||
fields.push('underline')
|
||||
}
|
||||
const fontSize = Number(params.fontSize)
|
||||
if (params.fontSize != null && Number.isFinite(fontSize)) {
|
||||
textStyle.fontSize = { magnitude: fontSize, unit: 'PT' }
|
||||
fields.push('fontSize')
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
throw new Error(
|
||||
'At least one style (bold, italic, underline, or fontSize) must be provided'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
requests: [
|
||||
{
|
||||
updateTextStyle: {
|
||||
range: { startIndex, endIndex },
|
||||
textStyle,
|
||||
fields: fields.join(','),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
const data = responseText.trim() ? JSON.parse(responseText) : {}
|
||||
const metadata = buildBatchUpdateMetadata(data, response.url)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if the text style was applied successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Resolve the document ID for a Docs API request, preferring the selected
|
||||
* document ID and falling back to a manually entered one. Throws when neither
|
||||
* is present so callers fail loudly before issuing a request.
|
||||
*/
|
||||
export function resolveDocumentId(params: {
|
||||
documentId?: string
|
||||
manualDocumentId?: string
|
||||
}): string {
|
||||
const documentId = params.documentId?.trim() || params.manualDocumentId?.trim()
|
||||
if (!documentId) {
|
||||
throw new Error('Document ID is required')
|
||||
}
|
||||
return documentId
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an optional boolean param that may arrive as a real boolean or as a
|
||||
* string (`"true"`/`"false"`), which happens when values come from block inputs
|
||||
* or agent tool-calls rather than a typed UI switch. Returns `undefined` when the
|
||||
* value is absent or unrecognized so callers can skip the field entirely.
|
||||
*/
|
||||
export function parseOptionalBoolean(value: unknown): boolean | undefined {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase()
|
||||
if (normalized === 'true') return true
|
||||
if (normalized === 'false') return false
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a `Location` for a batchUpdate request when an insertion index is
|
||||
* provided, otherwise fall back to `endOfSegmentLocation` (end of the body).
|
||||
* The Docs API treats index 0 as invalid, so a positive index is required to
|
||||
* use an explicit location.
|
||||
*/
|
||||
export function buildInsertLocation(
|
||||
index?: number
|
||||
): { location: { index: number } } | { endOfSegmentLocation: Record<string, never> } {
|
||||
if (typeof index === 'number' && Number.isFinite(index) && index >= 1) {
|
||||
return { location: { index } }
|
||||
}
|
||||
return { endOfSegmentLocation: {} }
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and validate a Docs API `Range` from a start and end character index.
|
||||
* The Docs API requires `endIndex` to be strictly greater than `startIndex`.
|
||||
* Throws when either index is missing or the range is empty/inverted so callers
|
||||
* fail loudly before issuing a request.
|
||||
*/
|
||||
export function buildContentRange(
|
||||
startIndex: unknown,
|
||||
endIndex: unknown
|
||||
): { startIndex: number; endIndex: number } {
|
||||
const start = Number(startIndex)
|
||||
const end = Number(endIndex)
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
||||
throw new Error('startIndex and endIndex are required')
|
||||
}
|
||||
if (end <= start) {
|
||||
throw new Error('endIndex must be greater than startIndex')
|
||||
}
|
||||
return { startIndex: start, endIndex: end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical Google Docs metadata from a batchUpdate response. The
|
||||
* `documentId` is taken from the response body when present, otherwise parsed
|
||||
* from the request URL (`.../documents/{id}:batchUpdate`).
|
||||
*/
|
||||
export function buildBatchUpdateMetadata(
|
||||
data: { documentId?: string },
|
||||
responseUrl: string
|
||||
): { documentId: string; title: string; mimeType: string; url: string } {
|
||||
let documentId = data.documentId ?? ''
|
||||
if (!documentId) {
|
||||
const urlParts = responseUrl.split('/')
|
||||
for (let i = 0; i < urlParts.length; i++) {
|
||||
if (urlParts[i] === 'documents' && i + 1 < urlParts.length) {
|
||||
documentId = urlParts[i + 1].split(':')[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
documentId,
|
||||
title: 'Updated Document',
|
||||
mimeType: 'application/vnd.google-apps.document',
|
||||
url: `https://docs.google.com/document/d/${documentId}/edit`,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract text content from Google Docs document structure
|
||||
export function extractTextFromDocument(document: any): string {
|
||||
let text = ''
|
||||
|
||||
if (!document.body || !document.body.content) {
|
||||
return text
|
||||
}
|
||||
|
||||
// Process each structural element in the document
|
||||
for (const element of document.body.content) {
|
||||
if (element.paragraph) {
|
||||
for (const paragraphElement of element.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
} else if (element.table) {
|
||||
// Process tables if needed
|
||||
for (const tableRow of element.table.tableRows) {
|
||||
for (const tableCell of tableRow.tableCells) {
|
||||
if (tableCell.content) {
|
||||
for (const cellContent of tableCell.content) {
|
||||
if (cellContent.paragraph) {
|
||||
for (const paragraphElement of cellContent.paragraph.elements) {
|
||||
if (paragraphElement.textRun?.content) {
|
||||
text += paragraphElement.textRun.content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { GoogleDocsToolParams, GoogleDocsWriteResponse } from '@/tools/google_docs/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const writeTool: ToolConfig<GoogleDocsToolParams, GoogleDocsWriteResponse> = {
|
||||
id: 'google_docs_write',
|
||||
name: 'Write to Google Docs Document',
|
||||
description:
|
||||
'Append content to a Google Docs document. Content is inserted literally; Markdown is not interpreted. For formatted output from Markdown, use the Create operation with the markdown toggle enabled.',
|
||||
version: '1.0',
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-docs',
|
||||
},
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Docs API',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The ID of the document to write to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The content to write to the document',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Ensure documentId is valid
|
||||
const documentId = params.documentId?.trim() || params.manualDocumentId?.trim()
|
||||
if (!documentId) {
|
||||
throw new Error('Document ID is required')
|
||||
}
|
||||
|
||||
return `https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate`
|
||||
},
|
||||
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) => {
|
||||
// Validate content
|
||||
if (!params.content) {
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
// Following the exact format from the Google Docs API examples
|
||||
// Always insert at the end of the document to avoid duplication
|
||||
// See: https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertTextRequest
|
||||
const requestBody = {
|
||||
requests: [
|
||||
{
|
||||
insertText: {
|
||||
endOfSegmentLocation: {},
|
||||
text: params.content,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Indicates if document content was updated successfully',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated document metadata including ID, title, and URL',
|
||||
properties: {
|
||||
documentId: { type: 'string', description: 'Google Docs document ID' },
|
||||
title: { type: 'string', description: 'Document title' },
|
||||
mimeType: { type: 'string', description: 'Document MIME type' },
|
||||
url: { type: 'string', description: 'Document URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const responseText = await response.text()
|
||||
|
||||
// Parse the response if it's not empty
|
||||
let _data = {}
|
||||
if (responseText.trim()) {
|
||||
_data = JSON.parse(responseText)
|
||||
}
|
||||
|
||||
// Get the document ID from the URL
|
||||
const urlParts = response.url.split('/')
|
||||
let documentId = ''
|
||||
for (let i = 0; i < urlParts.length; i++) {
|
||||
if (urlParts[i] === 'documents' && i + 1 < urlParts.length) {
|
||||
documentId = urlParts[i + 1].split(':')[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create document metadata
|
||||
const metadata = {
|
||||
documentId,
|
||||
title: 'Updated Document',
|
||||
mimeType: 'application/vnd.google-apps.document',
|
||||
url: `https://docs.google.com/document/d/${documentId}/edit`,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updatedContent: true,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user